linkshell-cli 0.3.28 → 0.4.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.
Files changed (62) hide show
  1. package/dist/cli/src/commands/setup.js +20 -2
  2. package/dist/cli/src/commands/setup.js.map +1 -1
  3. package/dist/cli/src/commands/upgrade.js +9 -24
  4. package/dist/cli/src/commands/upgrade.js.map +1 -1
  5. package/dist/cli/src/index.js +28 -26
  6. package/dist/cli/src/index.js.map +1 -1
  7. package/dist/cli/src/providers.d.ts +3 -7
  8. package/dist/cli/src/providers.js +76 -19
  9. package/dist/cli/src/providers.js.map +1 -1
  10. package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -10
  11. package/dist/cli/src/runtime/acp/acp-client.js +20 -146
  12. package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
  13. package/dist/cli/src/runtime/acp/agent-session.d.ts +62 -0
  14. package/dist/cli/src/runtime/acp/agent-session.js +1075 -0
  15. package/dist/cli/src/runtime/acp/agent-session.js.map +1 -0
  16. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +2 -27
  17. package/dist/cli/src/runtime/acp/agent-workspace.js +224 -1008
  18. package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
  19. package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +1 -3
  20. package/dist/cli/src/runtime/acp/claude-sdk-client.js +50 -82
  21. package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
  22. package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +1 -2
  23. package/dist/cli/src/runtime/acp/claude-stream-json-client.js +36 -54
  24. package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
  25. package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -34
  26. package/dist/cli/src/runtime/acp/json-rpc.js +8 -32
  27. package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
  28. package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -3
  29. package/dist/cli/src/runtime/acp/provider-resolver.js +6 -26
  30. package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
  31. package/dist/cli/src/runtime/bridge-session.d.ts +31 -1
  32. package/dist/cli/src/runtime/bridge-session.js +991 -81
  33. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  34. package/dist/cli/src/runtime/screen-fallback.d.ts +1 -1
  35. package/dist/cli/src/runtime/screen-fallback.js +4 -4
  36. package/dist/cli/src/runtime/screen-fallback.js.map +1 -1
  37. package/dist/cli/src/runtime/screen-share.d.ts +1 -1
  38. package/dist/cli/src/runtime/screen-share.js +7 -7
  39. package/dist/cli/src/runtime/screen-share.js.map +1 -1
  40. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  41. package/dist/shared-protocol/src/index.d.ts +11978 -3423
  42. package/dist/shared-protocol/src/index.js +114 -163
  43. package/dist/shared-protocol/src/index.js.map +1 -1
  44. package/package.json +13 -13
  45. package/src/commands/setup.ts +31 -5
  46. package/src/commands/upgrade.ts +8 -28
  47. package/src/index.ts +34 -29
  48. package/src/providers.ts +108 -26
  49. package/src/runtime/acp/acp-client.ts +22 -159
  50. package/src/runtime/acp/agent-session.ts +1180 -0
  51. package/src/runtime/acp/agent-workspace.ts +246 -1094
  52. package/src/runtime/acp/claude-sdk-client.ts +51 -86
  53. package/src/runtime/acp/claude-stream-json-client.ts +42 -59
  54. package/src/runtime/acp/json-rpc.ts +14 -46
  55. package/src/runtime/acp/provider-resolver.ts +6 -29
  56. package/src/runtime/bridge-session.ts +1092 -84
  57. package/src/runtime/screen-fallback.ts +5 -5
  58. package/src/runtime/screen-share.ts +8 -8
  59. package/src/runtime/acp/claude-sessions.ts +0 -563
  60. package/src/runtime/acp/codex-rpc-bridge.ts +0 -233
  61. package/src/runtime/acp/codex-sessions.ts +0 -802
  62. package/src/types/linkshell-gateway.d.ts +0 -18
@@ -1,5 +1,4 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
- import { spawn } from "node:child_process";
3
2
  import { homedir } from "node:os";
4
3
  import { basename, join, relative } from "node:path";
