mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
refactor(core): centralize extension runtime refresh (#6152)
* refactor(core): centralize extension runtime refresh * test(core): add regression tests for allSettled and try/catch resilience in refreshExtensionRuntime * test(core): add restartMcpServers reject test and restore design rationale comments Address @wenshao's review feedback: - Add test for restartMcpServers rejection (the only fatal error path) - Restore key 'why' comments about allSettled and try/catch design decisions - Logger tag change to EXTENSION_RUNTIME_REFRESH is intentional (standalone module) * docs(core): add error-handling tier contract to refreshExtensionRuntime Add block comment documenting the three-tier error-handling contract (fatal/swallow/swallow) so future maintainers know which tier applies when adding new refresh steps. Addresses review feedback on #6152. --------- Co-authored-by: 俊良 <zzj542558@alibaba-inc.com> Co-authored-by: 易良 <1204183885@qq.com>
This commit is contained in:
parent
1ead34c1db
commit
6a726f6c63
4 changed files with 498 additions and 41 deletions
|
|
@ -0,0 +1,320 @@
|
|||
# Extension Runtime Refresh Implementation Plan
|
||||
|
||||
Tracking issue: https://github.com/QwenLM/qwen-code/issues/3696
|
||||
|
||||
Working branch: `feat/extension-runtime-refresh`
|
||||
|
||||
## Summary
|
||||
|
||||
Qwen Code should keep the existing user experience where extension UI
|
||||
mutations take effect automatically. Installing, updating, enabling, disabling,
|
||||
or uninstalling an extension should continue to refresh runtime state without
|
||||
requiring the user to run a manual command.
|
||||
|
||||
The remaining work is to make that automatic path complete, cheaper, and more
|
||||
visible to the model. A separate `/reload-plugins` path should be added later
|
||||
for external file changes that Qwen Code did not initiate, such as editing
|
||||
installed extension files by hand.
|
||||
|
||||
## Current Behavior
|
||||
|
||||
`ExtensionManager` already refreshes part of the runtime after extension
|
||||
mutations:
|
||||
|
||||
```text
|
||||
enable/disable/install/update/uninstall
|
||||
-> ExtensionManager.refreshTools()
|
||||
-> refreshMemory()
|
||||
-> ToolRegistry.restartMcpServers()
|
||||
-> SkillManager.refreshCache()
|
||||
-> SubagentManager.refreshCache()
|
||||
-> refreshHierarchicalMemory()
|
||||
```
|
||||
|
||||
This is useful but incomplete:
|
||||
|
||||
- MCP refresh is currently too expensive because it restarts all MCP servers.
|
||||
- Commands are rebuilt indirectly through UI-layer command reload behavior.
|
||||
- Hooks are not part of the extension runtime refresh orchestration.
|
||||
- LSP server configuration from extensions is not reinitialized at runtime.
|
||||
- Model-facing availability notices are asymmetric:
|
||||
- skills and model-invocable commands announce additions, but not removals;
|
||||
- MCP tools announce additions, but not removals;
|
||||
- agents update the Agent tool schema/description, but do not send an
|
||||
explicit added/removed agent reminder.
|
||||
|
||||
## Design Direction
|
||||
|
||||
### Keep Automatic Mutation Refresh
|
||||
|
||||
Extension UI mutations should remain automatic. This differs from Claude Code's
|
||||
plugin flow, where plugin mutations primarily set a stale flag and ask the user
|
||||
to run `/reload-plugins`.
|
||||
|
||||
For Qwen Code, automatic mutation refresh is the better default because it
|
||||
preserves the current UX: when a user toggles an extension, the extension should
|
||||
actually become usable or unusable in the running session.
|
||||
|
||||
### Add Manual Reload Only for External Changes
|
||||
|
||||
`/reload-plugins` should be a repair/resync path for file changes outside Qwen
|
||||
Code's mutation APIs:
|
||||
|
||||
- a user edits files under an installed extension directory;
|
||||
- an external process updates extension contents;
|
||||
- a marketplace/local extension source changes on disk;
|
||||
- a watcher sees a change but should not auto-refresh during noisy file writes.
|
||||
|
||||
This command should reuse the same runtime refresh orchestration as automatic
|
||||
mutations.
|
||||
|
||||
### Separate Runtime Refresh from Model Notification
|
||||
|
||||
Refreshing runtime state is not enough. The model also needs an accurate view of
|
||||
which capabilities are available.
|
||||
|
||||
Runtime refresh updates what can actually execute. Model notification updates
|
||||
what the model believes it can call. These are related but separate concerns.
|
||||
|
||||
## Subsystem Behavior
|
||||
|
||||
### Skills
|
||||
|
||||
Extension skills are loaded by `SkillManager.refreshCache()` from active
|
||||
extensions. `SkillTool.refreshSkills()` updates the in-memory runtime sets.
|
||||
|
||||
Problem: the model-facing `<available_skills>` path is currently add-only after
|
||||
startup. Disabling an extension removes the skill from runtime state, but the
|
||||
old listing may remain in conversation history.
|
||||
|
||||
Planned behavior:
|
||||
|
||||
- keep runtime refresh through `SkillManager.refreshCache()`;
|
||||
- after extension runtime refresh, provide a way to send the current complete
|
||||
available skills/commands list, or an equivalent clear delta;
|
||||
- avoid relying only on "new skills became available" reminders for extension
|
||||
mutation changes.
|
||||
|
||||
### Commands
|
||||
|
||||
Extension commands are user-visible slash commands by default. Some are also
|
||||
model-visible when `modelInvocable === true`; those enter the Skill tool's
|
||||
available-skill list and can be invoked by name through `SkillTool`.
|
||||
|
||||
Problem: there is no core command manager. Command reload is a CLI/UI-layer
|
||||
operation, and extension mutations do not have a clear command refresh contract.
|
||||
|
||||
Planned behavior:
|
||||
|
||||
- make command reload an explicit part of extension runtime refresh where the
|
||||
CLI/TUI layer is available;
|
||||
- ensure model-invocable extension commands update the Skill tool provider
|
||||
before model-facing skill reminders are emitted;
|
||||
- keep this minimal and avoid moving the entire command system into core.
|
||||
|
||||
### MCP
|
||||
|
||||
Extension MCP server config is merged through `Config.getMergedMcpServers()`.
|
||||
|
||||
Problem: extension mutations currently call `restartMcpServers()`, which
|
||||
restarts and rediscovers all MCP servers.
|
||||
|
||||
Planned behavior:
|
||||
|
||||
- reuse or extend the existing incremental MCP reconcile path;
|
||||
- after extension cache state is updated, reconcile against the latest merged
|
||||
MCP map;
|
||||
- connect added servers, disconnect removed servers, and restart only changed
|
||||
servers;
|
||||
- keep existing added MCP tool reminders;
|
||||
- add a removed MCP tool reminder so disabling an extension does not leave the
|
||||
model relying on stale tool availability context.
|
||||
|
||||
### Agents
|
||||
|
||||
Extension agents are loaded by `SubagentManager.refreshCache()`. `AgentTool`
|
||||
listens for subagent changes and updates its description and `subagent_type`
|
||||
schema enum, then calls `geminiClient.setTools()`.
|
||||
|
||||
Problem: this updates runtime/tool schema, but there is no explicit
|
||||
conversation reminder for added or removed agent types. In practice, the model
|
||||
may not notice new extension agents unless prompted.
|
||||
|
||||
Planned behavior:
|
||||
|
||||
- keep `SubagentManager.refreshCache()` and `AgentTool` schema refresh;
|
||||
- add a model-facing added/removed agent-type reminder, similar in intent to
|
||||
Claude Code's `agent_listing_delta`;
|
||||
- do not make each agent a separate tool. Agents remain selected through the
|
||||
`Agent` tool's `subagent_type` parameter.
|
||||
|
||||
### LSP
|
||||
|
||||
Extension `lspServers` can be read by the LSP config loader.
|
||||
|
||||
Problem: LSP startup currently initializes the native LSP service once. Runtime
|
||||
extension changes do not reinitialize LSP server configuration.
|
||||
|
||||
Planned behavior:
|
||||
|
||||
- add an optional LSP reinitialization hook to extension runtime refresh;
|
||||
- call it when LSP is enabled and the API is available;
|
||||
- do not add a model-facing reminder for LSP initially, because LSP is exposed
|
||||
as a fixed `lsp` tool and server state is better surfaced through status and
|
||||
tool results.
|
||||
|
||||
### Hooks
|
||||
|
||||
Extension hooks should be treated as runtime extension components.
|
||||
|
||||
Problem: extension mutation refresh does not currently have a clear hook reload
|
||||
step.
|
||||
|
||||
Planned behavior:
|
||||
|
||||
- add hook reload to the extension runtime refresh orchestration;
|
||||
- disabling/uninstalling an extension should remove its hooks;
|
||||
- enabling/installing/updating an extension should register its current hooks.
|
||||
|
||||
## Proposed PR Sequence
|
||||
|
||||
### PR 1: Shared Extension Runtime Refresh Orchestrator
|
||||
|
||||
Create a small shared orchestration function used by extension mutations.
|
||||
|
||||
Scope:
|
||||
|
||||
- preserve current behavior;
|
||||
- keep automatic extension mutation refresh;
|
||||
- move the current refresh sequence behind one function;
|
||||
- do not add watcher support;
|
||||
- do not add `/reload-plugins`;
|
||||
- do not change model reminders;
|
||||
- MCP may still use the existing full restart in this PR.
|
||||
|
||||
Expected value:
|
||||
|
||||
- gives later PRs one place to attach commands, hooks, LSP, MCP reconcile, and
|
||||
model notifications;
|
||||
- keeps review focused on structure, not behavior changes.
|
||||
|
||||
### PR 2: Complete Automatic Mutation Refresh
|
||||
|
||||
Extend the orchestrator to refresh all runtime components that should update
|
||||
after extension UI mutations.
|
||||
|
||||
Scope:
|
||||
|
||||
- commands reload when the CLI/TUI command service is available;
|
||||
- hooks reload;
|
||||
- optional LSP reinitialization;
|
||||
- keep skills, agents, and memory refresh;
|
||||
- keep MCP behavior unchanged unless a minimal no-risk integration is already
|
||||
available.
|
||||
|
||||
Expected value:
|
||||
|
||||
- extension enable/disable/install/update/uninstall becomes more complete
|
||||
without changing the user-facing automatic-refresh model.
|
||||
|
||||
### PR 3: Incremental MCP Refresh for Extension Mutations
|
||||
|
||||
Replace full MCP restart with incremental reconcile for extension-driven
|
||||
changes.
|
||||
|
||||
Scope:
|
||||
|
||||
- reconcile against the latest merged MCP config after extension cache changes;
|
||||
- avoid restarting unrelated MCP servers;
|
||||
- keep existing event/update behavior so `setTools()` still runs through the
|
||||
MCP update path.
|
||||
|
||||
Expected value:
|
||||
|
||||
- reduces side effects and latency when toggling extensions.
|
||||
|
||||
### PR 4: Model-Facing Availability Notifications
|
||||
|
||||
Fix stale model-visible capability lists.
|
||||
|
||||
Scope:
|
||||
|
||||
- send a current complete skills/model-invocable commands listing, or equivalent
|
||||
clear delta, after extension refresh changes that affect skills/commands;
|
||||
- add removed MCP tool reminders;
|
||||
- add agent added/removed reminders;
|
||||
- avoid adding an LSP reminder initially.
|
||||
|
||||
Expected value:
|
||||
|
||||
- the model's conversation context matches runtime availability after extension
|
||||
changes.
|
||||
|
||||
### PR 5: `/reload-plugins`
|
||||
|
||||
Add a manual reload command for external extension file changes.
|
||||
|
||||
Scope:
|
||||
|
||||
- reuse the shared extension runtime refresh orchestrator;
|
||||
- report a concise refresh summary;
|
||||
- do not replace automatic mutation refresh;
|
||||
- clear any stale state introduced by the later watcher PR.
|
||||
|
||||
Expected value:
|
||||
|
||||
- users have a manual repair path when file watching detects changes or when
|
||||
auto-detection misses a change.
|
||||
|
||||
### PR 6: Extension File Watcher and Stale Notification
|
||||
|
||||
Detect external extension file changes and notify the user.
|
||||
|
||||
Scope:
|
||||
|
||||
- watch a conservative allowlist of extension-related files:
|
||||
- `qwen-extension.json`;
|
||||
- `.qwen-extension-install.json`;
|
||||
- extension enablement, preferences, and source metadata;
|
||||
- `commands/**`;
|
||||
- `skills/**`;
|
||||
- `agents/**`;
|
||||
- `hooks/**`;
|
||||
- referenced LSP config files;
|
||||
- extension context files such as `GEMINI.md`;
|
||||
- mark extension runtime as stale;
|
||||
- show a UI notification suggesting `/reload-plugins`;
|
||||
- do not automatically reload on every file event.
|
||||
|
||||
Expected value:
|
||||
|
||||
- external file changes are discoverable without making runtime refresh noisy or
|
||||
fragile.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not replace Qwen Code's automatic extension mutation behavior with a
|
||||
mandatory manual reload flow.
|
||||
- Do not introduce a broad generic refresh framework before concrete call sites
|
||||
need it.
|
||||
- Do not make each agent type a separate model tool.
|
||||
- Do not make LSP availability reminders part of the first implementation.
|
||||
- Do not automatically reload every extension file watcher event.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- What is the smallest stable API for command reload from extension runtime
|
||||
refresh without over-coupling core and CLI?
|
||||
- Should skills/commands use a full current listing after extension refresh, or
|
||||
an added/removed delta? Full listing is simpler and closer to the current
|
||||
`<available_skills>` model.
|
||||
- Should removed MCP tool reminders include all removed tools, or group by
|
||||
extension/server to reduce noise?
|
||||
- Which existing hook reload function should become the public orchestration
|
||||
point?
|
||||
- How should `/reload-plugins` behave in non-interactive mode, if at all?
|
||||
|
||||
## Tracking Notes
|
||||
|
||||
Use this document to keep PR scope narrow. Each PR should update this file when
|
||||
its part of the plan is completed or intentionally changed.
|
||||
103
packages/core/src/extension/extension-runtime-refresh.test.ts
Normal file
103
packages/core/src/extension/extension-runtime-refresh.test.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
refreshExtensionRuntime,
|
||||
type ExtensionRuntimeRefreshConfig,
|
||||
} from './extension-runtime-refresh.js';
|
||||
|
||||
describe('refreshExtensionRuntime', () => {
|
||||
it('returns early without config', async () => {
|
||||
await expect(refreshExtensionRuntime(undefined)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('refreshes existing extension runtime components', async () => {
|
||||
const restartMcpServers = vi.fn();
|
||||
const refreshSkills = vi.fn();
|
||||
const refreshSubagents = vi.fn();
|
||||
const refreshHierarchicalMemory = vi.fn();
|
||||
|
||||
const config = {
|
||||
getToolRegistry: () => ({ restartMcpServers }),
|
||||
getSkillManager: () => ({ refreshCache: refreshSkills }),
|
||||
getSubagentManager: () => ({ refreshCache: refreshSubagents }),
|
||||
refreshHierarchicalMemory,
|
||||
} as unknown as ExtensionRuntimeRefreshConfig;
|
||||
|
||||
await refreshExtensionRuntime(config);
|
||||
|
||||
expect(restartMcpServers).toHaveBeenCalledOnce();
|
||||
expect(refreshSkills).toHaveBeenCalledOnce();
|
||||
expect(refreshSubagents).toHaveBeenCalledOnce();
|
||||
expect(refreshHierarchicalMemory).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('continues when a refreshCache leg rejects', async () => {
|
||||
const restartMcpServers = vi.fn();
|
||||
const refreshSkills = vi.fn().mockRejectedValue(new Error('skills failed'));
|
||||
const refreshSubagents = vi.fn();
|
||||
const refreshHierarchicalMemory = vi.fn();
|
||||
|
||||
const config = {
|
||||
getToolRegistry: () => ({ restartMcpServers }),
|
||||
getSkillManager: () => ({ refreshCache: refreshSkills }),
|
||||
getSubagentManager: () => ({ refreshCache: refreshSubagents }),
|
||||
refreshHierarchicalMemory,
|
||||
} as unknown as ExtensionRuntimeRefreshConfig;
|
||||
|
||||
await expect(refreshExtensionRuntime(config)).resolves.toBeUndefined();
|
||||
|
||||
expect(restartMcpServers).toHaveBeenCalledOnce();
|
||||
expect(refreshSkills).toHaveBeenCalledOnce();
|
||||
expect(refreshSubagents).toHaveBeenCalledOnce();
|
||||
expect(refreshHierarchicalMemory).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('resolves when refreshHierarchicalMemory throws', async () => {
|
||||
const restartMcpServers = vi.fn();
|
||||
const refreshSkills = vi.fn();
|
||||
const refreshSubagents = vi.fn();
|
||||
|
||||
const config = {
|
||||
getToolRegistry: () => ({ restartMcpServers }),
|
||||
getSkillManager: () => ({ refreshCache: refreshSkills }),
|
||||
getSubagentManager: () => ({ refreshCache: refreshSubagents }),
|
||||
refreshHierarchicalMemory: vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('memory failed')),
|
||||
} as unknown as ExtensionRuntimeRefreshConfig;
|
||||
|
||||
await expect(refreshExtensionRuntime(config)).resolves.toBeUndefined();
|
||||
|
||||
expect(restartMcpServers).toHaveBeenCalledOnce();
|
||||
expect(refreshSkills).toHaveBeenCalledOnce();
|
||||
expect(refreshSubagents).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('rejects when restartMcpServers fails', async () => {
|
||||
const restartMcpServers = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('mcp failed'));
|
||||
const refreshSkills = vi.fn();
|
||||
const refreshSubagents = vi.fn();
|
||||
const refreshHierarchicalMemory = vi.fn();
|
||||
|
||||
const config = {
|
||||
getToolRegistry: () => ({ restartMcpServers }),
|
||||
getSkillManager: () => ({ refreshCache: refreshSkills }),
|
||||
getSubagentManager: () => ({ refreshCache: refreshSubagents }),
|
||||
refreshHierarchicalMemory,
|
||||
} as unknown as ExtensionRuntimeRefreshConfig;
|
||||
|
||||
await expect(refreshExtensionRuntime(config)).rejects.toThrow('mcp failed');
|
||||
|
||||
expect(restartMcpServers).toHaveBeenCalledOnce();
|
||||
expect(refreshSkills).not.toHaveBeenCalled();
|
||||
expect(refreshSubagents).not.toHaveBeenCalled();
|
||||
expect(refreshHierarchicalMemory).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
73
packages/core/src/extension/extension-runtime-refresh.ts
Normal file
73
packages/core/src/extension/extension-runtime-refresh.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { createDebugLogger } from '../utils/debugLogger.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
|
||||
const debugLogger = createDebugLogger('EXTENSION_RUNTIME_REFRESH');
|
||||
|
||||
export type ExtensionRuntimeRefreshConfig = Pick<
|
||||
Config,
|
||||
| 'getToolRegistry'
|
||||
| 'getSkillManager'
|
||||
| 'getSubagentManager'
|
||||
| 'refreshHierarchicalMemory'
|
||||
>;
|
||||
|
||||
export async function refreshExtensionRuntime(
|
||||
config: ExtensionRuntimeRefreshConfig | undefined,
|
||||
): Promise<void> {
|
||||
if (!config) return;
|
||||
|
||||
// Error-handling contract:
|
||||
// Tier 1 (fatal) — restartMcpServers failure propagates. MCP is a
|
||||
// prerequisite for tool discovery; callers must know
|
||||
// if it failed so they can surface the error.
|
||||
// Tier 2 (swallow) — refreshCache failures (skills, subagents) are
|
||||
// logged via warn but swallowed by allSettled.
|
||||
// Tier 3 (swallow) — refreshHierarchicalMemory failure is logged via
|
||||
// error but swallowed by try/catch.
|
||||
// When adding new refresh steps, decide explicitly which tier applies.
|
||||
await config.getToolRegistry().restartMcpServers();
|
||||
|
||||
// Refresh skills + subagents in parallel. Both `refreshCache` calls now
|
||||
// resolve only after their async change-listener chain settles — for skills,
|
||||
// that includes `SkillTool.refreshSkills()` rebuilding the model-facing tool
|
||||
// description and updating `geminiClient`'s tool list. Use allSettled (rather
|
||||
// than Promise.all) so a rejection from one leg does not cascade — the other
|
||||
// leg's result is still applied, refreshHierarchicalMemory below still runs,
|
||||
// and callers (`enableExtension`, etc.) don't unwind because of an unrelated
|
||||
// transient failure.
|
||||
const skillManager = config.getSkillManager();
|
||||
const settled = await Promise.allSettled([
|
||||
skillManager?.refreshCache(),
|
||||
config.getSubagentManager().refreshCache(),
|
||||
]);
|
||||
|
||||
for (const result of settled) {
|
||||
if (result.status === 'rejected') {
|
||||
debugLogger.warn(
|
||||
'refreshExtensionRuntime: a refreshCache leg failed:',
|
||||
result.reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Await hierarchical memory refresh so callers only continue after the
|
||||
// extension refresh has settled. Wrap in try/catch so a transient failure
|
||||
// doesn't propagate up to `enableExtension` / `installExtension` callers,
|
||||
// which have already mutated their `isActive`/`installed` flags by the time
|
||||
// this function is invoked — a failed memory refresh leaves stale memory
|
||||
// but should not back out the surrounding extension transition.
|
||||
try {
|
||||
await config.refreshHierarchicalMemory();
|
||||
} catch (err) {
|
||||
debugLogger.error(
|
||||
'refreshExtensionRuntime: refreshHierarchicalMemory failed:',
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -98,6 +98,7 @@ import {
|
|||
import { loadSkillsFromDir } from '../skills/skill-load.js';
|
||||
import { loadSubagentFromDir } from '../subagents/subagent-manager.js';
|
||||
import { createDebugLogger } from '../utils/debugLogger.js';
|
||||
import { refreshExtensionRuntime } from './extension-runtime-refresh.js';
|
||||
|
||||
const debugLogger = createDebugLogger('EXTENSIONS');
|
||||
|
||||
|
|
@ -1570,47 +1571,7 @@ export class ExtensionManager {
|
|||
}
|
||||
|
||||
async refreshMemory(): Promise<void> {
|
||||
if (!this.config) return;
|
||||
// refresh mcp servers
|
||||
await this.config.getToolRegistry().restartMcpServers();
|
||||
// Refresh skills + subagents in parallel. Both `refreshCache` calls
|
||||
// now resolve only after their async change-listener chain settles
|
||||
// — for skills, that includes `SkillTool.refreshSkills()` rebuilding
|
||||
// the model-facing tool description and updating `geminiClient`'s
|
||||
// tool list. allSettled (rather than Promise.all) so a rejection
|
||||
// from one leg does not cascade — the other leg's result is still
|
||||
// applied, refreshHierarchicalMemory below still runs, and the
|
||||
// `refreshTools` callers (`enableExtension`, etc.) don't unwind
|
||||
// because of an unrelated transient failure.
|
||||
const skillManager = this.config.getSkillManager();
|
||||
const settled = await Promise.allSettled([
|
||||
skillManager?.refreshCache(),
|
||||
this.config.getSubagentManager().refreshCache(),
|
||||
]);
|
||||
for (const result of settled) {
|
||||
if (result.status === 'rejected') {
|
||||
debugLogger.warn(
|
||||
'refreshMemory: a refreshCache leg failed:',
|
||||
result.reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Hierarchical memory refresh is now awaited too — the previous
|
||||
// fire-and-forget defeated the rest of the function's "wait until
|
||||
// refresh is done" contract. Wrap in try/catch so a transient
|
||||
// failure doesn't propagate up to `enableExtension` /
|
||||
// `installExtension` callers, which have already mutated their
|
||||
// `isActive`/`installed` flags by the time refreshMemory is
|
||||
// invoked. A failed memory refresh leaves stale memory but should
|
||||
// not back out the surrounding extension transition.
|
||||
try {
|
||||
await this.config.refreshHierarchicalMemory();
|
||||
} catch (err) {
|
||||
debugLogger.error(
|
||||
'refreshMemory: refreshHierarchicalMemory failed:',
|
||||
err,
|
||||
);
|
||||
}
|
||||
await refreshExtensionRuntime(this.config);
|
||||
}
|
||||
|
||||
async refreshTools(): Promise<void> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue