wave-code 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle/wave.mjs +1049 -584
- package/package.json +4 -3
- package/src/cli.tsx +9 -0
- package/src/commands/plugin/install.ts +6 -7
- package/src/commands/plugin/uninstall.ts +11 -5
- package/src/components/ChatInterface.tsx +1 -0
- package/src/components/ConfirmationDetails.tsx +36 -15
- package/src/components/ConfirmationSelector.tsx +39 -0
- package/src/components/InputBox.tsx +39 -2
- package/src/components/Markdown.tsx +21 -8
- package/src/components/MarketplaceDetail.tsx +10 -27
- package/src/components/MarketplaceView.tsx +17 -3
- package/src/components/Notifications.tsx +7 -0
- package/src/components/PlanView.tsx +22 -17
- package/src/components/PluginManagerShell.tsx +3 -3
- package/src/components/PluginManagerTypes.ts +2 -1
- package/src/components/ResumeCommand.tsx +84 -0
- package/src/components/ScrolledContent.tsx +71 -0
- package/src/components/SessionSelector.tsx +184 -6
- package/src/components/SessionSelectorFlow.tsx +246 -0
- package/src/constants/commands.ts +18 -0
- package/src/contexts/useChat.tsx +158 -28
- package/src/daemon/commands.ts +569 -46
- package/src/daemon-cli.ts +6 -0
- package/src/hooks/useInputManager.ts +16 -0
- package/src/hooks/usePluginManager.ts +104 -78
- package/src/index.ts +62 -8
- package/src/managers/inputHandlers.ts +14 -9
- package/src/managers/inputReducer.ts +29 -9
- package/src/reducers/confirmationReducer.ts +20 -1
- package/src/reducers/selectorReducer.ts +3 -0
- package/src/session-selector-cli.tsx +9 -255
- package/src/stdio/agentBridge.ts +397 -95
- package/src/stdio/protocol.ts +9 -0
- package/src/stdio-cli.ts +6 -0
- package/src/utils/sessionOwnership.ts +62 -0
- package/src/components/PlanDisplay.tsx +0 -18
package/src/stdio/agentBridge.ts
CHANGED
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
type PartialHookConfiguration,
|
|
37
37
|
isValidHookEvent,
|
|
38
38
|
listSessions,
|
|
39
|
+
listAllSessions,
|
|
40
|
+
setSessionCustomTitle,
|
|
39
41
|
searchFiles,
|
|
40
42
|
generateRandomName,
|
|
41
43
|
getDefaultRemoteBranch,
|
|
@@ -49,8 +51,13 @@ import {
|
|
|
49
51
|
loadWaveConfigFromFile,
|
|
50
52
|
getUserConfigPaths,
|
|
51
53
|
getProjectConfigPaths,
|
|
54
|
+
readUserPreferenceView,
|
|
55
|
+
updateUserPreferenceSettings,
|
|
56
|
+
type UserPreferenceSettings,
|
|
57
|
+
type UserPreferenceSettingsView,
|
|
52
58
|
type SubagentConfiguration,
|
|
53
59
|
type SkillMetadata,
|
|
60
|
+
type PluginReloadResult,
|
|
54
61
|
} from "wave-agent-sdk";
|
|
55
62
|
import {
|
|
56
63
|
type JsonRpcError,
|
|
@@ -95,7 +102,6 @@ interface InitializeParams {
|
|
|
95
102
|
defaultHeaders?: Record<string, string>;
|
|
96
103
|
model?: string;
|
|
97
104
|
fastModel?: string;
|
|
98
|
-
language?: string;
|
|
99
105
|
permissionMode?: PermissionMode;
|
|
100
106
|
tools?: string[];
|
|
101
107
|
allowedTools?: string[];
|
|
@@ -104,11 +110,11 @@ interface InitializeParams {
|
|
|
104
110
|
mcpServers?: Record<string, McpServerConfig>;
|
|
105
111
|
worktreeName?: string;
|
|
106
112
|
isNewWorktree?: boolean;
|
|
107
|
-
/** Settings-page auto-memory toggle/frequency (session-level override). */
|
|
108
|
-
autoMemoryEnabled?: boolean;
|
|
109
|
-
autoMemoryFrequency?: number;
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
// 会话级覆盖项:用户偏好(语言 / 上下文长度 / 自动记忆开关与频率)不在此层——
|
|
116
|
+
// 它们落用户级 ~/.wave/settings.json 经实时重载生效(getUserSettings /
|
|
117
|
+
// updateUserSettings),走覆盖层会永久遮蔽 settings.json 的实时值。
|
|
112
118
|
interface UpdateConfigParams {
|
|
113
119
|
apiKey?: string;
|
|
114
120
|
baseURL?: string;
|
|
@@ -116,10 +122,6 @@ interface UpdateConfigParams {
|
|
|
116
122
|
defaultHeaders?: Record<string, string>;
|
|
117
123
|
model?: string;
|
|
118
124
|
fastModel?: string;
|
|
119
|
-
language?: string;
|
|
120
|
-
/** Settings-page auto-memory toggle/frequency (session-level override). */
|
|
121
|
-
autoMemoryEnabled?: boolean;
|
|
122
|
-
autoMemoryFrequency?: number;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
interface SearchFilesParams {
|
|
@@ -141,6 +143,24 @@ interface SessionEntry {
|
|
|
141
143
|
* header when the session is re-attached from disk after a daemon restart.
|
|
142
144
|
*/
|
|
143
145
|
createdWorkdir: string;
|
|
146
|
+
/**
|
|
147
|
+
* True while `updateConfig` is tearing the session's agent down and rebuilding
|
|
148
|
+
* it from the updated config — the entry stays in the registry (so the session
|
|
149
|
+
* does not look deleted to a `wait` that is blocked on it) but has no usable
|
|
150
|
+
* agent. Two consequences, both deliberate:
|
|
151
|
+
*
|
|
152
|
+
* - `listDaemonSessions` reports it as `isLoading: true`. The teardown aborts
|
|
153
|
+
* the in-flight turn, so the outgoing agent reads `isLoading: false` while the
|
|
154
|
+
* rebuild is still running; reporting that would let a `wait` print an idle
|
|
155
|
+
* snapshot for a session whose agent is mid-replacement. A waiter that keeps
|
|
156
|
+
* waiting is never wrong here — the rebuild is bounded, and if it fails the
|
|
157
|
+
* entry is dropped, so the waiter then gets a clear "no longer exists".
|
|
158
|
+
* - State-changing RPCs refuse it (`requireSession`); a message sent to the
|
|
159
|
+
* outgoing agent would be silently lost. Read-only RPCs still work
|
|
160
|
+
* (`requireSessionForRead`) and see the outgoing agent's data, which is this
|
|
161
|
+
* same session's transcript.
|
|
162
|
+
*/
|
|
163
|
+
transitioning?: boolean;
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
/**
|
|
@@ -206,6 +226,21 @@ export class AgentBridge {
|
|
|
206
226
|
return this.restoreSession(p.sessionId as string, sessionId);
|
|
207
227
|
case "listSessions":
|
|
208
228
|
return this.listSessions(p.workdir as string | undefined, sessionId);
|
|
229
|
+
case "listAllSessions":
|
|
230
|
+
// Every project on this host (no session required) — the desktop's
|
|
231
|
+
// /resume picker lists sessions the app never created, so it cannot use
|
|
232
|
+
// the per-workdir listSessions.
|
|
233
|
+
return this.listAllSessions();
|
|
234
|
+
case "renameSession":
|
|
235
|
+
// Host-level like listAllSessions: the target session may have no live
|
|
236
|
+
// agent (the desktop renames sessions straight from its sidebar, and a
|
|
237
|
+
// restored one is not running), so the id/workdir come from the caller
|
|
238
|
+
// instead of the bound session.
|
|
239
|
+
return this.renameSession(
|
|
240
|
+
p.sessionId as string,
|
|
241
|
+
p.workdir as string,
|
|
242
|
+
p.title as string,
|
|
243
|
+
);
|
|
209
244
|
case "getSessionInfo":
|
|
210
245
|
return this.getSessionInfo(sessionId);
|
|
211
246
|
case "listPendingPermissions":
|
|
@@ -233,6 +268,15 @@ export class AgentBridge {
|
|
|
233
268
|
case "setModel":
|
|
234
269
|
return this.setModel(p.model as string, sessionId);
|
|
235
270
|
|
|
271
|
+
// ── User preferences (global — user-level ~/.wave/settings.json) ──
|
|
272
|
+
// 设置页保存路径:用户偏好写文件后由 SDK 实时重载在各会话下一轮生效,
|
|
273
|
+
// 不经 updateConfig 当 AgentOptions 覆盖层下发(见 protocol.ts 注释)。
|
|
274
|
+
// 读回的是**生效值**(可能来自 Remote 组织下发/环境变量)+ 每键来源层。
|
|
275
|
+
case "getUserSettings":
|
|
276
|
+
return this.userSettingsView();
|
|
277
|
+
case "updateUserSettings":
|
|
278
|
+
return this.updateUserSettings(p as UserPreferenceSettings);
|
|
279
|
+
|
|
236
280
|
// ── Messages ──
|
|
237
281
|
case "sendMessage":
|
|
238
282
|
return this.sendMessage(
|
|
@@ -376,6 +420,7 @@ export class AgentBridge {
|
|
|
376
420
|
case "uninstallPlugin":
|
|
377
421
|
return this.uninstallPlugin(
|
|
378
422
|
p.pluginId as string,
|
|
423
|
+
p.scope as Scope | undefined,
|
|
379
424
|
p.workdir as string | undefined,
|
|
380
425
|
sessionId,
|
|
381
426
|
);
|
|
@@ -424,11 +469,25 @@ export class AgentBridge {
|
|
|
424
469
|
p.workdir as string | undefined,
|
|
425
470
|
sessionId,
|
|
426
471
|
);
|
|
472
|
+
case "setPluginScope":
|
|
473
|
+
return this.setPluginScope(
|
|
474
|
+
p.pluginId as string,
|
|
475
|
+
p.scope as Scope,
|
|
476
|
+
p.workdir as string | undefined,
|
|
477
|
+
sessionId,
|
|
478
|
+
);
|
|
479
|
+
case "reloadPlugins":
|
|
480
|
+
return this.reloadPlugins();
|
|
427
481
|
case "listMarketplaces":
|
|
428
482
|
return this.listMarketplaces(
|
|
429
483
|
p.workdir as string | undefined,
|
|
430
484
|
sessionId,
|
|
431
485
|
);
|
|
486
|
+
case "refreshMarketplaces":
|
|
487
|
+
return this.refreshMarketplaces(
|
|
488
|
+
p.workdir as string | undefined,
|
|
489
|
+
sessionId,
|
|
490
|
+
);
|
|
432
491
|
case "addMarketplace":
|
|
433
492
|
return this.addMarketplace(
|
|
434
493
|
p.input as string,
|
|
@@ -552,9 +611,6 @@ export class AgentBridge {
|
|
|
552
611
|
defaultHeaders: params.defaultHeaders,
|
|
553
612
|
model: params.model,
|
|
554
613
|
fastModel: params.fastModel,
|
|
555
|
-
language: params.language,
|
|
556
|
-
autoMemoryEnabled: params.autoMemoryEnabled,
|
|
557
|
-
autoMemoryFrequency: params.autoMemoryFrequency,
|
|
558
614
|
permissionMode: params.permissionMode,
|
|
559
615
|
tools: params.tools,
|
|
560
616
|
allowedTools: params.allowedTools,
|
|
@@ -608,8 +664,30 @@ export class AgentBridge {
|
|
|
608
664
|
if (sessionId) {
|
|
609
665
|
const entry = this.sessions.get(sessionId);
|
|
610
666
|
if (entry) {
|
|
611
|
-
|
|
667
|
+
if (entry.transitioning) {
|
|
668
|
+
// A rebuild owns this entry (updateConfig): destroying it here would
|
|
669
|
+
// race the rebuild, which would then re-register a replacement for a
|
|
670
|
+
// session the caller just destroyed. Refuse loudly instead — the
|
|
671
|
+
// caller can retry once the reload settles.
|
|
672
|
+
throw new RpcError(
|
|
673
|
+
PROTOCOL_INTERNAL_ERROR,
|
|
674
|
+
`Session ${sessionId} is being reconfigured (config reload in progress), retry shortly`,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
// Drop the registry entry BEFORE awaiting the teardown. Agent.destroy()
|
|
678
|
+
// aborts the in-flight turn, which pushes `loadingChange:false` — the
|
|
679
|
+
// exact notification a finished turn pushes — and the teardown itself is
|
|
680
|
+
// not instant (transcript save, subagent/mcp cleanup, auto-memory drain).
|
|
681
|
+
// A client blocked in `wait` that is woken by that push re-reads the
|
|
682
|
+
// registry for the verdict: with the entry still listed (isLoading now
|
|
683
|
+
// false) it settles as exit 0 on an intermediate snapshot — a false
|
|
684
|
+
// "finished" for a session that is being removed. With the entry gone the
|
|
685
|
+
// existence check wins and it reports "no longer exists" (exit 1).
|
|
686
|
+
// Removed first also means a throwing teardown cannot leave a
|
|
687
|
+
// half-destroyed agent listed (destroyAll() below clears the map first
|
|
688
|
+
// for the same reason).
|
|
612
689
|
this.sessions.delete(sessionId);
|
|
690
|
+
await entry.agent.destroy();
|
|
613
691
|
}
|
|
614
692
|
}
|
|
615
693
|
return null;
|
|
@@ -633,7 +711,7 @@ export class AgentBridge {
|
|
|
633
711
|
restoreId: string,
|
|
634
712
|
sessionId?: string,
|
|
635
713
|
): Promise<null> {
|
|
636
|
-
const entry = this.
|
|
714
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
637
715
|
// Re-attach to a live session: the SDK restore would no-op (target is
|
|
638
716
|
// already current). Emit the current messages so the freshly attached
|
|
639
717
|
// client — whose router registered only after initialize returned and so
|
|
@@ -665,6 +743,18 @@ export class AgentBridge {
|
|
|
665
743
|
this.emitContextUsage(entry.agent);
|
|
666
744
|
return null;
|
|
667
745
|
}
|
|
746
|
+
// The replay branch above is safe during an `updateConfig` transition (it
|
|
747
|
+
// only re-pushes the outgoing agent's own state, which is this session's
|
|
748
|
+
// transcript). A REAL restore is not: it would load another conversation
|
|
749
|
+
// into the agent that is about to be destroyed and replaced, so the call
|
|
750
|
+
// would be silently thrown away together with the outgoing agent. Refuse it
|
|
751
|
+
// with the same retryable error the mutating RPCs use.
|
|
752
|
+
if (entry.transitioning) {
|
|
753
|
+
throw new RpcError(
|
|
754
|
+
PROTOCOL_INTERNAL_ERROR,
|
|
755
|
+
`Session ${sessionId} is being reconfigured (config reload in progress), retry shortly`,
|
|
756
|
+
);
|
|
757
|
+
}
|
|
668
758
|
await entry.agent.restoreSession(restoreId);
|
|
669
759
|
// A real restore must also re-emit the usage unconditionally: the SDK
|
|
670
760
|
// only fires onLatestTotalTokensChange on a VALUE CHANGE, so restoring a
|
|
@@ -717,6 +807,38 @@ export class AgentBridge {
|
|
|
717
807
|
return { sessions };
|
|
718
808
|
}
|
|
719
809
|
|
|
810
|
+
/**
|
|
811
|
+
* All main sessions across every project directory of this host, newest
|
|
812
|
+
* first (deduplicated by session id). Each entry carries the session's real
|
|
813
|
+
* `workdir`, which is what a caller needs to resume it.
|
|
814
|
+
*/
|
|
815
|
+
private async listAllSessions(): Promise<{ sessions: SessionMetadata[] }> {
|
|
816
|
+
const sessions = await listAllSessions();
|
|
817
|
+
return { sessions };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Set a session's user-visible title. Works for any session on this host —
|
|
822
|
+
* the target does not need to be the bound one, or even live.
|
|
823
|
+
*/
|
|
824
|
+
private async renameSession(
|
|
825
|
+
sessionId: string,
|
|
826
|
+
workdir: string,
|
|
827
|
+
title: string,
|
|
828
|
+
): Promise<void> {
|
|
829
|
+
// A session this host is running keeps the title in memory and writes it
|
|
830
|
+
// back at its next flush point (exit, compaction, resume, rewind), so a
|
|
831
|
+
// file-only write here would be clobbered by the stale in-memory value.
|
|
832
|
+
// Route through the agent; fall back to the file for sessions that are not
|
|
833
|
+
// bound to this host's process.
|
|
834
|
+
const bound = this.sessions.get(sessionId)?.agent;
|
|
835
|
+
if (bound) {
|
|
836
|
+
await bound.renameSession(title);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
await setSessionCustomTitle(sessionId, workdir, title);
|
|
840
|
+
}
|
|
841
|
+
|
|
720
842
|
// ── Git / worktree ────────────────────────────────────────────
|
|
721
843
|
|
|
722
844
|
private listGitBranches(workdir?: string): {
|
|
@@ -857,7 +979,7 @@ export class AgentBridge {
|
|
|
857
979
|
permissionMode: PermissionMode;
|
|
858
980
|
availableTools: string[];
|
|
859
981
|
} {
|
|
860
|
-
const entry = this.
|
|
982
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
861
983
|
return {
|
|
862
984
|
sessionId: entry.agent.sessionId,
|
|
863
985
|
// The recorded creation workdir, not the live agent.workingDirectory:
|
|
@@ -878,76 +1000,105 @@ export class AgentBridge {
|
|
|
878
1000
|
const currentSessionId = entry.agent.sessionId;
|
|
879
1001
|
// Merge new config into stored config
|
|
880
1002
|
entry.storedConfig = { ...entry.storedConfig, ...params };
|
|
881
|
-
//
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1003
|
+
// The entry is NOT removed while the agent is replaced: dropping it would
|
|
1004
|
+
// make a `wait` blocked on this session report "no longer exists" (exit 1)
|
|
1005
|
+
// for a session that is alive and merely reloading — a false error, the same
|
|
1006
|
+
// family as the false idle this whole change is about. Instead the entry is
|
|
1007
|
+
// flagged `transitioning` for the whole destroy + recreate, which
|
|
1008
|
+
// `listDaemonSessions` reports as `isLoading: true` (so no reader can see the
|
|
1009
|
+
// "listed + not busy" combination a waiter would settle on) while
|
|
1010
|
+
// state-changing RPCs refuse it (`requireSession`) instead of talking to the
|
|
1011
|
+
// outgoing, dying agent. Read-only RPCs keep working off that agent.
|
|
1012
|
+
entry.transitioning = true;
|
|
885
1013
|
const ctx: SessionContext = {};
|
|
886
|
-
const callbacks = this.createCallbacks(ctx);
|
|
887
|
-
const options: AgentOptions = {
|
|
888
|
-
callbacks,
|
|
889
|
-
workdir: entry.storedConfig.workdir,
|
|
890
|
-
restoreSessionId: currentSessionId,
|
|
891
|
-
apiKey: entry.storedConfig.apiKey,
|
|
892
|
-
baseURL: entry.storedConfig.baseURL,
|
|
893
|
-
defaultHeaders: entry.storedConfig.defaultHeaders,
|
|
894
|
-
model: entry.storedConfig.model,
|
|
895
|
-
fastModel: entry.storedConfig.fastModel,
|
|
896
|
-
language: entry.storedConfig.language,
|
|
897
|
-
autoMemoryEnabled: entry.storedConfig.autoMemoryEnabled,
|
|
898
|
-
autoMemoryFrequency: entry.storedConfig.autoMemoryFrequency,
|
|
899
|
-
permissionMode: entry.storedConfig.permissionMode,
|
|
900
|
-
tools: entry.storedConfig.tools,
|
|
901
|
-
allowedTools: entry.storedConfig.allowedTools,
|
|
902
|
-
disallowedTools: entry.storedConfig.disallowedTools,
|
|
903
|
-
plugins: entry.storedConfig.pluginDirs?.map((p) => ({
|
|
904
|
-
type: "local",
|
|
905
|
-
path: p,
|
|
906
|
-
})),
|
|
907
|
-
mcpServers: entry.storedConfig.mcpServers,
|
|
908
|
-
// Keep worktree context (permission safety) across recreation, but never
|
|
909
|
-
// re-fire WorktreeCreate — the hook ran at initial creation.
|
|
910
|
-
worktreeName: entry.storedConfig.worktreeName,
|
|
911
|
-
canUseTool: (context: ToolPermissionContext) =>
|
|
912
|
-
this.canUseTool(context, ctx),
|
|
913
|
-
};
|
|
914
|
-
|
|
915
|
-
let agent: Agent;
|
|
916
1014
|
try {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
//
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1015
|
+
// Destroy and recreate within the same session slot. The destroy comes
|
|
1016
|
+
// first so the outgoing agent stops writing the transcript before the
|
|
1017
|
+
// replacement loads it.
|
|
1018
|
+
await entry.agent.destroy();
|
|
1019
|
+
|
|
1020
|
+
const callbacks = this.createCallbacks(ctx);
|
|
1021
|
+
const options: AgentOptions = {
|
|
1022
|
+
callbacks,
|
|
1023
|
+
workdir: entry.storedConfig.workdir,
|
|
1024
|
+
restoreSessionId: currentSessionId,
|
|
1025
|
+
apiKey: entry.storedConfig.apiKey,
|
|
1026
|
+
baseURL: entry.storedConfig.baseURL,
|
|
1027
|
+
defaultHeaders: entry.storedConfig.defaultHeaders,
|
|
1028
|
+
model: entry.storedConfig.model,
|
|
1029
|
+
fastModel: entry.storedConfig.fastModel,
|
|
1030
|
+
permissionMode: entry.storedConfig.permissionMode,
|
|
1031
|
+
tools: entry.storedConfig.tools,
|
|
1032
|
+
allowedTools: entry.storedConfig.allowedTools,
|
|
1033
|
+
disallowedTools: entry.storedConfig.disallowedTools,
|
|
1034
|
+
plugins: entry.storedConfig.pluginDirs?.map((p) => ({
|
|
1035
|
+
type: "local",
|
|
1036
|
+
path: p,
|
|
1037
|
+
})),
|
|
1038
|
+
mcpServers: entry.storedConfig.mcpServers,
|
|
1039
|
+
// Keep worktree context (permission safety) across recreation, but never
|
|
1040
|
+
// re-fire WorktreeCreate — the hook ran at initial creation.
|
|
1041
|
+
worktreeName: entry.storedConfig.worktreeName,
|
|
1042
|
+
canUseTool: (context: ToolPermissionContext) =>
|
|
1043
|
+
this.canUseTool(context, ctx),
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
let agent: Agent;
|
|
1047
|
+
try {
|
|
1048
|
+
agent = await Agent.create(options);
|
|
1049
|
+
} catch (createError) {
|
|
1050
|
+
// The old agent is already destroyed. If the session file is missing or
|
|
1051
|
+
// unrecoverable (fresh session, cleared chat, or a never-persisted empty
|
|
1052
|
+
// session), fail soft: recreate WITHOUT restoreSessionId so this session
|
|
1053
|
+
// slot keeps working. Without this the client's sessionId still points at
|
|
1054
|
+
// the destroyed entry and every later request fails with "Session not
|
|
1055
|
+
// found" until the window is reloaded. Non-session errors (bad
|
|
1056
|
+
// baseURL/apiKey/config) must surface unchanged.
|
|
1057
|
+
if (!options.restoreSessionId || !isSessionRecoveryError(createError)) {
|
|
1058
|
+
throw createError;
|
|
1059
|
+
}
|
|
1060
|
+
logger?.warn(
|
|
1061
|
+
`updateConfig: failed to restore session ${currentSessionId}, recreating as a fresh session:`,
|
|
1062
|
+
createError,
|
|
1063
|
+
);
|
|
1064
|
+
agent = await Agent.create({ ...options, restoreSessionId: undefined });
|
|
928
1065
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1066
|
+
ctx.agent = agent;
|
|
1067
|
+
ctx.registeredSessionId = agent.sessionId;
|
|
1068
|
+
entry.agent = agent;
|
|
1069
|
+
// A recovery-recreated session gets a NEW id: move the registry key with it.
|
|
1070
|
+
// (The entry object is reused, so the flag and the recorded creation workdir
|
|
1071
|
+
// survive either way.) Announced with the OLD id as the envelope — same
|
|
1072
|
+
// contract as `onSessionIdChange` — so a client blocked on this session can
|
|
1073
|
+
// rebind to the new id instead of concluding it disappeared.
|
|
1074
|
+
if (agent.sessionId !== currentSessionId) {
|
|
1075
|
+
this.emit(
|
|
1076
|
+
"sessionIdChange",
|
|
1077
|
+
{ sessionId: agent.sessionId },
|
|
1078
|
+
currentSessionId,
|
|
1079
|
+
);
|
|
1080
|
+
this.sessions.delete(currentSessionId);
|
|
1081
|
+
this.sessions.set(agent.sessionId, entry);
|
|
1082
|
+
}
|
|
1083
|
+
entry.transitioning = false;
|
|
1084
|
+
return { sessionId: agent.sessionId };
|
|
1085
|
+
} catch (teardownError) {
|
|
1086
|
+
// Nothing usable is left: the outgoing agent is torn down (fully or
|
|
1087
|
+
// partially) and no replacement was built. Leaving the entry listed would
|
|
1088
|
+
// be a "half-dead" session — reachable through the registry but with no
|
|
1089
|
+
// working agent — so it is dropped, exactly like a failing `destroy`: a
|
|
1090
|
+
// waiter then reports "no longer exists" (exit 1) instead of hanging or
|
|
1091
|
+
// settling. The error surfaces to the caller unchanged.
|
|
1092
|
+
this.sessions.delete(currentSessionId);
|
|
1093
|
+
throw teardownError;
|
|
934
1094
|
}
|
|
935
|
-
ctx.agent = agent;
|
|
936
|
-
ctx.registeredSessionId = agent.sessionId;
|
|
937
|
-
this.sessions.set(agent.sessionId, {
|
|
938
|
-
agent,
|
|
939
|
-
storedConfig: { ...entry.storedConfig },
|
|
940
|
-
createdWorkdir: entry.createdWorkdir,
|
|
941
|
-
});
|
|
942
|
-
|
|
943
|
-
return { sessionId: agent.sessionId };
|
|
944
1095
|
}
|
|
945
1096
|
|
|
946
1097
|
private getConfiguredModels(sessionId?: string): {
|
|
947
1098
|
models: string[];
|
|
948
1099
|
currentModel: string | undefined;
|
|
949
1100
|
} {
|
|
950
|
-
const entry = this.
|
|
1101
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
951
1102
|
return {
|
|
952
1103
|
models: entry.agent.getConfiguredModels(),
|
|
953
1104
|
currentModel: entry.agent.getModelConfig().model,
|
|
@@ -956,7 +1107,7 @@ export class AgentBridge {
|
|
|
956
1107
|
|
|
957
1108
|
private async setModel(model: string, sessionId?: string): Promise<null> {
|
|
958
1109
|
const entry = this.requireSession(sessionId);
|
|
959
|
-
entry.agent.setModel(model);
|
|
1110
|
+
await entry.agent.setModel(model);
|
|
960
1111
|
// Keep storedConfig in sync: updateConfig recreates the agent from
|
|
961
1112
|
// storedConfig, so without this a later config save would revert the
|
|
962
1113
|
// model chosen here.
|
|
@@ -964,6 +1115,47 @@ export class AgentBridge {
|
|
|
964
1115
|
return null;
|
|
965
1116
|
}
|
|
966
1117
|
|
|
1118
|
+
// ── User preferences ──────────────────────────────────────────
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* 设置页保存路径:写用户级 `~/.wave/settings.json` 后,**显式重载本进程内
|
|
1122
|
+
* 全部会话**的实时配置。
|
|
1123
|
+
*
|
|
1124
|
+
* 不能只依赖文件监视:会话启动时该文件(乃至 `~/.wave` 目录)通常还不存在,
|
|
1125
|
+
* 监视要靠在父目录链存在的前提下补发事件;而这个文件正是被本次保存**创建**
|
|
1126
|
+
* 出来的最常见场景(全新安装的第一次保存,spec core/agent-config.md
|
|
1127
|
+
* 「设置实时重载」场景 7)。写完即重载使「保存 ⇒ 下一轮生效」与监视是否就绪
|
|
1128
|
+
* 无关;监视若也命中,重载是幂等的(并发重载按 `reloadInProgress` 去重)。
|
|
1129
|
+
*
|
|
1130
|
+
* 空载荷(无差异保存、只回读)不落盘,也不重载。
|
|
1131
|
+
*
|
|
1132
|
+
* 回包是**生效视图**(同 `getUserSettings`):设置页在保存后仍须看到「被组织
|
|
1133
|
+
* 配置覆盖」的键的生效值与来源,而不是回退成用户文件里的值。
|
|
1134
|
+
*/
|
|
1135
|
+
private async updateUserSettings(
|
|
1136
|
+
patch: UserPreferenceSettings,
|
|
1137
|
+
): Promise<UserPreferenceSettingsView> {
|
|
1138
|
+
await updateUserPreferenceSettings(patch);
|
|
1139
|
+
if (!Object.values(patch).every((value) => value === undefined)) {
|
|
1140
|
+
await Promise.all(
|
|
1141
|
+
[...this.sessions.values()].map((entry) =>
|
|
1142
|
+
entry.agent.reloadConfiguration(),
|
|
1143
|
+
),
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
return this.userSettingsView();
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* 用户偏好的**生效视图**:生效值 + 每键来源层(用户级文件 / Remote 组织下发 /
|
|
1151
|
+
* 机器环境变量 / 默认)。设置页据此把「由组织配置管理」的键显示为生效值 +
|
|
1152
|
+
* 置灰(spec core/agent-config.md 边界说明「用户偏好的层与来源」)。
|
|
1153
|
+
*/
|
|
1154
|
+
private userSettingsView(): UserPreferenceSettingsView {
|
|
1155
|
+
const { values, sources } = readUserPreferenceView();
|
|
1156
|
+
return { ...values, preferenceSources: sources };
|
|
1157
|
+
}
|
|
1158
|
+
|
|
967
1159
|
// ── Messages ──────────────────────────────────────────────────
|
|
968
1160
|
|
|
969
1161
|
private async sendMessage(
|
|
@@ -1104,7 +1296,7 @@ export class AgentBridge {
|
|
|
1104
1296
|
private async listRewindCheckpoints(sessionId?: string): Promise<{
|
|
1105
1297
|
checkpoints: Array<{ id: string; content: string }>;
|
|
1106
1298
|
}> {
|
|
1107
|
-
const entry = this.
|
|
1299
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1108
1300
|
const { messages } = await entry.agent.getFullMessageThread();
|
|
1109
1301
|
// 压缩 append-only 后同 id 消息在磁盘完整线程中重复出现(压缩前历史 +
|
|
1110
1302
|
// 压缩后 append 的重复)。按 id 去重并保留最后一次出现(与折叠后的
|
|
@@ -1151,7 +1343,7 @@ export class AgentBridge {
|
|
|
1151
1343
|
messages: Message[];
|
|
1152
1344
|
contextUsagePercent?: number;
|
|
1153
1345
|
} {
|
|
1154
|
-
const entry = this.
|
|
1346
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1155
1347
|
return {
|
|
1156
1348
|
messages: entry.agent.displayMessages,
|
|
1157
1349
|
contextUsagePercent: this.contextUsagePercentOf(entry.agent),
|
|
@@ -1162,7 +1354,7 @@ export class AgentBridge {
|
|
|
1162
1354
|
messages: Message[];
|
|
1163
1355
|
sessionIds: string[];
|
|
1164
1356
|
}> {
|
|
1165
|
-
const entry = this.
|
|
1357
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1166
1358
|
return entry.agent.getFullMessageThread();
|
|
1167
1359
|
}
|
|
1168
1360
|
|
|
@@ -1181,7 +1373,7 @@ export class AgentBridge {
|
|
|
1181
1373
|
taskId: string,
|
|
1182
1374
|
sessionId?: string,
|
|
1183
1375
|
): { output: ReturnType<Agent["getBackgroundTaskOutput"]> } {
|
|
1184
|
-
const entry = this.
|
|
1376
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1185
1377
|
return { output: entry.agent.getBackgroundTaskOutput(taskId) };
|
|
1186
1378
|
}
|
|
1187
1379
|
|
|
@@ -1203,7 +1395,7 @@ export class AgentBridge {
|
|
|
1203
1395
|
private async getWorkflowRuns(
|
|
1204
1396
|
sessionId?: string,
|
|
1205
1397
|
): Promise<{ runs: SerializableWorkflowRun[] }> {
|
|
1206
|
-
const entry = this.
|
|
1398
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1207
1399
|
const runs = await entry.agent.getWorkflowRuns();
|
|
1208
1400
|
return {
|
|
1209
1401
|
runs: runs.map((r) => {
|
|
@@ -1235,7 +1427,7 @@ export class AgentBridge {
|
|
|
1235
1427
|
}
|
|
1236
1428
|
|
|
1237
1429
|
private getPermissionMode(sessionId?: string): { mode: PermissionMode } {
|
|
1238
|
-
const entry = this.
|
|
1430
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1239
1431
|
return { mode: entry.agent.getPermissionMode() };
|
|
1240
1432
|
}
|
|
1241
1433
|
|
|
@@ -1249,7 +1441,7 @@ export class AgentBridge {
|
|
|
1249
1441
|
path: string | null;
|
|
1250
1442
|
content: string | null;
|
|
1251
1443
|
}> {
|
|
1252
|
-
const entry = this.
|
|
1444
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1253
1445
|
const agent = entry.agent;
|
|
1254
1446
|
const planPath =
|
|
1255
1447
|
agent.getPlanFilePath() ?? (await agent.awaitPlanFilePath());
|
|
@@ -1268,7 +1460,7 @@ export class AgentBridge {
|
|
|
1268
1460
|
// ── MCP ───────────────────────────────────────────────────────
|
|
1269
1461
|
|
|
1270
1462
|
private getMcpServers(sessionId?: string): { servers: McpServerStatus[] } {
|
|
1271
|
-
const entry = this.
|
|
1463
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1272
1464
|
return { servers: entry.agent.getMcpServers() };
|
|
1273
1465
|
}
|
|
1274
1466
|
|
|
@@ -1304,7 +1496,7 @@ export class AgentBridge {
|
|
|
1304
1496
|
userPath: string | null;
|
|
1305
1497
|
projectPath: string | null;
|
|
1306
1498
|
} {
|
|
1307
|
-
const entry = this.
|
|
1499
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1308
1500
|
return {
|
|
1309
1501
|
userPath: entry.agent.getUserMcpConfigPath(),
|
|
1310
1502
|
projectPath: entry.agent.getProjectMcpConfigPath(),
|
|
@@ -1314,19 +1506,19 @@ export class AgentBridge {
|
|
|
1314
1506
|
// ── Commands ──────────────────────────────────────────────────
|
|
1315
1507
|
|
|
1316
1508
|
private getSlashCommands(sessionId?: string): { commands: SlashCommand[] } {
|
|
1317
|
-
const entry = this.
|
|
1509
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1318
1510
|
return { commands: entry.agent.getSlashCommands() };
|
|
1319
1511
|
}
|
|
1320
1512
|
|
|
1321
1513
|
private getSubagentConfigurations(sessionId?: string): {
|
|
1322
1514
|
configurations: SubagentConfiguration[];
|
|
1323
1515
|
} {
|
|
1324
|
-
const entry = this.
|
|
1516
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1325
1517
|
return { configurations: entry.agent.getSubagentConfigurations() };
|
|
1326
1518
|
}
|
|
1327
1519
|
|
|
1328
1520
|
private getSkillMetadata(sessionId?: string): { skills: SkillMetadata[] } {
|
|
1329
|
-
const entry = this.
|
|
1521
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1330
1522
|
return { skills: entry.agent.getSkillMetadata() };
|
|
1331
1523
|
}
|
|
1332
1524
|
|
|
@@ -1355,7 +1547,7 @@ export class AgentBridge {
|
|
|
1355
1547
|
hooks: Partial<Record<string, unknown[]>>;
|
|
1356
1548
|
configPath: string | null;
|
|
1357
1549
|
}> {
|
|
1358
|
-
const entry = this.
|
|
1550
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
1359
1551
|
const hooks = await entry.agent.getHooksByScope(scope);
|
|
1360
1552
|
// 回带该 scope 钩子所在 settings.json 的绝对路径(宿主 GUI 打开文件用;
|
|
1361
1553
|
// plugin 钩子来自代码而非配置文件 → null)。
|
|
@@ -1492,12 +1684,31 @@ export class AgentBridge {
|
|
|
1492
1684
|
/** Daemon list: expose the in-memory session registry (live sessions only,
|
|
1493
1685
|
* not disk-scanning). Registration order is preserved. The working directory
|
|
1494
1686
|
* shown is the session's recorded creation workdir (stable identity), not the
|
|
1495
|
-
* live value that drifts with in-session `cd`.
|
|
1687
|
+
* live value that drifts with in-session `cd`.
|
|
1688
|
+
*
|
|
1689
|
+
* A session mid-rebuild (`transitioning`, see `SessionEntry`) reports
|
|
1690
|
+
* `isLoading: true`: its outgoing agent has already been aborted by the
|
|
1691
|
+
* teardown, so its own flag reads false while the replacement is still being
|
|
1692
|
+
* built — reporting that would hand a `wait` the "listed + not busy"
|
|
1693
|
+
* combination it settles on. "Not idle" is the honest reading of "this session
|
|
1694
|
+
* has no agent right now", and it can only make a waiter wait.
|
|
1695
|
+
*
|
|
1696
|
+
* `hasRunningBackgroundWork` / `hasPendingMessages` are the other two halves of
|
|
1697
|
+
* the idle predicate `wait` / `status` share with `wave -p` (print mode): a
|
|
1698
|
+
* session whose foreground turn has ended can still own a running background
|
|
1699
|
+
* bash task or subagent, and the last such task flips to `completed` before its
|
|
1700
|
+
* completion notification is enqueued for the main agent's follow-up turn —
|
|
1701
|
+
* reading only `isLoading` reports "done" in both cases. They are read from the
|
|
1702
|
+
* live agent here rather than tracked separately, and mirror it as-is: the
|
|
1703
|
+
* rebuild window above is already covered by `isLoading`, so there is no need
|
|
1704
|
+
* to also claim a `transitioning` session owns background work it doesn't. */
|
|
1496
1705
|
private listDaemonSessions(): {
|
|
1497
1706
|
sessions: Array<{
|
|
1498
1707
|
sessionId: string;
|
|
1499
1708
|
workingDirectory: string;
|
|
1500
1709
|
isLoading: boolean;
|
|
1710
|
+
hasRunningBackgroundWork: boolean;
|
|
1711
|
+
hasPendingMessages: boolean;
|
|
1501
1712
|
messageCount: number;
|
|
1502
1713
|
}>;
|
|
1503
1714
|
} {
|
|
@@ -1505,7 +1716,9 @@ export class AgentBridge {
|
|
|
1505
1716
|
sessions: [...this.sessions.entries()].map(([sessionId, entry]) => ({
|
|
1506
1717
|
sessionId,
|
|
1507
1718
|
workingDirectory: entry.createdWorkdir,
|
|
1508
|
-
isLoading: entry.agent.isLoading,
|
|
1719
|
+
isLoading: entry.transitioning === true || entry.agent.isLoading,
|
|
1720
|
+
hasRunningBackgroundWork: entry.agent.hasRunningBackgroundWork,
|
|
1721
|
+
hasPendingMessages: entry.agent.hasPendingMessages,
|
|
1509
1722
|
messageCount: entry.agent.displayMessages.length,
|
|
1510
1723
|
})),
|
|
1511
1724
|
};
|
|
@@ -1528,7 +1741,12 @@ export class AgentBridge {
|
|
|
1528
1741
|
}
|
|
1529
1742
|
|
|
1530
1743
|
private async getAccountInfo(): Promise<{
|
|
1531
|
-
|
|
1744
|
+
/**
|
|
1745
|
+
* 计费结论(套餐两根条 + 到期日 + 降级原因),形状见 codechat `GET /api/v1/account` → `billing`
|
|
1746
|
+
* (判定出自与 proxy 扣费同一份代码)。本层只转发、不解析;具体形状由 wave-webview-fixtures
|
|
1747
|
+
* 的 AccountBillingInfo 在宿主/卡片侧约束。
|
|
1748
|
+
*/
|
|
1749
|
+
billing?: unknown;
|
|
1532
1750
|
apiQuota?: { limit: number | null; used: number } | null;
|
|
1533
1751
|
}> {
|
|
1534
1752
|
const authService = AuthService.getInstance();
|
|
@@ -1547,7 +1765,7 @@ export class AgentBridge {
|
|
|
1547
1765
|
throw new Error(`Account query failed (${response.status})`);
|
|
1548
1766
|
}
|
|
1549
1767
|
return (await response.json()) as {
|
|
1550
|
-
|
|
1768
|
+
billing?: unknown;
|
|
1551
1769
|
apiQuota?: { limit: number | null; used: number } | null;
|
|
1552
1770
|
};
|
|
1553
1771
|
}
|
|
@@ -1678,6 +1896,7 @@ export class AgentBridge {
|
|
|
1678
1896
|
marketplace: p.marketplace,
|
|
1679
1897
|
installed: p.installed,
|
|
1680
1898
|
version: p.version,
|
|
1899
|
+
latestVersion: p.latestVersion,
|
|
1681
1900
|
enabled: mergedEnabled[pluginId] !== false,
|
|
1682
1901
|
scope: p.scope,
|
|
1683
1902
|
};
|
|
@@ -1699,10 +1918,14 @@ export class AgentBridge {
|
|
|
1699
1918
|
|
|
1700
1919
|
private async uninstallPlugin(
|
|
1701
1920
|
pluginId: string,
|
|
1921
|
+
scope?: Scope,
|
|
1702
1922
|
workdir?: string,
|
|
1703
1923
|
sessionId?: string,
|
|
1704
1924
|
) {
|
|
1705
|
-
await this.getPluginCore(workdir, sessionId).uninstallPlugin(
|
|
1925
|
+
await this.getPluginCore(workdir, sessionId).uninstallPlugin(
|
|
1926
|
+
pluginId,
|
|
1927
|
+
scope,
|
|
1928
|
+
);
|
|
1706
1929
|
return null;
|
|
1707
1930
|
}
|
|
1708
1931
|
|
|
@@ -1727,6 +1950,21 @@ export class AgentBridge {
|
|
|
1727
1950
|
);
|
|
1728
1951
|
}
|
|
1729
1952
|
|
|
1953
|
+
/**
|
|
1954
|
+
* `/reload-plugins`:把磁盘上的插件状态就地换装进**本进程内全部活跃会话**
|
|
1955
|
+
* (spec ecosystem/plugin.md「插件变更的就地重载」)。不重建会话、sessionId
|
|
1956
|
+
* 不变;没有活跃会话时也返回成功——新会话创建时会按磁盘状态自然装载。
|
|
1957
|
+
*
|
|
1958
|
+
* 每个会话读的是同一份磁盘状态,因此取任一份结果汇报即可(各会话的差异只体现
|
|
1959
|
+
* 在各自注册表的可见性上)。失败不回滚,原样回传给宿主提示。
|
|
1960
|
+
*/
|
|
1961
|
+
private async reloadPlugins(): Promise<PluginReloadResult> {
|
|
1962
|
+
const results = await Promise.all(
|
|
1963
|
+
[...this.sessions.values()].map((entry) => entry.agent.reloadPlugins()),
|
|
1964
|
+
);
|
|
1965
|
+
return results[0] ?? { plugins: [], failures: [] };
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1730
1968
|
private async getProjectSettings(
|
|
1731
1969
|
workdir?: string,
|
|
1732
1970
|
sessionId?: string,
|
|
@@ -1839,10 +2077,36 @@ export class AgentBridge {
|
|
|
1839
2077
|
return this.getPluginCore(workdir, sessionId).updatePlugin(pluginId);
|
|
1840
2078
|
}
|
|
1841
2079
|
|
|
2080
|
+
/**
|
|
2081
|
+
* 更换已安装插件的安装作用域(设置页插件市场):清掉各作用域的启用记录后
|
|
2082
|
+
* 在目标作用域启用,因此旧作用域不再保留该插件。
|
|
2083
|
+
*/
|
|
2084
|
+
private async setPluginScope(
|
|
2085
|
+
pluginId: string,
|
|
2086
|
+
scope: Scope,
|
|
2087
|
+
workdir?: string,
|
|
2088
|
+
sessionId?: string,
|
|
2089
|
+
) {
|
|
2090
|
+
return this.getPluginCore(workdir, sessionId).setPluginScope(
|
|
2091
|
+
pluginId,
|
|
2092
|
+
scope,
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
1842
2096
|
private async listMarketplaces(workdir?: string, sessionId?: string) {
|
|
1843
2097
|
return this.getPluginCore(workdir, sessionId).listMarketplaces();
|
|
1844
2098
|
}
|
|
1845
2099
|
|
|
2100
|
+
/**
|
|
2101
|
+
* 刷新各已注册市场的检出(不装插件),供「打开插件市场界面」时后台调用
|
|
2102
|
+
* (spec 插件市场 A-013 场景 5)。市场级错误在 SDK 内静默记录,宿主侧失败
|
|
2103
|
+
* 也不打断界面(场景 9)。
|
|
2104
|
+
*/
|
|
2105
|
+
private async refreshMarketplaces(workdir?: string, sessionId?: string) {
|
|
2106
|
+
await this.getPluginCore(workdir, sessionId).refreshMarketplaces();
|
|
2107
|
+
return null;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
1846
2110
|
private async addMarketplace(
|
|
1847
2111
|
input: string,
|
|
1848
2112
|
scope?: Scope,
|
|
@@ -1867,8 +2131,16 @@ export class AgentBridge {
|
|
|
1867
2131
|
workdir?: string,
|
|
1868
2132
|
sessionId?: string,
|
|
1869
2133
|
) {
|
|
1870
|
-
|
|
1871
|
-
|
|
2134
|
+
// 这个通道只服务 GUI 的「批量更新插件」按钮:按当前清单升级、不拉检出
|
|
2135
|
+
// (清单刷新由打开视图时的 refreshMarketplaces 承担,spec 插件市场 A-013/
|
|
2136
|
+
// 场景 14)。
|
|
2137
|
+
const updated = await this.getPluginCore(
|
|
2138
|
+
workdir,
|
|
2139
|
+
sessionId,
|
|
2140
|
+
).updateMarketplacePlugins(name);
|
|
2141
|
+
// 实际升级的插件数回给宿主:0 = 该市场已是最新(宿主据此提示,
|
|
2142
|
+
// spec 插件市场场景 13)。
|
|
2143
|
+
return { updated };
|
|
1872
2144
|
}
|
|
1873
2145
|
|
|
1874
2146
|
// ── Callbacks → Notifications ─────────────────────────────────
|
|
@@ -2042,6 +2314,9 @@ export class AgentBridge {
|
|
|
2042
2314
|
ctx.registeredSessionId,
|
|
2043
2315
|
);
|
|
2044
2316
|
},
|
|
2317
|
+
onPlanFileUpdated: (content: string) => {
|
|
2318
|
+
this.emit("planFileUpdated", { content }, ctx.registeredSessionId);
|
|
2319
|
+
},
|
|
2045
2320
|
};
|
|
2046
2321
|
}
|
|
2047
2322
|
|
|
@@ -2052,7 +2327,34 @@ export class AgentBridge {
|
|
|
2052
2327
|
|
|
2053
2328
|
// ── Utils ─────────────────────────────────────────────────────
|
|
2054
2329
|
|
|
2330
|
+
/**
|
|
2331
|
+
* The session entry for a request that CHANGES session state. A session whose
|
|
2332
|
+
* agent is mid-rebuild (`updateConfig` → `SessionEntry.transitioning`) has no
|
|
2333
|
+
* usable agent: its outgoing one is being torn down, so a message injected into
|
|
2334
|
+
* it would be lost and a config write would be overwritten by the rebuild's own
|
|
2335
|
+
* stored config. Those requests are refused with a retryable error instead.
|
|
2336
|
+
* Read-only requests use `requireSessionForRead` and stay available.
|
|
2337
|
+
*/
|
|
2055
2338
|
private requireSession(sessionId?: string): SessionEntry {
|
|
2339
|
+
const entry = this.requireSessionForRead(sessionId);
|
|
2340
|
+
if (entry.transitioning) {
|
|
2341
|
+
throw new RpcError(
|
|
2342
|
+
PROTOCOL_INTERNAL_ERROR,
|
|
2343
|
+
`Session ${sessionId} is being reconfigured (config reload in progress), retry shortly`,
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
return entry;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
/**
|
|
2350
|
+
* The session entry for a request that only READS. During a rebuild the entry
|
|
2351
|
+
* holds the outgoing agent: its messages are this same session's transcript and
|
|
2352
|
+
* its config is the pre-reload one, both of which are the best answer available
|
|
2353
|
+
* for the few hundred milliseconds the rebuild takes. Refusing reads instead
|
|
2354
|
+
* would turn a config reload into a burst of errors in whatever host happens to
|
|
2355
|
+
* be polling (`getMcpServers`, `getSlashCommands`, …).
|
|
2356
|
+
*/
|
|
2357
|
+
private requireSessionForRead(sessionId?: string): SessionEntry {
|
|
2056
2358
|
if (!sessionId) {
|
|
2057
2359
|
throw new RpcError(
|
|
2058
2360
|
PROTOCOL_INTERNAL_ERROR,
|