5
4
  import {
@@ -10,9 +9,6 @@ import {
10
9
  import { AcpClient } from "./acp-client.js";
11
10
  import { ClaudeSdkClient } from "./claude-sdk-client.js";
12
11
  import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
13
- import { CodexRpcBridge } from "./codex-rpc-bridge.js";
14
- import { listClaudeStoredSessions, loadClaudeStoredTimeline } from "./claude-sessions.js";
15
- import { loadCodexStoredTimeline } from "./codex-sessions.js";
16
12
  import type { AgentProtocol, AgentProvider } from "./provider-resolver.js";
17
13
  import { resolveAgentCommand } from "./provider-resolver.js";
18
14
 
@@ -21,15 +17,12 @@ type AgentPermissionMode = "read_only" | "workspace_write" | "full_access";
21
17
  type AgentCollaborationMode = "default" | "plan";
22
18
  type AgentCommandExecutionKind = "prompt" | "native" | "local_ui";
23
19
  type AgentCommandSource = "built_in" | "custom" | "project" | "user" | "linkshell";
24
- type AgentSyncSource = "device" | "device-history" | "device-live" | "app-server" | "cache";
25
20
 
26
21
  interface AgentContentBlock {
27
- type: "text" | "image" | "file";
22
+ type: "text" | "image";
28
23
  text?: string;
29
24
  data?: string;
30
25
  mimeType?: string;
31
- path?: string;
32
- name?: string;
33
26
  }
34
27
 
35
28
  interface AgentToolCall {
@@ -165,16 +158,10 @@ interface AgentConversation {
165
158
  title?: string;
166
159
  model?: string;
167
160
  reasoningEffort?: string;
168
- serviceTier?: string;
169
161
  permissionMode?: AgentPermissionMode;
170
162
  collaborationMode?: AgentCollaborationMode;
171
163
  status: AgentStatus;
172
164
  archived: boolean;
173
- timelineRevision?: number;
174
- historyComplete?: boolean;
175
- runningTurnId?: string;
176
- source?: AgentSyncSource;
177
- canonical?: boolean;
178
165
  lastMessagePreview?: string;
179
166
  lastActivityAt: number;
180
167
  createdAt: number;
@@ -185,9 +172,6 @@ interface AgentTimelineItem {
185
172
  conversationId: string;
186
173
  type: "message" | "tool_call" | "plan" | "permission" | "status" | "error";
187
174
  kind?: AgentTimelineKind;
188
- revision?: number;
189
- source?: AgentSyncSource;
190
- canonical?: boolean;
191
175
  turnId?: string;
192
176
  itemId?: string;
193
177
  role?: "user" | "assistant" | "system";
@@ -219,51 +203,13 @@ interface PendingStructuredInputWaiter {
219
203
  source?: string;
220
204
  }
221
205
 
222
- interface AgentRevisionEvent {
223
- revision: number;
224
- item?: AgentTimelineItem;
225
- conversation?: AgentConversation;
226
- }
227
-
228
206
  const PERMISSION_TIMEOUT_MS = 5 * 60_000;
229
207
  const MAX_TIMELINE_ITEMS = 200;
230
- const MAX_SNAPSHOT_ITEMS = 80;
231
- const MAX_SNAPSHOT_TEXT_BYTES = 128 * 1024;
232
- const MAX_DELTA_EVENTS = 500;
233
- const HISTORY_PAGE_MAX_ITEMS = 500;
234
208
 
235
209
  function id(prefix: string): string {
236
210
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
237
211
  }
238
212
 
239
- function clampHistoryCursor(value: string | undefined, fallback: number, max: number): number {
240
- if (!value) return fallback;
241
- const parsed = Number.parseInt(value, 10);
242
- if (!Number.isFinite(parsed)) return fallback;
243
- return Math.max(0, Math.min(max, parsed));
244
- }
245
-
246
- function appServerText(value: unknown): string | undefined {
247
- if (typeof value === "string") {
248
- const text = value.trim();
249
- return text || undefined;
250
- }
251
- if (Array.isArray(value)) {
252
- const text = value
253
- .map((part) => appServerText(part))
254
- .filter(Boolean)
255
- .join("\n")
256
- .trim();
257
- return text || undefined;
258
- }
259
- const record = asRecord(value);
260
- if (!record) return undefined;
261
- if (typeof record.text === "string") return appServerText(record.text);
262
- if (typeof record.content === "string") return appServerText(record.content);
263
- if (typeof record.message === "string") return appServerText(record.message);
264
- return appServerText(record.content ?? record.message ?? record.parts);
265
- }
266
-
267
213
  function stringify(value: unknown): string {
268
214
  if (typeof value === "string") return value;
269
215
  try {
@@ -273,71 +219,6 @@ function stringify(value: unknown): string {
273
219
  }
274
220
  }
275
221
 
276
- function truncateUtf8(value: string | undefined, maxBytes = MAX_SNAPSHOT_TEXT_BYTES): string | undefined {
277
- if (!value) return value;
278
- if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
279
- let end = Math.min(value.length, maxBytes);
280
- while (end > 0 && Buffer.byteLength(value.slice(0, end), "utf8") > maxBytes) {
281
- end = Math.floor(end * 0.9);
282
- }
283
- return `${value.slice(0, end)}\n\n[truncated by LinkShell: original ${Buffer.byteLength(value, "utf8")} bytes]`;
284
- }
285
-
286
- function snapshotContentBlocks(
287
- blocks: AgentContentBlock[] | undefined,
288
- options: { stripImages?: boolean } = {},
289
- ): AgentContentBlock[] | undefined {
290
- if (!blocks) return undefined;
291
- return blocks.map((block) =>
292
- block.type === "image" && options.stripImages !== false
293
- ? { ...block, data: undefined, text: block.text || "图片附件" }
294
- : { ...block, text: truncateUtf8(block.text) },
295
- );
296
- }
297
-
298
- function snapshotTimelineItem(
299
- item: AgentTimelineItem,
300
- options: { stripImages?: boolean } = {},
301
- ): AgentTimelineItem {
302
- return {
303
- ...item,
304
- content: snapshotContentBlocks(item.content, options),
305
- text: truncateUtf8(item.text),
306
- toolCall: item.toolCall
307
- ? {
308
- ...item.toolCall,
309
- input: truncateUtf8(item.toolCall.input),
310
- output: truncateUtf8(item.toolCall.output),
311
- }
312
- : undefined,
313
- commandExecution: item.commandExecution
314
- ? {
315
- ...item.commandExecution,
316
- command: truncateUtf8(item.commandExecution.command, 16 * 1024),
317
- output: truncateUtf8(item.commandExecution.output),
318
- }
319
- : undefined,
320
- fileChange: item.fileChange
321
- ? {
322
- ...item.fileChange,
323
- diff: truncateUtf8(item.fileChange.diff),
324
- summary: truncateUtf8(item.fileChange.summary),
325
- }
326
- : undefined,
327
- permission: item.permission
328
- ? {
329
- ...item.permission,
330
- toolInput: truncateUtf8(item.permission.toolInput),
331
- context: truncateUtf8(item.permission.context),
332
- }
333
- : undefined,
334
- };
335
- }
336
-
337
- function snapshotTimelineItems(items: AgentTimelineItem[]): AgentTimelineItem[] {
338
- return items.slice(-MAX_SNAPSHOT_ITEMS).map((item) => snapshotTimelineItem(item));
339
- }
340
-
341
222
  function asRecord(value: unknown): Record<string, unknown> | undefined {
342
223
  return typeof value === "object" && value ? value as Record<string, unknown> : undefined;
343
224
  }
@@ -643,12 +524,7 @@ function titleFromCwd(cwd: string): string {
643
524
 
644
525
  function textFromBlocks(blocks: AgentContentBlock[]): string {
645
526
  return blocks
646
- .map((block) => {
647
- if (block.type === "text") return block.text ?? "";
648
- if (block.type === "image") return `[${block.mimeType ?? "image"} attachment]`;
649
- if (block.type === "file" && block.path) return `@${block.path}`;
650
- return "";
651
- })
527
+ .map((block) => block.type === "text" ? block.text ?? "" : `[${block.mimeType ?? "image"} attachment]`)
652
528
  .filter(Boolean)
653
529
  .join("\n");
654
530
  }
@@ -681,11 +557,6 @@ function contentBlocksFromValue(value: unknown): AgentContentBlock[] {
681
557
  const text = firstString(raw, ["text", "content", "message"]);
682
558
  return text ? [{ type: "text", text }] : [];
683
559
  }
684
- if (normalizedType === "file" || normalizedType === "inputfile" || normalizedType === "filereference") {
685
- const path = firstString(raw, ["path", "file", "filePath", "file_path", "absolutePath", "relativePath"]);
686
- const text = firstString(raw, ["text", "name", "label"]);
687
- return path ? [{ type: "file", path, name: text, text: text ?? `@${path}` }] : [];
688
- }
689
560
  const nested = raw.content ?? raw.contentItems ?? raw.parts;
690
561
  if (Array.isArray(nested)) return contentBlocksFromValue(nested);
691
562
  const text = firstString(raw, ["text", "message", "content"]);
@@ -881,54 +752,20 @@ function providerLabel(provider: AgentProvider): string {
881
752
  interface AgentModelOption {
882
753
  id: string;
883
754
  label: string;
884
- reasoningEfforts?: string[];
885
- defaultReasoningEffort?: string;
886
- speedTiers?: string[];
887
- supportsImages?: boolean;
888
- description?: string;
889
755
  }
890
756
 
891
757
  interface ProviderRuntimeCapabilities {
892
758
  models?: AgentModelOption[];
893
759
  defaultModel?: string;
894
760
  reasoningEfforts?: string[];
895
- speedTiers?: string[];
896
- defaultServiceTier?: string;
897
761
  commands?: AgentCommandDescriptor[];
898
762
  modes?: AgentModeDescriptor[];
899
763
  currentMode?: string;
900
764
  }
901
765
 
902
766
  const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const;
903
- const CLAUDE_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"] as const;
904
- const ALL_SERVICE_TIERS = ["standard", "fast"] as const;
905
- const AGENT_PERMISSION_MODES: AgentPermissionMode[] = ["read_only", "workspace_write", "full_access"];
906
- const COMMAND_OUTPUT_MAX_BYTES = 96 * 1024;
907
- const LINKSHELL_NATIVE_COMMANDS: Array<{
908
- name: string;
909
- description: string;
910
- category: string;
911
- argsMode?: AgentCommandDescriptor["argsMode"];
912
- destructive?: boolean;
913
- providers?: AgentProvider[];
914
- }> = [
915
- { name: "status", description: "Show current Agent and workspace status", category: "LinkShell", argsMode: "none" },
916
- { name: "plan", description: "Enter Plan mode for the next turn", category: "Agent", argsMode: "none" },
917
- { name: "exit-plan", description: "Exit Plan mode", category: "Agent", argsMode: "none" },
918
- { name: "review", description: "Ask the Agent to review current local changes", category: "Agent", argsMode: "optional" },
919
- { name: "subagents", description: "Ask the Agent to split work across subagents when useful", category: "Agent", argsMode: "optional" },
920
- { name: "compact", description: "Compact the active Codex context", category: "Codex", argsMode: "none", providers: ["codex"] },
921
- { name: "new", description: "Start a fresh Codex thread", category: "Codex", argsMode: "none", providers: ["codex"], destructive: true },
922
- { name: "clear", description: "Start a fresh Agent context for this conversation", category: "Agent", argsMode: "none", destructive: true },
923
- { name: "git-status", description: "Show branch and working tree status", category: "Git", argsMode: "none" },
924
- { name: "git-diff", description: "Show a compact diffstat for current changes", category: "Git", argsMode: "none" },
925
- { name: "git-commit", description: "Commit staged changes with the given message", category: "Git", argsMode: "required" },
926
- { name: "git-pull", description: "Pull with fast-forward only", category: "Git", argsMode: "none" },
927
- { name: "git-push", description: "Push the current branch", category: "Git", argsMode: "none" },
928
- { name: "git-stash", description: "Stash current working tree changes", category: "Git", argsMode: "optional" },
929
- { name: "git-stash-pop", description: "Pop the latest stash", category: "Git", argsMode: "none" },
930
- ];
931
- const CODEX_JSON_RPC_COMMANDS = new Set(["plan", "exit-plan", "review", "compact", "new"]);
767
+ const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"] as const;
768
+ const CODEX_COMMAND_NAMES = ["plan", "exit-plan", "compact", "clear", "status", "review", "subagents"] as const;
932
769
  const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
933
770
  "add-dir",
934
771
  "agents",
@@ -1201,31 +1038,57 @@ function customClaudeCommands(cwd: string): AgentCommandDescriptor[] {
1201
1038
 
1202
1039
  function defaultProviderCommands(provider: AgentProvider, cwd: string, enabled: boolean): AgentCommandDescriptor[] {
1203
1040
  const disabledReason = enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`;
1204
- const linkshellCommands = LINKSHELL_NATIVE_COMMANDS
1205
- .filter((command) => provider !== "codex" || CODEX_JSON_RPC_COMMANDS.has(command.name))
1206
- .filter((command) => !command.providers || command.providers.includes(provider))
1207
- .map((command) => makeCommand({
1041
+ if (provider === "codex") {
1042
+ return CODEX_COMMAND_NAMES.map((name) => makeCommand({
1208
1043
  provider,
1209
- name: command.name,
1210
- description: command.description,
1044
+ name,
1211
1045
  source: "linkshell",
1212
- category: command.category,
1213
- argsMode: command.argsMode ?? "optional",
1214
- destructive: command.destructive,
1046
+ category: name === "plan" || name === "exit-plan" ? "Modes" : "Codex",
1047
+ description: {
1048
+ "plan": "Enter Codex plan mode",
1049
+ "exit-plan": "Exit Codex plan mode",
1050
+ compact: "Compact the current thread",
1051
+ clear: "Start a fresh Codex thread",
1052
+ status: "Show LinkShell agent status",
1053
+ review: "Ask Codex to review local changes",
1054
+ subagents: "Insert a delegation prompt",
1055
+ }[name],
1056
+ argsMode: name === "review" || name === "subagents" ? "optional" : "none",
1057
+ destructive: name === "clear",
1215
1058
  disabledReason,
1216
- executionKind: "native",
1059
+ executionKind: name === "review" || name === "subagents" ? "prompt" : "native",
1217
1060
  }));
1218
- if (provider === "codex") {
1219
- return linkshellCommands;
1220
1061
  }
1221
1062
  if (provider === "claude") {
1063
+ const builtIns = CLAUDE_BUILT_IN_COMMANDS
1064
+ .filter((entry) => isClaudeRemoteFriendlyCommand(entry.name))
1065
+ .map((entry) => makeCommand({
1066
+ provider,
1067
+ name: entry.name,
1068
+ description: entry.description,
1069
+ argsMode: entry.argsMode,
1070
+ destructive: entry.destructive,
1071
+ disabledReason,
1072
+ executionKind: "prompt",
1073
+ }));
1222
1074
  const custom = customClaudeCommands(cwd).map((command) => ({
1223
1075
  ...command,
1224
1076
  disabledReason: command.disabledReason ?? disabledReason,
1225
1077
  }));
1226
- return [...linkshellCommands, ...custom];
1078
+ return [...builtIns, ...custom];
1227
1079
  }
1228
- return linkshellCommands;
1080
+ return [
1081
+ makeCommand({
1082
+ provider,
1083
+ name: "status",
1084
+ source: "linkshell",
1085
+ category: "LinkShell",
1086
+ description: "Show LinkShell agent status",
1087
+ argsMode: "none",
1088
+ disabledReason,
1089
+ executionKind: "native",
1090
+ }),
1091
+ ];
1229
1092
  }
1230
1093
 
1231
1094
  function mergeCommands(...groups: Array<AgentCommandDescriptor[] | undefined>): AgentCommandDescriptor[] {
@@ -1244,81 +1107,6 @@ function mergeCommands(...groups: Array<AgentCommandDescriptor[] | undefined>):
1244
1107
  return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
1245
1108
  }
1246
1109
 
1247
- function isGitNativeCommand(name: string): boolean {
1248
- return name === "git-status" ||
1249
- name === "git-diff" ||
1250
- name === "git-commit" ||
1251
- name === "git-pull" ||
1252
- name === "git-push" ||
1253
- name === "git-stash" ||
1254
- name === "git-stash-pop";
1255
- }
1256
-
1257
- function gitCommandArgs(name: string, args?: string): { display: string; argv: string[] } {
1258
- const message = args?.trim();
1259
- switch (name) {
1260
- case "git-status":
1261
- return { display: "git status --short --branch", argv: ["status", "--short", "--branch"] };
1262
- case "git-diff":
1263
- return { display: "git diff --stat", argv: ["diff", "--stat"] };
1264
- case "git-commit":
1265
- if (!message) throw new Error("请先输入提交信息,例如 /git-commit fix mobile agent timeline");
1266
- return { display: `git commit -m ${JSON.stringify(message)}`, argv: ["commit", "-m", message] };
1267
- case "git-pull":
1268
- return { display: "git pull --ff-only", argv: ["pull", "--ff-only"] };
1269
- case "git-push":
1270
- return { display: "git push", argv: ["push"] };
1271
- case "git-stash":
1272
- return {
1273
- display: "git stash push -u",
1274
- argv: ["stash", "push", "-u", "-m", message || "LinkShell mobile stash"],
1275
- };
1276
- case "git-stash-pop":
1277
- return { display: "git stash pop", argv: ["stash", "pop"] };
1278
- default:
1279
- throw new Error(`未知 Git 命令:/${name}`);
1280
- }
1281
- }
1282
-
1283
- function runProcess(
1284
- command: string,
1285
- args: string[],
1286
- options: { cwd: string; maxBytes?: number },
1287
- ): Promise<{ output: string; exitCode: number | null; signal: NodeJS.Signals | null }> {
1288
- return new Promise((resolve, reject) => {
1289
- const child = spawn(command, args, {
1290
- cwd: options.cwd,
1291
- stdio: ["ignore", "pipe", "pipe"],
1292
- windowsHide: true,
1293
- });
1294
- const maxBytes = options.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES;
1295
- let output = "";
1296
- let bytes = 0;
1297
- let truncated = false;
1298
- const append = (chunk: Buffer) => {
1299
- if (bytes >= maxBytes) {
1300
- truncated = true;
1301
- return;
1302
- }
1303
- const remaining = maxBytes - bytes;
1304
- const slice = chunk.byteLength > remaining ? chunk.subarray(0, remaining) : chunk;
1305
- output += slice.toString("utf8");
1306
- bytes += slice.byteLength;
1307
- if (slice.byteLength < chunk.byteLength) truncated = true;
1308
- };
1309
- child.stdout.on("data", append);
1310
- child.stderr.on("data", append);
1311
- child.once("error", reject);
1312
- child.once("close", (exitCode, signal) => {
1313
- resolve({
1314
- output: `${output.trimEnd()}${truncated ? "\n\n[truncated by LinkShell]" : ""}`,
1315
- exitCode,
1316
- signal,
1317
- });
1318
- });
1319
- });
1320
- }
1321
-
1322
1110
  function runtimeCommands(provider: AgentProvider, value: unknown): AgentCommandDescriptor[] {
1323
1111
  const raw = asRecord(value);
1324
1112
  const commandsValue =
@@ -1362,14 +1150,10 @@ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities
1362
1150
  const raw = asRecord(value);
1363
1151
  const modelsValue =
1364
1152
  Array.isArray(value) ? value :
1365
- Array.isArray(raw?.data) ? raw.data :
1366
1153
  Array.isArray(raw?.models) ? raw.models :
1367
1154
  Array.isArray(raw?.items) ? raw.items :
1368
1155
  Array.isArray(raw?.modelOptions) ? raw.modelOptions :
1369
1156
  [];
1370
- const effortSet = new Set<string>();
1371
- const speedTierSet = new Set<string>(["standard"]);
1372
- let defaultModelFromEntry: string | undefined;
1373
1157
  const models = modelsValue
1374
1158
  .map((entry, index) => {
1375
1159
  const model = asRecord(entry);
@@ -1379,40 +1163,13 @@ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities
1379
1163
  : undefined;
1380
1164
  }
1381
1165
  const modelId = firstString(model, ["id", "model", "name", "value"]) ?? `model-${index + 1}`;
1382
- const label = firstString(model, ["displayName", "label", "title", "name", "model"]) ?? modelId;
1383
- if (model.hidden === true) return undefined;
1384
- const modelEfforts = arrayFromKeys(model, ["supportedReasoningEfforts", "reasoningEfforts"])
1385
- .map((effort) => {
1386
- const rawEffort = asRecord(effort);
1387
- return firstString(rawEffort, ["reasoningEffort", "id", "value"]) ?? (typeof effort === "string" ? effort : undefined);
1388
- })
1389
- .filter((effort): effort is string => Boolean(effort) && ALL_REASONING_EFFORTS.includes(effort as typeof ALL_REASONING_EFFORTS[number]));
1390
- modelEfforts.forEach((effort) => effortSet.add(effort));
1391
- const defaultReasoningEffort = firstString(model, ["defaultReasoningEffort", "default_reasoning_effort"]);
1392
- if (defaultReasoningEffort && ALL_REASONING_EFFORTS.includes(defaultReasoningEffort as typeof ALL_REASONING_EFFORTS[number])) {
1393
- effortSet.add(defaultReasoningEffort);
1394
- }
1395
- const modalities = arrayFromKeys(model, ["inputModalities", "modalities"]);
1396
- const speedTiers = arrayFromKeys(model, ["additionalSpeedTiers", "additional_speed_tiers", "serviceTiers", "service_tiers"])
1397
- .map((tier) => typeof tier === "string" ? tier : firstString(asRecord(tier), ["id", "tier", "value"]))
1398
- .filter((tier): tier is string => Boolean(tier) && ALL_SERVICE_TIERS.includes(tier as typeof ALL_SERVICE_TIERS[number]));
1399
- speedTiers.forEach((tier) => speedTierSet.add(tier));
1400
- if (model.isDefault === true) defaultModelFromEntry = modelId;
1401
- return {
1402
- id: modelId,
1403
- label,
1404
- ...(modelEfforts.length > 0 ? { reasoningEfforts: modelEfforts } : {}),
1405
- ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
1406
- ...(speedTiers.length > 0 ? { speedTiers: ["standard", ...speedTiers.filter((tier) => tier !== "standard")] } : {}),
1407
- ...(modalities.length > 0 ? { supportsImages: modalities.includes("image") } : {}),
1408
- ...(firstString(model, ["description"]) ? { description: firstString(model, ["description"]) } : {}),
1409
- };
1166
+ const label = firstString(model, ["label", "title", "displayName", "name"]) ?? modelId;
1167
+ return { id: modelId, label };
1410
1168
  })
1411
1169
  .filter((entry): entry is AgentModelOption => Boolean(entry));
1412
1170
  const defaultModel =
1413
1171
  firstString(raw, ["defaultModel", "default_model", "currentModel"]) ??
1414
- firstString(asRecord(raw?.defaults), ["model"]) ??
1415
- defaultModelFromEntry;
1172
+ firstString(asRecord(raw?.defaults), ["model"]);
1416
1173
  const effortsValue =
1417
1174
  Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
1418
1175
  Array.isArray(raw?.reasoning_efforts) ? raw.reasoning_efforts :
@@ -1420,14 +1177,11 @@ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities
1420
1177
  undefined;
1421
1178
  const reasoningEfforts = effortsValue
1422
1179
  ?.filter((entry): entry is string => typeof entry === "string" && ALL_REASONING_EFFORTS.includes(entry as typeof ALL_REASONING_EFFORTS[number]));
1423
- const resolvedReasoningEfforts = reasoningEfforts?.length ? reasoningEfforts : [...effortSet];
1424
- const resolvedSpeedTiers = [...speedTierSet];
1425
- if (models.length === 0 && !defaultModel && resolvedReasoningEfforts.length === 0 && resolvedSpeedTiers.length <= 1) return undefined;
1180
+ if (models.length === 0 && !defaultModel && !reasoningEfforts?.length) return undefined;
1426
1181
  return {
1427
- ...(models.length > 0 ? { models } : {}),
1182
+ ...(models.length > 0 ? { models: [{ id: "default", label: "默认模型" }, ...models] } : {}),
1428
1183
  ...(defaultModel ? { defaultModel } : {}),
1429
- ...(resolvedReasoningEfforts.length > 0 ? { reasoningEfforts: resolvedReasoningEfforts } : {}),
1430
- ...(resolvedSpeedTiers.length > 0 ? { speedTiers: resolvedSpeedTiers } : {}),
1184
+ ...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
1431
1185
  };
1432
1186
  }
1433
1187
 
@@ -1449,9 +1203,6 @@ function parseRemoteSessions(value: unknown): Array<{
1449
1203
  model?: string;
1450
1204
  createdAt?: number;
1451
1205
  lastActivityAt?: number;
1452
- archived?: boolean;
1453
- status?: AgentStatus;
1454
- runningTurnId?: string;
1455
1206
  }> {
1456
1207
  const raw = asRecord(value);
1457
1208
  const sessionsValue =
@@ -1467,9 +1218,6 @@ function parseRemoteSessions(value: unknown): Array<{
1467
1218
  model?: string;
1468
1219
  createdAt?: number;
1469
1220
  lastActivityAt?: number;
1470
- archived?: boolean;
1471
- status?: AgentStatus;
1472
- runningTurnId?: string;
1473
1221
  }> = [];
1474
1222
  for (const entry of sessionsValue) {
1475
1223
  const session = asRecord(entry);
@@ -1488,34 +1236,16 @@ function parseRemoteSessions(value: unknown): Array<{
1488
1236
  model: firstString(source, ["model", "modelId"]),
1489
1237
  createdAt: parseTimestamp(source.createdAt ?? source.created_at),
1490
1238
  lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
1491
- archived: typeof source.archived === "boolean" ? source.archived : undefined,
1492
- status: normalizeAgentStatus(firstString(source, ["status", "state", "phase"])),
1493
- runningTurnId: firstString(source, ["runningTurnId", "running_turn_id", "turnId", "activeTurnId"]),
1494
1239
  });
1495
1240
  }
1496
1241
  return result;
1497
1242
  }
1498
1243
 
1499
- function normalizeAgentStatus(value: string | undefined): AgentStatus | undefined {
1500
- if (!value) return undefined;
1501
- const normalized = value.toLowerCase();
1502
- if (normalized === "running" || normalized === "in_progress" || normalized === "busy") return "running";
1503
- if (normalized === "waiting_permission" || normalized === "waiting" || normalized === "blocked") return "waiting_permission";
1504
- if (normalized === "error" || normalized === "failed") return "error";
1505
- if (normalized === "idle" || normalized === "completed" || normalized === "done") return "idle";
1506
- return undefined;
1507
- }
1508
-
1509
1244
  export class AgentWorkspaceProxy {
1510
1245
  private clients = new Map<AgentProvider, AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient>();
1511
- private codexRpcBridge: CodexRpcBridge | undefined;
1512
1246
  private agentProtocols = new Map<AgentProvider, AgentProtocol>();
1513
1247
  private providerCapabilities = new Map<AgentProvider, ProviderRuntimeCapabilities>();
1514
- private providerCapabilityErrors = new Map<AgentProvider, string>();
1515
- private capabilitiesRevision = 0;
1516
1248
  private initialized = false;
1517
- private initializePromise: Promise<void> | undefined;
1518
- private providerStartPromises = new Map<AgentProvider, Promise<AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined>>();
1519
1249
  private status: AgentStatus = "unavailable";
1520
1250
  private error: string | undefined;
1521
1251
  private activeConversationId: string | undefined;
@@ -1524,8 +1254,6 @@ export class AgentWorkspaceProxy {
1524
1254
  private conversations = new Map<string, AgentConversation>();
1525
1255
  private conversationByAgentSessionId = new Map<string, string>();
1526
1256
  private timelines = new Map<string, AgentTimelineItem[]>();
1527
- private conversationRevisions = new Map<string, number>();
1528
- private revisionEvents = new Map<string, AgentRevisionEvent[]>();
1529
1257
  private toolOutputBuffers = new Map<string, string>();
1530
1258
  private pendingPermissions = new Map<string, AgentPermission>();
1531
1259
  private permissionWaiters = new Map<string, PendingPermissionWaiter>();
@@ -1537,7 +1265,7 @@ export class AgentWorkspaceProxy {
1537
1265
 
1538
1266
  constructor(
1539
1267
  private readonly input: {
1540
- hostDeviceId: string;
1268
+ sessionId: string;
1541
1269
  cwd: string;
1542
1270
  availableProviders: AgentProvider[];
1543
1271
  command?: string;
@@ -1548,31 +1276,6 @@ export class AgentWorkspaceProxy {
1548
1276
 
1549
1277
  async handleEnvelope(envelope: Envelope): Promise<void> {
1550
1278
  switch (envelope.type) {
1551
- case "agent.codex.rpc": {
1552
- const payload = parseTypedPayload("agent.codex.rpc", envelope.payload);
1553
- try {
1554
- this.codexRpc().send(payload as never);
1555
- } catch (error) {
1556
- const idValue = payload && typeof payload === "object" && "id" in payload
1557
- ? (payload as { id?: unknown }).id
1558
- : undefined;
1559
- if (typeof idValue === "string" || typeof idValue === "number") {
1560
- this.input.send(createEnvelope({
1561
- type: "agent.codex.rpc",
1562
- hostDeviceId: this.input.hostDeviceId,
1563
- payload: {
1564
- jsonrpc: "2.0",
1565
- id: idValue,
1566
- error: {
1567
- code: -32000,
1568
- message: error instanceof Error ? error.message : String(error),
1569
- },
1570
- },
1571
- }));
1572
- }
1573
- }
1574
- break;
1575
- }
1576
1279
  case "agent.v2.capabilities.request":
1577
1280
  await this.initialize();
1578
1281
  this.sendCapabilities();
@@ -1590,8 +1293,8 @@ export class AgentWorkspaceProxy {
1590
1293
  );
1591
1294
  this.input.send(createEnvelope({
1592
1295
  type: "agent.v2.conversation.list.result",
1593
- hostDeviceId: this.input.hostDeviceId,
1594
- payload: { conversations: conversations.map((conversation) => this.conversationSnapshot(conversation)) },
1296
+ sessionId: this.input.sessionId,
1297
+ payload: { conversations },
1595
1298
  }));
1596
1299
  break;
1597
1300
  }
@@ -1600,16 +1303,6 @@ export class AgentWorkspaceProxy {
1600
1303
  this.sendSnapshot(payload.conversationId);
1601
1304
  break;
1602
1305
  }
1603
- case "agent.v2.history.request": {
1604
- const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
1605
- await this.sendHistoryPage(payload);
1606
- break;
1607
- }
1608
- case "agent.v2.delta.request": {
1609
- const payload = parseTypedPayload("agent.v2.delta.request", envelope.payload);
1610
- this.sendDelta(payload);
1611
- break;
1612
- }
1613
1306
  case "agent.v2.prompt": {
1614
1307
  const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
1615
1308
  await this.sendPrompt(payload);
@@ -1648,68 +1341,35 @@ export class AgentWorkspaceProxy {
1648
1341
  }
1649
1342
 
1650
1343
  stop(): void {
1651
- this.codexRpcBridge?.stop();
1652
- this.codexRpcBridge = undefined;
1653
1344
  for (const client of this.clients.values()) {
1654
1345
  client.stop();
1655
1346
  }
1656
1347
  this.clients.clear();
1657
- this.providerStartPromises.clear();
1658
- this.initializePromise = undefined;
1659
1348
  }
1660
1349
 
1661
1350
  private clientForProvider(provider: AgentProvider): AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined {
1662
1351
  return this.clients.get(provider);
1663
1352
  }
1664
1353
 
1665
- private codexRpc(): CodexRpcBridge {
1666
- this.codexRpcBridge ??= new CodexRpcBridge({
1667
- command: this.input.command,
1668
- cwd: this.input.cwd,
1669
- hostDeviceId: this.input.hostDeviceId,
1670
- send: this.input.send,
1671
- verbose: this.input.verbose,
1672
- });
1673
- return this.codexRpcBridge;
1674
- }
1675
-
1676
1354
  private protocolForProvider(provider: AgentProvider): AgentProtocol | undefined {
1677
1355
  return this.agentProtocols.get(provider);
1678
1356
  }
1679
1357
 
1680
1358
  private async initialize(): Promise<void> {
1681
1359
  if (this.initialized) return;
1682
- if (this.initializePromise) return this.initializePromise;
1683
- this.initializePromise = (async () => {
1684
- // Eagerly start all detected providers so capabilities report real status.
1685
- const startPromises = this.input.availableProviders
1686
- .filter((provider) => provider !== "codex")
1687
- .map((p) => this.ensureProviderClient(p));
1688
- await Promise.allSettled(startPromises);
1689
- this.initialized = true;
1690
- this.status = "idle";
1691
- this.error = undefined;
1692
- this.sendCapabilities();
1693
- })().finally(() => {
1694
- this.initializePromise = undefined;
1695
- });
1696
- return this.initializePromise;
1360
+ this.initialized = true;
1361
+ // Eagerly start all detected providers so capabilities report real status
1362
+ const startPromises = this.input.availableProviders.map((p) => this.ensureProviderClient(p));
1363
+ await Promise.allSettled(startPromises);
1364
+ this.status = "idle";
1365
+ this.error = undefined;
1366
+ this.sendCapabilities();
1697
1367
  }
1698
1368
 
1699
1369
  private async ensureProviderClient(provider: AgentProvider): Promise<AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined> {
1700
1370
  const existing = this.clients.get(provider);
1701
1371
  if (existing) return existing;
1702
- const starting = this.providerStartPromises.get(provider);
1703
- if (starting) return starting;
1704
1372
 
1705
- const startPromise = this.startProviderClient(provider).finally(() => {
1706
- this.providerStartPromises.delete(provider);
1707
- });
1708
- this.providerStartPromises.set(provider, startPromise);
1709
- return startPromise;
1710
- }
1711
-
1712
- private async startProviderClient(provider: AgentProvider): Promise<AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined> {
1713
1373
  const resolved = resolveAgentCommand({
1714
1374
  provider,
1715
1375
  command: this.input.command,
@@ -1809,12 +1469,8 @@ export class AgentWorkspaceProxy {
1809
1469
  try {
1810
1470
  const result = await listModels.call(client);
1811
1471
  const runtimeCapabilities = parseModelListCapabilities(result);
1812
- if (runtimeCapabilities) {
1813
- this.providerCapabilities.set(provider, runtimeCapabilities);
1814
- this.providerCapabilityErrors.delete(provider);
1815
- }
1472
+ if (runtimeCapabilities) this.providerCapabilities.set(provider, runtimeCapabilities);
1816
1473
  } catch (error) {
1817
- this.providerCapabilityErrors.set(provider, error instanceof Error ? error.message : String(error));
1818
1474
  if (this.input.verbose) {
1819
1475
  process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1820
1476
  }
@@ -1823,11 +1479,36 @@ export class AgentWorkspaceProxy {
1823
1479
 
1824
1480
  private async syncProviderSessions(): Promise<void> {
1825
1481
  await this.initialize();
1826
- this.upsertProviderSessions("claude", listClaudeStoredSessions(this.input.cwd));
1827
1482
  for (const [provider, client] of this.clients) {
1828
1483
  try {
1829
1484
  const result = await client.listSessions();
1830
- this.upsertProviderSessions(provider, result);
1485
+ for (const remote of parseRemoteSessions(result)) {
1486
+ const agentSessionId = remote.id;
1487
+ const existingId = this.conversationByAgentSessionId.get(agentSessionId);
1488
+ const now = Date.now();
1489
+ const conversationId = existingId ?? `agent:${agentSessionId}`;
1490
+ const existing = this.conversations.get(conversationId);
1491
+ const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1492
+ const conversation: AgentConversation = {
1493
+ id: conversationId,
1494
+ agentSessionId,
1495
+ provider,
1496
+ cwd,
1497
+ title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
1498
+ model: remote.model ?? existing?.model,
1499
+ reasoningEffort: existing?.reasoningEffort,
1500
+ permissionMode: existing?.permissionMode,
1501
+ collaborationMode: existing?.collaborationMode,
1502
+ status: existing?.status ?? "idle",
1503
+ archived: existing?.archived ?? false,
1504
+ lastMessagePreview: existing?.lastMessagePreview,
1505
+ lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
1506
+ createdAt: remote.createdAt ?? existing?.createdAt ?? now,
1507
+ };
1508
+ this.conversations.set(conversation.id, conversation);
1509
+ this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1510
+ this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1511
+ }
1831
1512
  } catch (error) {
1832
1513
  if (this.input.verbose) {
1833
1514
  process.stderr.write(`[agent:v2] session list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -1836,55 +1517,16 @@ export class AgentWorkspaceProxy {
1836
1517
  }
1837
1518
  }
1838
1519
 
1839
- private upsertProviderSessions(provider: AgentProvider, result: unknown): void {
1840
- for (const remote of parseRemoteSessions(result)) {
1841
- const agentSessionId = remote.id;
1842
- const existingId = this.conversationByAgentSessionId.get(agentSessionId);
1843
- const now = Date.now();
1844
- const conversationId = existingId ?? `agent:${agentSessionId}`;
1845
- const existing = this.conversations.get(conversationId);
1846
- const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1847
- const conversation: AgentConversation = {
1848
- id: conversationId,
1849
- agentSessionId,
1850
- provider,
1851
- cwd,
1852
- title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
1853
- model: remote.model ?? existing?.model,
1854
- reasoningEffort: existing?.reasoningEffort,
1855
- serviceTier: existing?.serviceTier,
1856
- permissionMode: existing?.permissionMode,
1857
- collaborationMode: existing?.collaborationMode,
1858
- status: remote.status ?? existing?.status ?? "idle",
1859
- archived: remote.archived ?? existing?.archived ?? false,
1860
- timelineRevision: existing?.timelineRevision ?? this.getRevision(conversationId),
1861
- historyComplete: existing?.historyComplete ?? false,
1862
- runningTurnId: remote.runningTurnId ?? this.currentTurnIds.get(conversationId),
1863
- source: "device",
1864
- canonical: true,
1865
- lastMessagePreview: existing?.lastMessagePreview,
1866
- lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
1867
- createdAt: remote.createdAt ?? existing?.createdAt ?? now,
1868
- };
1869
- this.conversations.set(conversation.id, conversation);
1870
- this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1871
- if (remote.runningTurnId) this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
1872
- this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1873
- }
1874
- }
1875
-
1876
1520
  private sendCapabilities(): void {
1877
1521
  const providers = this.input.availableProviders.map((provider) => {
1878
1522
  const client = this.clients.get(provider);
1879
- const protocol = provider === "codex" ? "codex-app-server" : this.agentProtocols.get(provider);
1523
+ const protocol = this.agentProtocols.get(provider);
1880
1524
  const runtimeCapabilities = this.providerCapabilities.get(provider);
1881
- const enabled = provider === "codex" ? true : Boolean(client);
1882
- const hasRuntimeModels = Boolean(runtimeCapabilities?.models?.length);
1525
+ const enabled = Boolean(client);
1883
1526
  const supportsImages = enabled && protocolSupportsImages(protocol);
1884
- const isClaudeSdk = protocol === "claude-agent-sdk";
1885
1527
  const isClaudeFallback = protocol === "claude-stream-json";
1886
- const supportsPermission = enabled && (isClaudeSdk || !isClaudeFallback);
1887
- const supportsReasoningEffort = enabled;
1528
+ const supportsPermission = enabled && !isClaudeFallback;
1529
+ const supportsReasoningEffort = enabled && !isClaudeFallback;
1888
1530
  const commands = mergeCommands(
1889
1531
  defaultProviderCommands(provider, this.input.cwd, enabled),
1890
1532
  runtimeCapabilities?.commands,
@@ -1895,23 +1537,21 @@ export class AgentWorkspaceProxy {
1895
1537
  label: providerLabel(provider),
1896
1538
  enabled,
1897
1539
  reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
1898
- providerProtocol: protocol,
1899
1540
  supportsImages,
1900
1541
  supportsPermission,
1901
1542
  supportsPlan: enabled,
1902
1543
  supportsCancel: enabled,
1903
1544
  models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
1904
- defaultModel: runtimeCapabilities?.defaultModel,
1905
- modelsSource: hasRuntimeModels ? "runtime" : enabled ? "fallback" : "unavailable",
1906
- modelListError: this.providerCapabilityErrors.get(provider),
1545
+ defaultModel: runtimeCapabilities?.defaultModel ?? "default",
1907
1546
  reasoningEfforts: supportsReasoningEffort
1908
- ? runtimeCapabilities?.reasoningEfforts ?? (provider === "claude" ? [...CLAUDE_REASONING_EFFORTS] : [...ALL_REASONING_EFFORTS])
1547
+ ? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
1909
1548
  : [],
1910
- speedTiers: provider === "codex" ? runtimeCapabilities?.speedTiers ?? ["standard"] : [],
1911
- defaultServiceTier: provider === "codex" ? runtimeCapabilities?.defaultServiceTier : undefined,
1912
- permissionModes: supportsPermission ? AGENT_PERMISSION_MODES : [],
1549
+ permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
1913
1550
  commands,
1914
- modes: runtimeCapabilities?.modes ?? [],
1551
+ modes: runtimeCapabilities?.modes ?? (provider === "codex" ? [
1552
+ { id: "default", title: "Default", description: "Run normal implementation turns" },
1553
+ { id: "plan", title: "Plan", description: "Discuss and produce an implementation plan first" },
1554
+ ] : []),
1915
1555
  currentMode,
1916
1556
  features: {
1917
1557
  images: supportsImages,
@@ -1919,8 +1559,6 @@ export class AgentWorkspaceProxy {
1919
1559
  plan: enabled,
1920
1560
  cancel: enabled,
1921
1561
  reasoningEffort: supportsReasoningEffort,
1922
- serviceTier: provider === "codex" && Boolean(runtimeCapabilities?.speedTiers?.includes("fast")),
1923
- claudeSdk: isClaudeSdk,
1924
1562
  streamJsonFallback: isClaudeFallback,
1925
1563
  },
1926
1564
  };
@@ -1929,14 +1567,13 @@ export class AgentWorkspaceProxy {
1929
1567
  const anyPermission = providers.some((p) => p.supportsPermission);
1930
1568
  this.input.send(createEnvelope({
1931
1569
  type: "agent.v2.capabilities",
1932
- hostDeviceId: this.input.hostDeviceId,
1570
+ sessionId: this.input.sessionId,
1933
1571
  payload: {
1934
1572
  enabled: anyEnabled,
1935
1573
  provider: this.input.availableProviders[0] ?? "codex",
1936
1574
  providers,
1937
1575
  protocolVersion: 1,
1938
1576
  workspaceProtocolVersion: 2,
1939
- capabilitiesRevision: ++this.capabilitiesRevision,
1940
1577
  error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
1941
1578
  supportsSessionList: anyEnabled,
1942
1579
  supportsSessionLoad: anyEnabled,
@@ -1956,12 +1593,29 @@ export class AgentWorkspaceProxy {
1956
1593
  provider?: AgentProvider;
1957
1594
  model?: string;
1958
1595
  reasoningEffort?: string;
1959
- serviceTier?: string;
1960
1596
  permissionMode?: AgentPermissionMode;
1961
1597
  collaborationMode?: AgentCollaborationMode;
1962
1598
  title?: string;
1963
1599
  }): Promise<AgentConversation | undefined> {
1964
1600
  const provider = payload.provider ?? this.input.availableProviders[0];
1601
+ if (!provider) {
1602
+ return this.openFailure(payload, "没有可用的 Agent provider。");
1603
+ }
1604
+ if (!this.input.availableProviders.includes(provider)) {
1605
+ return this.openFailure(
1606
+ payload,
1607
+ `${providerLabel(provider)} 未安装或不可用。`,
1608
+ );
1609
+ }
1610
+
1611
+ const client = await this.ensureProviderClient(provider);
1612
+ if (!client) {
1613
+ return this.openFailure(
1614
+ payload,
1615
+ `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`,
1616
+ );
1617
+ }
1618
+
1965
1619
  const cwd = payload.cwd ?? this.input.cwd;
1966
1620
  let agentSessionId = payload.agentSessionId;
1967
1621
  let existingConversation =
@@ -1972,43 +1626,18 @@ export class AgentWorkspaceProxy {
1972
1626
  if (payload.conversationId && existingConversation.id !== payload.conversationId) {
1973
1627
  existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
1974
1628
  }
1975
- this.hydrateStoredTimeline(existingConversation);
1976
1629
  this.activeConversationId = existingConversation.id;
1977
- const snapshot = this.latestSnapshot(existingConversation.id);
1978
1630
  this.input.send(createEnvelope({
1979
1631
  type: "agent.v2.conversation.opened",
1980
- hostDeviceId: this.input.hostDeviceId,
1632
+ sessionId: this.input.sessionId,
1981
1633
  payload: {
1982
- conversation: this.conversationSnapshot(existingConversation),
1983
- snapshot: snapshot.items,
1984
- revision: this.getRevision(existingConversation.id),
1985
- cursor: snapshot.cursor,
1986
- hasMore: snapshot.hasMore,
1987
- source: "device-history",
1988
- canonical: true,
1634
+ conversation: existingConversation,
1635
+ snapshot: this.timelines.get(existingConversation.id) ?? [],
1989
1636
  },
1990
1637
  }));
1991
1638
  return existingConversation;
1992
1639
  }
1993
1640
 
1994
- if (!provider) {
1995
- return this.openFailure(payload, "没有可用的 Agent provider。");
1996
- }
1997
- if (!this.input.availableProviders.includes(provider)) {
1998
- return this.openFailure(
1999
- payload,
2000
- `${providerLabel(provider)} 未安装或不可用。`,
2001
- );
2002
- }
2003
-
2004
- const client = await this.ensureProviderClient(provider);
2005
- if (!client) {
2006
- return this.openFailure(
2007
- payload,
2008
- `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`,
2009
- );
2010
- }
2011
-
2012
1641
  try {
2013
1642
  const result = agentSessionId
2014
1643
  ? await client.loadSession({ sessionId: agentSessionId, cwd })
@@ -2025,16 +1654,10 @@ export class AgentWorkspaceProxy {
2025
1654
  title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
2026
1655
  model: payload.model ?? existingConversation?.model,
2027
1656
  reasoningEffort: payload.reasoningEffort ?? existingConversation?.reasoningEffort,
2028
- serviceTier: payload.serviceTier ?? existingConversation?.serviceTier,
2029
1657
  permissionMode: payload.permissionMode ?? existingConversation?.permissionMode,
2030
1658
  collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
2031
1659
  status: "idle",
2032
1660
  archived: existingConversation?.archived ?? false,
2033
- timelineRevision: existingConversation?.timelineRevision ?? this.getRevision(conversationId),
2034
- historyComplete: existingConversation?.historyComplete ?? false,
2035
- runningTurnId: this.currentTurnIds.get(conversationId),
2036
- source: "device",
2037
- canonical: true,
2038
1661
  lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
2039
1662
  lastActivityAt: now,
2040
1663
  createdAt: existingConversation?.createdAt ?? now,
@@ -2043,20 +1666,10 @@ export class AgentWorkspaceProxy {
2043
1666
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
2044
1667
  this.activeConversationId = conversation.id;
2045
1668
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
2046
- this.hydrateStoredTimeline(conversation);
2047
- const snapshot = this.latestSnapshot(conversation.id);
2048
1669
  this.input.send(createEnvelope({
2049
1670
  type: "agent.v2.conversation.opened",
2050
- hostDeviceId: this.input.hostDeviceId,
2051
- payload: {
2052
- conversation: this.conversationSnapshot(conversation),
2053
- snapshot: snapshot.items,
2054
- revision: this.getRevision(conversation.id),
2055
- cursor: snapshot.cursor,
2056
- hasMore: snapshot.hasMore,
2057
- source: "device-history",
2058
- canonical: true,
2059
- },
1671
+ sessionId: this.input.sessionId,
1672
+ payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
2060
1673
  }));
2061
1674
  return conversation;
2062
1675
  } catch (error) {
@@ -2092,10 +1705,6 @@ export class AgentWorkspaceProxy {
2092
1705
  collaborationMode: payload.collaborationMode,
2093
1706
  status: "error",
2094
1707
  archived: false,
2095
- timelineRevision: this.getRevision(fallbackId),
2096
- historyComplete: false,
2097
- source: "device",
2098
- canonical: true,
2099
1708
  lastMessagePreview: message,
2100
1709
  lastActivityAt: now,
2101
1710
  createdAt: now,
@@ -2111,15 +1720,8 @@ export class AgentWorkspaceProxy {
2111
1720
  });
2112
1721
  this.input.send(createEnvelope({
2113
1722
  type: "agent.v2.conversation.opened",
2114
- hostDeviceId: this.input.hostDeviceId,
2115
- payload: {
2116
- conversation: this.conversationSnapshot(conversation),
2117
- snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []),
2118
- revision: this.getRevision(conversation.id),
2119
- hasMore: false,
2120
- source: "device",
2121
- canonical: true,
2122
- },
1723
+ sessionId: this.input.sessionId,
1724
+ payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
2123
1725
  }));
2124
1726
  return conversation;
2125
1727
  }
@@ -2128,11 +1730,10 @@ export class AgentWorkspaceProxy {
2128
1730
  conversationId: string;
2129
1731
  clientMessageId: string;
2130
1732
  contentBlocks: AgentContentBlock[];
2131
- model?: string | null;
2132
- reasoningEffort?: string | null;
2133
- serviceTier?: string | null;
2134
- permissionMode?: AgentPermissionMode | null;
2135
- collaborationMode?: AgentCollaborationMode | null;
1733
+ model?: string;
1734
+ reasoningEffort?: string;
1735
+ permissionMode?: AgentPermissionMode;
1736
+ collaborationMode?: AgentCollaborationMode;
2136
1737
  }): Promise<void> {
2137
1738
  const conversation =
2138
1739
  this.conversations.get(payload.conversationId) ??
@@ -2175,25 +1776,44 @@ export class AgentWorkspaceProxy {
2175
1776
  return;
2176
1777
  }
2177
1778
 
2178
- if (Object.prototype.hasOwnProperty.call(payload, "model")) {
2179
- conversation.model = payload.model ?? undefined;
2180
- }
2181
- if (Object.prototype.hasOwnProperty.call(payload, "reasoningEffort")) {
2182
- conversation.reasoningEffort = payload.reasoningEffort ?? undefined;
2183
- }
2184
- if (Object.prototype.hasOwnProperty.call(payload, "serviceTier")) {
2185
- conversation.serviceTier = payload.serviceTier ?? undefined;
2186
- }
2187
- if (Object.prototype.hasOwnProperty.call(payload, "permissionMode")) {
2188
- conversation.permissionMode = payload.permissionMode ?? undefined;
2189
- }
2190
- if (Object.prototype.hasOwnProperty.call(payload, "collaborationMode")) {
2191
- conversation.collaborationMode = payload.collaborationMode ?? "default";
2192
- }
1779
+ const priorSettings = {
1780
+ model: conversation.model,
1781
+ reasoningEffort: conversation.reasoningEffort,
1782
+ permissionMode: conversation.permissionMode,
1783
+ };
1784
+ conversation.model = payload.model ?? conversation.model;
1785
+ conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
1786
+ conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
1787
+ conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
2193
1788
  conversation.status = "running";
2194
1789
  conversation.lastActivityAt = Date.now();
2195
1790
  this.activeConversationId = conversation.id;
2196
1791
 
1792
+ if (payload.model && payload.model !== priorSettings.model) {
1793
+ const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
1794
+ const label = runtimeCapabilities?.models?.find((m) => m.id === payload.model)?.label ?? payload.model;
1795
+ this.emitNotice({
1796
+ conversationId: conversation.id,
1797
+ kind: "model_changed",
1798
+ title: `已切换模型 · ${label}`,
1799
+ detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${payload.model}`,
1800
+ });
1801
+ }
1802
+ if (payload.reasoningEffort && payload.reasoningEffort !== priorSettings.reasoningEffort) {
1803
+ this.emitNotice({
1804
+ conversationId: conversation.id,
1805
+ kind: "effort_changed",
1806
+ title: `思考强度 · ${payload.reasoningEffort}`,
1807
+ });
1808
+ }
1809
+ if (payload.permissionMode && payload.permissionMode !== priorSettings.permissionMode) {
1810
+ this.emitNotice({
1811
+ conversationId: conversation.id,
1812
+ kind: "permission_changed",
1813
+ title: `权限模式 · ${payload.permissionMode}`,
1814
+ });
1815
+ }
1816
+
2197
1817
  const userText = textFromBlocks(payload.contentBlocks);
2198
1818
  this.addItem(conversation.id, {
2199
1819
  id: payload.clientMessageId,
@@ -2211,11 +1831,10 @@ export class AgentWorkspaceProxy {
2211
1831
  sessionId: conversation.agentSessionId,
2212
1832
  content: payload.contentBlocks,
2213
1833
  clientMessageId: payload.clientMessageId,
2214
- model: conversation.model,
2215
- reasoningEffort: conversation.reasoningEffort,
2216
- serviceTier: conversation.serviceTier,
2217
- permissionMode: conversation.permissionMode,
2218
- collaborationMode: conversation.collaborationMode,
1834
+ model: payload.model,
1835
+ reasoningEffort: payload.reasoningEffort,
1836
+ permissionMode: payload.permissionMode,
1837
+ collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
2219
1838
  cwd: conversation.cwd,
2220
1839
  });
2221
1840
  const nextAgentSessionId = this.extractSessionId(result);
@@ -2308,7 +1927,6 @@ export class AgentWorkspaceProxy {
2308
1927
  contentBlocks: [{ type: "text", text: rawText }],
2309
1928
  model: conversation.model,
2310
1929
  reasoningEffort: conversation.reasoningEffort,
2311
- serviceTier: conversation.serviceTier,
2312
1930
  permissionMode: conversation.permissionMode,
2313
1931
  collaborationMode: conversation.collaborationMode,
2314
1932
  });
@@ -2352,8 +1970,21 @@ export class AgentWorkspaceProxy {
2352
1970
  return;
2353
1971
  }
2354
1972
 
2355
- if (isGitNativeCommand(command.name)) {
2356
- await this.executeGitNativeCommand(conversation, command.name, args);
1973
+ if (conversation.provider !== "codex") {
1974
+ const message = `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`;
1975
+ this.emitNotice({
1976
+ conversationId: conversation.id,
1977
+ kind: "native_unsupported",
1978
+ title: `${command.title} 不支持`,
1979
+ detail: `${providerLabel(conversation.provider)} 当前未实现这个命令。`,
1980
+ });
1981
+ this.addItem(conversation.id, {
1982
+ id: id("error"),
1983
+ conversationId: conversation.id,
1984
+ type: "error",
1985
+ error: message,
1986
+ createdAt: now,
1987
+ });
2357
1988
  return;
2358
1989
  }
2359
1990
 
@@ -2370,23 +2001,6 @@ export class AgentWorkspaceProxy {
2370
2001
  return;
2371
2002
  }
2372
2003
 
2373
- if (command.name === "review" || command.name === "subagents") {
2374
- const prompt = command.name === "review"
2375
- ? args || "Review the current local changes."
2376
- : args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
2377
- await this.sendPrompt({
2378
- conversationId: conversation.id,
2379
- clientMessageId: id(command.name),
2380
- contentBlocks: [{ type: "text", text: prompt }],
2381
- model: conversation.model,
2382
- reasoningEffort: conversation.reasoningEffort,
2383
- serviceTier: conversation.serviceTier,
2384
- permissionMode: conversation.permissionMode,
2385
- collaborationMode: conversation.collaborationMode,
2386
- });
2387
- return;
2388
- }
2389
-
2390
2004
  if (command.name === "compact") {
2391
2005
  if (!(client instanceof AcpClient)) throw new Error("当前 Codex runtime 不支持原生 compact。");
2392
2006
  conversation.status = "running";
@@ -2407,7 +2021,7 @@ export class AgentWorkspaceProxy {
2407
2021
  return;
2408
2022
  }
2409
2023
 
2410
- if (command.name === "clear" || command.name === "new") {
2024
+ if (command.name === "clear") {
2411
2025
  if (!client) throw new Error("Agent provider 不在线。");
2412
2026
  const result = await client.newSession({ cwd: conversation.cwd });
2413
2027
  const nextAgentSessionId = this.extractSessionId(result) ?? id("agent-session");
@@ -2424,6 +2038,22 @@ export class AgentWorkspaceProxy {
2424
2038
  return;
2425
2039
  }
2426
2040
 
2041
+ if (command.name === "review" || command.name === "subagents") {
2042
+ const prompt = command.name === "review"
2043
+ ? args || "Review the current local changes."
2044
+ : args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
2045
+ await this.sendPrompt({
2046
+ conversationId: conversation.id,
2047
+ clientMessageId: id(command.name),
2048
+ contentBlocks: [{ type: "text", text: prompt }],
2049
+ model: conversation.model,
2050
+ reasoningEffort: conversation.reasoningEffort,
2051
+ permissionMode: conversation.permissionMode,
2052
+ collaborationMode: conversation.collaborationMode,
2053
+ });
2054
+ return;
2055
+ }
2056
+
2427
2057
  throw new Error(`命令暂未实现:/${command.name}`);
2428
2058
  } catch (error) {
2429
2059
  const message = error instanceof Error ? error.message : String(error);
@@ -2438,46 +2068,6 @@ export class AgentWorkspaceProxy {
2438
2068
  }
2439
2069
  }
2440
2070
 
2441
- private async executeGitNativeCommand(
2442
- conversation: AgentConversation,
2443
- commandName: string,
2444
- args?: string,
2445
- ): Promise<void> {
2446
- const git = gitCommandArgs(commandName, args);
2447
- const toolId = id(commandName);
2448
- const now = Date.now();
2449
- conversation.status = "running";
2450
- conversation.lastMessagePreview = git.display;
2451
- conversation.lastActivityAt = now;
2452
- this.emitConversation(conversation);
2453
- this.upsertTool(conversation.id, {
2454
- id: toolId,
2455
- name: "命令",
2456
- input: `${git.display}\n\ncwd: ${conversation.cwd}`,
2457
- createdAt: now,
2458
- status: "running",
2459
- });
2460
- const result = await runProcess("git", git.argv, {
2461
- cwd: conversation.cwd,
2462
- maxBytes: COMMAND_OUTPUT_MAX_BYTES,
2463
- });
2464
- const ok = result.exitCode === 0;
2465
- this.upsertTool(conversation.id, {
2466
- id: toolId,
2467
- name: "命令",
2468
- input: `${git.display}\n\ncwd: ${conversation.cwd}`,
2469
- output: result.output || (ok ? "完成" : `退出码 ${result.exitCode ?? "unknown"}`),
2470
- createdAt: now,
2471
- status: ok ? "completed" : "failed",
2472
- });
2473
- conversation.status = ok ? "idle" : "error";
2474
- conversation.lastMessagePreview = ok
2475
- ? `${git.display} 完成`
2476
- : `${git.display} 失败`;
2477
- conversation.lastActivityAt = Date.now();
2478
- this.emitConversation(conversation);
2479
- }
2480
-
2481
2071
  private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
2482
2072
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
2483
2073
  return this.handleStructuredInput(params, true);
@@ -2512,12 +2102,20 @@ export class AgentWorkspaceProxy {
2512
2102
  this.sendCapabilities();
2513
2103
  }
2514
2104
  }
2105
+ const initModel = firstString(asRecord(params), ["model", "currentModel"]);
2106
+ if (initModel && conversationId) {
2107
+ const conversation = this.conversations.get(conversationId);
2108
+ if (conversation && conversation.model !== initModel) {
2109
+ conversation.model = initModel;
2110
+ conversation.lastActivityAt = Date.now();
2111
+ this.emitConversation(conversation);
2112
+ }
2113
+ }
2515
2114
  return;
2516
2115
  }
2517
2116
  if (
2518
2117
  method.startsWith("account/") ||
2519
2118
  method.startsWith("mcpServer/startupStatus/") ||
2520
- method.startsWith("remoteControl/") ||
2521
2119
  method === "thread/status/changed" ||
2522
2120
  method === "thread/tokenUsage/updated" ||
2523
2121
  method === "serverRequest/resolved" ||
@@ -2545,11 +2143,12 @@ export class AgentWorkspaceProxy {
2545
2143
  this.conversationByAgentSessionId.set(agentSessionId, conversationId);
2546
2144
  const conversation = this.conversations.get(conversationId);
2547
2145
  if (conversation) {
2548
- if (conversation.agentSessionId && conversation.agentSessionId !== agentSessionId) {
2549
- this.conversationByAgentSessionId.delete(conversation.agentSessionId);
2550
- }
2551
2146
  conversation.agentSessionId = agentSessionId;
2552
2147
  conversation.lastActivityAt = Date.now();
2148
+ const startedModel = firstString(asRecord(params), ["model", "currentModel"]);
2149
+ if (startedModel && conversation.model !== startedModel) {
2150
+ conversation.model = startedModel;
2151
+ }
2553
2152
  this.emitConversation(conversation);
2554
2153
  }
2555
2154
  }
@@ -3129,7 +2728,7 @@ export class AgentWorkspaceProxy {
3129
2728
  this.upsertItem(conversationId, item);
3130
2729
  this.input.send(createEnvelope({
3131
2730
  type: "agent.v2.permission.request",
3132
- hostDeviceId: this.input.hostDeviceId,
2731
+ sessionId: this.input.sessionId,
3133
2732
  payload: { conversationId, ...permission, item },
3134
2733
  }));
3135
2734
 
@@ -3140,14 +2739,6 @@ export class AgentWorkspaceProxy {
3140
2739
  this.permissionWaiters.delete(requestId);
3141
2740
  this.permissionSources.delete(requestId);
3142
2741
  resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
3143
- this.markPermission(conversationId, requestId, {
3144
- permissionOutcome: "cancelled",
3145
- optionId: "cancelled",
3146
- permissionLive: false,
3147
- permissionPending: false,
3148
- permissionExpired: true,
3149
- permissionError: "等待授权超时",
3150
- });
3151
2742
  this.updateConversationStatus(conversationId, "idle");
3152
2743
  }, PERMISSION_TIMEOUT_MS);
3153
2744
  this.permissionWaiters.set(requestId, { resolve, timer });
@@ -3187,8 +2778,6 @@ export class AgentWorkspaceProxy {
3187
2778
  this.markPermission(payload.conversationId, payload.requestId, {
3188
2779
  permissionOutcome: payload.outcome,
3189
2780
  optionId: selectedOptionId,
3190
- permissionLive: false,
3191
- permissionExpired: false,
3192
2781
  permissionError: undefined,
3193
2782
  permissionPending: false,
3194
2783
  });
@@ -3249,228 +2838,6 @@ export class AgentWorkspaceProxy {
3249
2838
  });
3250
2839
  }
3251
2840
 
3252
- private getRevision(conversationId: string): number {
3253
- return this.conversationRevisions.get(conversationId) ??
3254
- this.conversations.get(conversationId)?.timelineRevision ??
3255
- 0;
3256
- }
3257
-
3258
- private setRevisionFloor(conversationId: string, revision: number): number {
3259
- const nextRevision = Math.max(this.getRevision(conversationId), revision);
3260
- this.conversationRevisions.set(conversationId, nextRevision);
3261
- const conversation = this.conversations.get(conversationId);
3262
- if (conversation) {
3263
- conversation.timelineRevision = nextRevision;
3264
- conversation.runningTurnId = this.currentTurnIds.get(conversationId);
3265
- }
3266
- return nextRevision;
3267
- }
3268
-
3269
- private conversationSnapshot(conversation: AgentConversation): AgentConversation {
3270
- return {
3271
- ...conversation,
3272
- timelineRevision: this.getRevision(conversation.id),
3273
- historyComplete: conversation.historyComplete ?? false,
3274
- runningTurnId: this.currentTurnIds.get(conversation.id),
3275
- source: conversation.source ?? "device",
3276
- canonical: conversation.canonical ?? true,
3277
- };
3278
- }
3279
-
3280
- private annotateTimelineItem(
3281
- item: AgentTimelineItem,
3282
- revision: number | undefined,
3283
- source: AgentSyncSource,
3284
- ): AgentTimelineItem {
3285
- return {
3286
- ...item,
3287
- revision: revision ?? item.revision,
3288
- source: item.source ?? source,
3289
- canonical: item.canonical ?? true,
3290
- };
3291
- }
3292
-
3293
- private recordRevisionEvent(
3294
- conversationId: string,
3295
- change: { item?: AgentTimelineItem; conversation?: AgentConversation },
3296
- ): AgentRevisionEvent {
3297
- const revision = this.getRevision(conversationId) + 1;
3298
- this.conversationRevisions.set(conversationId, revision);
3299
- const conversation = this.conversations.get(conversationId);
3300
- if (conversation) {
3301
- conversation.timelineRevision = revision;
3302
- conversation.runningTurnId = this.currentTurnIds.get(conversationId);
3303
- }
3304
- const event: AgentRevisionEvent = {
3305
- revision,
3306
- item: change.item ? this.annotateTimelineItem(change.item, revision, "device-live") : undefined,
3307
- conversation: change.conversation ? this.conversationSnapshot(change.conversation) : undefined,
3308
- };
3309
- const events = this.revisionEvents.get(conversationId) ?? [];
3310
- events.push(event);
3311
- if (events.length > MAX_DELTA_EVENTS) {
3312
- events.splice(0, events.length - MAX_DELTA_EVENTS);
3313
- }
3314
- this.revisionEvents.set(conversationId, events);
3315
- return event;
3316
- }
3317
-
3318
- private storedTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): AgentTimelineItem[] {
3319
- if (!conversation.agentSessionId) return [];
3320
- const result = conversation.provider === "codex"
3321
- ? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd, { maxItems })
3322
- : conversation.provider === "claude"
3323
- ? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id, { maxItems })
3324
- : { items: [] };
3325
- return result.items
3326
- .sort((a, b) => a.createdAt - b.createdAt)
3327
- .map((item, index) => this.annotateTimelineItem(item as AgentTimelineItem, index + 1, "device-history"));
3328
- }
3329
-
3330
- private canonicalTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): AgentTimelineItem[] {
3331
- const merged = new Map<string, AgentTimelineItem>();
3332
- for (const item of this.storedTimeline(conversation, maxItems)) {
3333
- merged.set(item.id, item);
3334
- }
3335
- for (const item of this.timelines.get(conversation.id) ?? []) {
3336
- merged.set(item.id, this.annotateTimelineItem(item, item.revision, item.source ?? "device-live"));
3337
- }
3338
- const items = [...merged.values()]
3339
- .sort((a, b) => a.createdAt - b.createdAt)
3340
- .slice(-maxItems);
3341
- this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
3342
- return items;
3343
- }
3344
-
3345
- private async appServerTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): Promise<AgentTimelineItem[]> {
3346
- if (conversation.provider !== "codex" || !conversation.agentSessionId) return [];
3347
- if (this.protocolForProvider(conversation.provider) !== "codex-app-server") return [];
3348
- const client = this.clientForProvider(conversation.provider);
3349
- const listTurns = (client as { listTurns?: (input: { sessionId: string; limit?: number }) => Promise<unknown> } | undefined)?.listTurns;
3350
- if (typeof listTurns !== "function") return [];
3351
- try {
3352
- const result = await listTurns.call(client, { sessionId: conversation.agentSessionId, limit: maxItems });
3353
- return this.timelineFromAppServerTurns(conversation.id, conversation.agentSessionId, result);
3354
- } catch (error) {
3355
- if (this.input.verbose) {
3356
- process.stderr.write(`[agent:v2] thread/turns/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
3357
- }
3358
- return [];
3359
- }
3360
- }
3361
-
3362
- private timelineFromAppServerTurns(
3363
- conversationId: string,
3364
- agentSessionId: string,
3365
- value: unknown,
3366
- ): AgentTimelineItem[] {
3367
- const raw = asRecord(value);
3368
- const turns =
3369
- Array.isArray(value) ? value :
3370
- Array.isArray(raw?.turns) ? raw.turns :
3371
- Array.isArray(raw?.items) ? raw.items :
3372
- Array.isArray(raw?.entries) ? raw.entries :
3373
- [];
3374
- const items: AgentTimelineItem[] = [];
3375
- turns.forEach((entry, index) => {
3376
- const turn = asRecord(entry);
3377
- if (!turn) return;
3378
- const turnId = firstString(turn, ["id", "turnId", "turn_id"]) ?? `turn-${index + 1}`;
3379
- const createdAt = parseTimestamp(turn.createdAt ?? turn.created_at ?? turn.startedAt ?? turn.started_at) ?? Date.now() + index;
3380
- const updatedAt = parseTimestamp(turn.updatedAt ?? turn.updated_at ?? turn.completedAt ?? turn.completed_at);
3381
- const userText = appServerText(turn.input ?? turn.prompt ?? turn.user ?? turn.userMessage ?? turn.request);
3382
- if (userText) {
3383
- items.push({
3384
- id: `app-server:${agentSessionId}:${turnId}:user`,
3385
- conversationId,
3386
- type: "message",
3387
- kind: "chat",
3388
- turnId,
3389
- role: "user",
3390
- content: [{ type: "text", text: userText }],
3391
- text: userText,
3392
- createdAt,
3393
- updatedAt,
3394
- metadata: { source: "app-server", provider: "codex" },
3395
- });
3396
- }
3397
- const assistantText = appServerText(turn.output ?? turn.response ?? turn.assistant ?? turn.assistantMessage ?? turn.result);
3398
- if (assistantText) {
3399
- items.push({
3400
- id: `app-server:${agentSessionId}:${turnId}:assistant`,
3401
- conversationId,
3402
- type: "message",
3403
- kind: "chat",
3404
- turnId,
3405
- role: "assistant",
3406
- content: [{ type: "text", text: assistantText }],
3407
- text: assistantText,
3408
- createdAt: updatedAt ?? createdAt + 1,
3409
- updatedAt,
3410
- metadata: { source: "app-server", provider: "codex" },
3411
- });
3412
- }
3413
- const nestedItems = Array.isArray(turn.items) ? turn.items : Array.isArray(turn.messages) ? turn.messages : [];
3414
- for (const nested of nestedItems) {
3415
- const nestedRecord = asRecord(nested);
3416
- if (!nestedRecord) continue;
3417
- const text = appServerText(nestedRecord.content ?? nestedRecord.text ?? nestedRecord.message);
3418
- const role = nestedRecord.role === "user" || nestedRecord.role === "assistant" || nestedRecord.role === "system"
3419
- ? nestedRecord.role
3420
- : undefined;
3421
- if (!text || !role) continue;
3422
- const itemId = firstString(nestedRecord, ["id", "itemId", "messageId"]) ?? `${role}-${items.length + 1}`;
3423
- items.push({
3424
- id: `app-server:${agentSessionId}:${turnId}:${itemId}`,
3425
- conversationId,
3426
- type: "message",
3427
- kind: "chat",
3428
- turnId,
3429
- itemId,
3430
- role,
3431
- content: [{ type: "text", text }],
3432
- text,
3433
- createdAt: parseTimestamp(nestedRecord.createdAt ?? nestedRecord.created_at) ?? createdAt + items.length,
3434
- updatedAt: parseTimestamp(nestedRecord.updatedAt ?? nestedRecord.updated_at),
3435
- metadata: { source: "app-server", provider: "codex" },
3436
- });
3437
- }
3438
- });
3439
- return items
3440
- .sort((a, b) => a.createdAt - b.createdAt)
3441
- .map((item, index) => this.annotateTimelineItem(item, index + 1, "app-server"));
3442
- }
3443
-
3444
- private mergeCanonicalTimelineItems(items: AgentTimelineItem[], maxItems: number): AgentTimelineItem[] {
3445
- const byKey = new Map<string, AgentTimelineItem>();
3446
- for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
3447
- const key = item.type === "message" && item.role && item.text
3448
- ? `${item.role}:${item.text.replace(/\s+/g, " ").trim().slice(0, 500)}`
3449
- : item.id;
3450
- const existing = byKey.get(key);
3451
- if (!existing || item.source === "app-server" || (item.updatedAt ?? item.createdAt) >= (existing.updatedAt ?? existing.createdAt)) {
3452
- byKey.set(key, item);
3453
- }
3454
- }
3455
- return [...byKey.values()]
3456
- .sort((a, b) => a.createdAt - b.createdAt)
3457
- .slice(-maxItems);
3458
- }
3459
-
3460
- private latestSnapshot(conversationId: string): {
3461
- items: AgentTimelineItem[];
3462
- cursor?: string;
3463
- hasMore: boolean;
3464
- } {
3465
- const timeline = this.timelines.get(conversationId) ?? [];
3466
- const start = Math.max(0, timeline.length - MAX_SNAPSHOT_ITEMS);
3467
- return {
3468
- items: timeline.slice(start).map((item) => snapshotTimelineItem(item)),
3469
- cursor: start > 0 ? String(start) : undefined,
3470
- hasMore: start > 0,
3471
- };
3472
- }
3473
-
3474
2841
  private addItem(conversationId: string, item: AgentTimelineItem): void {
3475
2842
  this.rememberItemConversationId(conversationId, item);
3476
2843
  const timeline = this.timelines.get(conversationId) ?? [];
@@ -3508,8 +2875,6 @@ export class AgentWorkspaceProxy {
3508
2875
  ...existing,
3509
2876
  ...toolCall,
3510
2877
  id: targetToolId,
3511
- input: toolCall.input ?? existing?.input,
3512
- output: toolCall.output ?? existing?.output,
3513
2878
  createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
3514
2879
  };
3515
2880
  this.toolConversationIds.set(toolCall.id, conversationId);
@@ -3588,7 +2953,6 @@ export class AgentWorkspaceProxy {
3588
2953
  conversation.title = conversation.title ?? target.title;
3589
2954
  conversation.model = conversation.model ?? target.model;
3590
2955
  conversation.reasoningEffort = conversation.reasoningEffort ?? target.reasoningEffort;
3591
- conversation.serviceTier = conversation.serviceTier ?? target.serviceTier;
3592
2956
  conversation.permissionMode = conversation.permissionMode ?? target.permissionMode;
3593
2957
  conversation.lastMessagePreview = conversation.lastMessagePreview ?? target.lastMessagePreview;
3594
2958
  conversation.createdAt = Math.min(conversation.createdAt, target.createdAt);
@@ -3605,17 +2969,6 @@ export class AgentWorkspaceProxy {
3605
2969
  .sort((a, b) => a.createdAt - b.createdAt)
3606
2970
  .slice(-MAX_TIMELINE_ITEMS),
3607
2971
  );
3608
- const oldRevision = this.conversationRevisions.get(oldId) ?? 0;
3609
- if (oldRevision > 0) {
3610
- this.conversationRevisions.delete(oldId);
3611
- this.setRevisionFloor(newId, oldRevision);
3612
- }
3613
- const oldEvents = this.revisionEvents.get(oldId);
3614
- if (oldEvents) {
3615
- this.revisionEvents.delete(oldId);
3616
- const existingEvents = this.revisionEvents.get(newId) ?? [];
3617
- this.revisionEvents.set(newId, [...existingEvents, ...oldEvents].slice(-MAX_DELTA_EVENTS));
3618
- }
3619
2972
 
3620
2973
  for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
3621
2974
  if (conversationId === oldId) {
@@ -3651,48 +3004,18 @@ export class AgentWorkspaceProxy {
3651
3004
 
3652
3005
  private emitItem(conversationId: string, item: AgentTimelineItem): void {
3653
3006
  const conversation = this.conversations.get(conversationId);
3654
- const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
3655
- const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
3656
3007
  this.input.send(createEnvelope({
3657
3008
  type: "agent.v2.event",
3658
- hostDeviceId: this.input.hostDeviceId,
3659
- payload: {
3660
- conversationId,
3661
- conversation: event.conversation,
3662
- item: event.item,
3663
- revision: event.revision,
3664
- source: "device-live",
3665
- canonical: true,
3666
- },
3009
+ sessionId: this.input.sessionId,
3010
+ payload: { conversationId, conversation, item },
3667
3011
  }));
3668
3012
  }
3669
3013
 
3670
3014
  private emitConversation(conversation: AgentConversation): void {
3671
- const event = this.recordRevisionEvent(conversation.id, { conversation });
3672
3015
  this.input.send(createEnvelope({
3673
3016
  type: "agent.v2.event",
3674
- hostDeviceId: this.input.hostDeviceId,
3675
- payload: {
3676
- conversationId: conversation.id,
3677
- conversation: event.conversation,
3678
- revision: event.revision,
3679
- source: "device-live",
3680
- canonical: true,
3681
- },
3682
- }));
3683
- this.input.send(createEnvelope({
3684
- type: "agent.v2.running_state",
3685
- hostDeviceId: this.input.hostDeviceId,
3686
- payload: {
3687
- conversationId: conversation.id,
3688
- status: conversation.status,
3689
- runningTurnId: this.currentTurnIds.get(conversation.id),
3690
- revision: event.revision,
3691
- error: conversation.status === "error" ? conversation.lastMessagePreview : undefined,
3692
- updatedAt: conversation.lastActivityAt,
3693
- source: "device-live",
3694
- canonical: true,
3695
- },
3017
+ sessionId: this.input.sessionId,
3018
+ payload: { conversationId: conversation.id, conversation },
3696
3019
  }));
3697
3020
  }
3698
3021
 
@@ -3707,6 +3030,26 @@ export class AgentWorkspaceProxy {
3707
3030
  });
3708
3031
  }
3709
3032
 
3033
+ private emitNotice(input: {
3034
+ conversationId?: string;
3035
+ kind:
3036
+ | "model_changed"
3037
+ | "effort_changed"
3038
+ | "permission_changed"
3039
+ | "native_unsupported"
3040
+ | "info"
3041
+ | "warning";
3042
+ title: string;
3043
+ detail?: string;
3044
+ durationMs?: number;
3045
+ }): void {
3046
+ this.input.send(createEnvelope({
3047
+ type: "agent.v2.notice",
3048
+ sessionId: this.input.sessionId,
3049
+ payload: input,
3050
+ }));
3051
+ }
3052
+
3710
3053
  private updateConversationPreview(
3711
3054
  conversationId: string,
3712
3055
  text: string,
@@ -3734,186 +3077,21 @@ export class AgentWorkspaceProxy {
3734
3077
  }
3735
3078
 
3736
3079
  private sendSnapshot(conversationId?: string): void {
3737
- if (conversationId) {
3738
- const conversation = this.conversations.get(conversationId);
3739
- if (conversation) this.hydrateStoredTimeline(conversation);
3740
- } else if (this.activeConversationId) {
3741
- const conversation = this.conversations.get(this.activeConversationId);
3742
- if (conversation) this.hydrateStoredTimeline(conversation);
3743
- }
3744
- const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
3745
- const snapshot = conversationId ? this.latestSnapshot(conversationId) : { items: [], cursor: undefined, hasMore: false };
3080
+ const conversations = [...this.conversations.values()];
3081
+ const items = conversationId
3082
+ ? this.timelines.get(conversationId) ?? []
3083
+ : [...this.timelines.values()].flat();
3746
3084
  this.input.send(createEnvelope({
3747
3085
  type: "agent.v2.snapshot",
3748
- hostDeviceId: this.input.hostDeviceId,
3086
+ sessionId: this.input.sessionId,
3749
3087
  payload: {
3750
3088
  conversations,
3751
3089
  activeConversationId: this.activeConversationId,
3752
- items: snapshot.items,
3753
- revision: conversationId ? this.getRevision(conversationId) : undefined,
3754
- cursor: snapshot.cursor,
3755
- hasMore: snapshot.hasMore,
3756
- source: "device-history",
3757
- canonical: true,
3090
+ items,
3758
3091
  },
3759
3092
  }));
3760
3093
  }
3761
3094
 
3762
- private async sendHistoryPage(payload: {
3763
- conversationId: string;
3764
- cursor?: string;
3765
- limit?: number;
3766
- direction?: "older" | "newer";
3767
- }): Promise<void> {
3768
- const conversation = this.conversations.get(payload.conversationId);
3769
- if (!conversation) return;
3770
- const limit = Math.min(Math.max(payload.limit ?? MAX_SNAPSHOT_ITEMS, 1), 200);
3771
- const items = this.mergeCanonicalTimelineItems([
3772
- ...this.canonicalTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
3773
- ...await this.appServerTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
3774
- ], HISTORY_PAGE_MAX_ITEMS);
3775
- this.timelines.set(
3776
- conversation.id,
3777
- items.slice(-MAX_TIMELINE_ITEMS).map((item) => this.annotateTimelineItem(item, item.revision, item.source ?? "device-history")),
3778
- );
3779
- for (const item of this.timelines.get(conversation.id) ?? []) {
3780
- this.rememberItemConversationId(conversation.id, item);
3781
- }
3782
-
3783
- const direction = payload.direction ?? "older";
3784
- let page: AgentTimelineItem[];
3785
- let cursor: string | undefined;
3786
- let hasMore = false;
3787
- if (direction === "newer") {
3788
- const start = clampHistoryCursor(payload.cursor, 0, items.length);
3789
- const end = Math.min(items.length, start + limit);
3790
- page = items.slice(start, end);
3791
- hasMore = end < items.length;
3792
- cursor = hasMore ? String(end) : undefined;
3793
- } else {
3794
- const end = clampHistoryCursor(payload.cursor, items.length, items.length);
3795
- const start = Math.max(0, end - limit);
3796
- page = items.slice(start, end);
3797
- hasMore = start > 0;
3798
- cursor = hasMore ? String(start) : undefined;
3799
- }
3800
- conversation.historyComplete = !hasMore;
3801
- const revision = this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
3802
- this.input.send(createEnvelope({
3803
- type: "agent.v2.history.page",
3804
- hostDeviceId: this.input.hostDeviceId,
3805
- payload: {
3806
- conversationId: conversation.id,
3807
- conversation: this.conversationSnapshot(conversation),
3808
- items: page.map((item) => snapshotTimelineItem(item)),
3809
- revision,
3810
- cursor,
3811
- hasMore,
3812
- source: "device-history",
3813
- canonical: true,
3814
- },
3815
- }));
3816
- }
3817
-
3818
- private sendDelta(payload: {
3819
- conversationId: string;
3820
- sinceRevision?: number;
3821
- limit?: number;
3822
- }): void {
3823
- const conversation = this.conversations.get(payload.conversationId);
3824
- if (!conversation) return;
3825
- this.hydrateStoredTimeline(conversation);
3826
- const sinceRevision = payload.sinceRevision ?? 0;
3827
- const limit = Math.min(Math.max(payload.limit ?? 100, 1), 500);
3828
- const events = this.revisionEvents.get(conversation.id) ?? [];
3829
- const oldestAvailable = events[0]?.revision ?? this.getRevision(conversation.id);
3830
- const newestRevision = this.getRevision(conversation.id);
3831
- const reset = sinceRevision > 0 &&
3832
- (
3833
- sinceRevision > newestRevision ||
3834
- (
3835
- sinceRevision < newestRevision &&
3836
- (events.length === 0 || sinceRevision < oldestAvailable - 1)
3837
- )
3838
- );
3839
-
3840
- if (reset) {
3841
- const snapshot = this.latestSnapshot(conversation.id);
3842
- this.input.send(createEnvelope({
3843
- type: "agent.v2.delta",
3844
- hostDeviceId: this.input.hostDeviceId,
3845
- payload: {
3846
- conversationId: conversation.id,
3847
- conversation: this.conversationSnapshot(conversation),
3848
- items: snapshot.items,
3849
- sinceRevision,
3850
- revision: newestRevision,
3851
- reset: true,
3852
- cursor: snapshot.cursor,
3853
- hasMore: snapshot.hasMore,
3854
- source: "device-history",
3855
- canonical: true,
3856
- },
3857
- }));
3858
- return;
3859
- }
3860
-
3861
- const changed = events
3862
- .filter((event) => event.revision > sinceRevision)
3863
- .slice(-limit);
3864
- const itemsById = new Map<string, AgentTimelineItem>();
3865
- for (const event of changed) {
3866
- if (event.item) itemsById.set(event.item.id, event.item);
3867
- }
3868
- this.input.send(createEnvelope({
3869
- type: "agent.v2.delta",
3870
- hostDeviceId: this.input.hostDeviceId,
3871
- payload: {
3872
- conversationId: conversation.id,
3873
- conversation: this.conversationSnapshot(conversation),
3874
- items: [...itemsById.values()].map((item) => snapshotTimelineItem(item)),
3875
- sinceRevision,
3876
- revision: newestRevision,
3877
- reset: false,
3878
- hasMore: changed.length === limit && events.some((event) => event.revision > sinceRevision && event.revision < changed[0]!.revision),
3879
- source: "device-live",
3880
- canonical: true,
3881
- },
3882
- }));
3883
- }
3884
-
3885
- private hydrateStoredTimeline(conversation: AgentConversation): void {
3886
- if (!conversation.agentSessionId) return;
3887
- const existing = this.timelines.get(conversation.id) ?? [];
3888
- if (existing.length > 0) {
3889
- this.setRevisionFloor(conversation.id, Math.max(
3890
- existing.length,
3891
- ...existing.map((item) => item.revision ?? 0),
3892
- ));
3893
- return;
3894
- }
3895
- const stored = this.storedTimeline(conversation, MAX_TIMELINE_ITEMS);
3896
- conversation.historyComplete = stored.length <= MAX_SNAPSHOT_ITEMS;
3897
- if (stored.length === 0) {
3898
- this.setRevisionFloor(conversation.id, this.getRevision(conversation.id));
3899
- return;
3900
- }
3901
- const items = stored
3902
- .sort((a, b) => a.createdAt - b.createdAt)
3903
- .slice(-MAX_TIMELINE_ITEMS);
3904
- this.timelines.set(conversation.id, items);
3905
- for (const item of items) this.rememberItemConversationId(conversation.id, item);
3906
- this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
3907
- const lastMessage = [...items].reverse().find((item) => item.text?.trim());
3908
- if (lastMessage?.text && !conversation.lastMessagePreview) {
3909
- conversation.lastMessagePreview = previewText(lastMessage.text);
3910
- }
3911
- const lastActivityAt = items.at(-1)?.createdAt;
3912
- if (lastActivityAt) {
3913
- conversation.lastActivityAt = Math.max(conversation.lastActivityAt, lastActivityAt);
3914
- }
3915
- }
3916
-
3917
3095
  private conversationIdFromParams(params: unknown): string | undefined {
3918
3096
  const raw = asRecord(params);
3919
3097
  if (!raw) return undefined;
@@ -3971,8 +3149,6 @@ export class AgentWorkspaceProxy {
3971
3149
  private rememberTurnConversationId(conversationId: string, turnId: string): void {
3972
3150
  this.currentTurnIds.set(conversationId, turnId);
3973
3151
  this.turnConversationIds.set(turnId, conversationId);
3974
- const conversation = this.conversations.get(conversationId);
3975
- if (conversation) conversation.runningTurnId = turnId;
3976
3152
  }
3977
3153
 
3978
3154
  private forgetCurrentTurn(conversationId: string, turnId?: string): void {
@@ -3980,8 +3156,6 @@ export class AgentWorkspaceProxy {
3980
3156
  this.currentTurnIds.delete(conversationId);
3981
3157
  if (turnId) this.turnConversationIds.delete(turnId);
3982
3158
  if (currentTurnId && currentTurnId !== turnId) this.turnConversationIds.delete(currentTurnId);
3983
- const conversation = this.conversations.get(conversationId);
3984
- if (conversation) conversation.runningTurnId = undefined;
3985
3159
  }
3986
3160
 
3987
3161
  private rememberItemConversationId(conversationId: string, item: AgentTimelineItem): void {
@@ -4023,54 +3197,32 @@ export class AgentWorkspaceProxy {
4023
3197
 
4024
3198
  private cancelPendingPermissions(conversationId?: string): void {
4025
3199
  for (const [requestId, waiter] of this.permissionWaiters) {
4026
- const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
4027
- if (conversationId && ownerConversationId && ownerConversationId !== conversationId) continue;
4028
3200
  clearTimeout(waiter.timer);
4029
3201
  waiter.resolve(formatPermissionResponse(
4030
3202
  this.permissionSources.get(requestId),
4031
3203
  "cancelled",
4032
3204
  "cancelled",
4033
3205
  ));
4034
- if (ownerConversationId) {
4035
- this.markPermission(ownerConversationId, requestId, {
4036
- permissionOutcome: "cancelled",
4037
- optionId: "cancelled",
4038
- permissionLive: false,
4039
- permissionPending: false,
4040
- permissionExpired: false,
4041
- permissionError: "已停止",
4042
- });
4043
- }
4044
- this.permissionWaiters.delete(requestId);
4045
3206
  this.pendingPermissions.delete(requestId);
4046
3207
  this.permissionSources.delete(requestId);
4047
3208
  }
3209
+ this.permissionWaiters.clear();
4048
3210
  for (const [requestId, waiter] of this.structuredInputWaiters) {
4049
- const pending = this.pendingStructuredInputs.get(requestId);
4050
- if (conversationId && pending?.conversationId && pending.conversationId !== conversationId) continue;
4051
3211
  clearTimeout(waiter.timer);
4052
3212
  waiter.resolve(formatStructuredInputResponse({}));
3213
+ const pending = this.pendingStructuredInputs.get(requestId);
4053
3214
  if (pending) {
4054
3215
  this.markStructuredInput(pending.conversationId, requestId, {
4055
3216
  inputPending: false,
4056
3217
  inputError: "已停止",
4057
3218
  });
4058
3219
  }
4059
- this.structuredInputWaiters.delete(requestId);
4060
3220
  this.pendingStructuredInputs.delete(requestId);
4061
3221
  }
3222
+ this.structuredInputWaiters.clear();
4062
3223
  if (conversationId) this.updateConversationStatus(conversationId, "idle");
4063
3224
  }
4064
3225
 
4065
- private conversationIdForPermissionRequest(requestId: string): string | undefined {
4066
- for (const [conversationId, timeline] of this.timelines) {
4067
- if (timeline.some((item) => item.type === "permission" && item.permission?.requestId === requestId)) {
4068
- return conversationId;
4069
- }
4070
- }
4071
- return undefined;
4072
- }
4073
-
4074
3226
  private extractSessionId(value: unknown): string | undefined {
4075
3227
  const raw = asRecord(value);
4076
3228
  if (!raw) return undefined;