linkshell-cli 0.3.27 → 0.3.29

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 (64) 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 +19 -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 +1 -24
  17. package/dist/cli/src/runtime/acp/agent-workspace.js +162 -981
  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 +35 -81
  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 +35 -54
  24. package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
  25. package/dist/cli/src/runtime/acp/codex-rpc-bridge.js +12 -1
  26. package/dist/cli/src/runtime/acp/codex-rpc-bridge.js.map +1 -1
  27. package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -34
  28. package/dist/cli/src/runtime/acp/json-rpc.js +8 -32
  29. package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
  30. package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -3
  31. package/dist/cli/src/runtime/acp/provider-resolver.js +6 -26
  32. package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
  33. package/dist/cli/src/runtime/bridge-session.d.ts +31 -1
  34. package/dist/cli/src/runtime/bridge-session.js +991 -81
  35. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  36. package/dist/cli/src/runtime/screen-fallback.d.ts +1 -1
  37. package/dist/cli/src/runtime/screen-fallback.js +4 -4
  38. package/dist/cli/src/runtime/screen-fallback.js.map +1 -1
  39. package/dist/cli/src/runtime/screen-share.d.ts +1 -1
  40. package/dist/cli/src/runtime/screen-share.js +7 -7
  41. package/dist/cli/src/runtime/screen-share.js.map +1 -1
  42. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  43. package/dist/shared-protocol/src/index.d.ts +11940 -3423
  44. package/dist/shared-protocol/src/index.js +98 -164
  45. package/dist/shared-protocol/src/index.js.map +1 -1
  46. package/package.json +5 -5
  47. package/src/commands/setup.ts +31 -5
  48. package/src/commands/upgrade.ts +8 -28
  49. package/src/index.ts +34 -29
  50. package/src/providers.ts +108 -26
  51. package/src/runtime/acp/acp-client.ts +21 -159
  52. package/src/runtime/acp/agent-session.ts +1180 -0
  53. package/src/runtime/acp/agent-workspace.ts +170 -1068
  54. package/src/runtime/acp/claude-sdk-client.ts +37 -85
  55. package/src/runtime/acp/claude-stream-json-client.ts +41 -59
  56. package/src/runtime/acp/json-rpc.ts +14 -46
  57. package/src/runtime/acp/provider-resolver.ts +6 -29
  58. package/src/runtime/bridge-session.ts +1092 -84
  59. package/src/runtime/screen-fallback.ts +5 -5
  60. package/src/runtime/screen-share.ts +8 -8
  61. package/src/runtime/acp/claude-sessions.ts +0 -563
  62. package/src/runtime/acp/codex-rpc-bridge.ts +0 -221
  63. package/src/runtime/acp/codex-sessions.ts +0 -802
  64. 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,31 +1236,15 @@ 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
1249
  private status: AgentStatus = "unavailable";
1518
1250
  private error: string | undefined;
@@ -1522,8 +1254,6 @@ export class AgentWorkspaceProxy {
1522
1254
  private conversations = new Map<string, AgentConversation>();
1523
1255
  private conversationByAgentSessionId = new Map<string, string>();
1524
1256
  private timelines = new Map<string, AgentTimelineItem[]>();
1525
- private conversationRevisions = new Map<string, number>();
1526
- private revisionEvents = new Map<string, AgentRevisionEvent[]>();
1527
1257
  private toolOutputBuffers = new Map<string, string>();
1528
1258
  private pendingPermissions = new Map<string, AgentPermission>();
1529
1259
  private permissionWaiters = new Map<string, PendingPermissionWaiter>();
@@ -1535,7 +1265,7 @@ export class AgentWorkspaceProxy {
1535
1265
 
1536
1266
  constructor(
1537
1267
  private readonly input: {
1538
- hostDeviceId: string;
1268
+ sessionId: string;
1539
1269
  cwd: string;
1540
1270
  availableProviders: AgentProvider[];
1541
1271
  command?: string;
@@ -1546,31 +1276,6 @@ export class AgentWorkspaceProxy {
1546
1276
 
1547
1277
  async handleEnvelope(envelope: Envelope): Promise<void> {
1548
1278
  switch (envelope.type) {
1549
- case "agent.codex.rpc": {
1550
- const payload = parseTypedPayload("agent.codex.rpc", envelope.payload);
1551
- try {
1552
- this.codexRpc().send(payload as never);
1553
- } catch (error) {
1554
- const idValue = payload && typeof payload === "object" && "id" in payload
1555
- ? (payload as { id?: unknown }).id
1556
- : undefined;
1557
- if (typeof idValue === "string" || typeof idValue === "number") {
1558
- this.input.send(createEnvelope({
1559
- type: "agent.codex.rpc",
1560
- hostDeviceId: this.input.hostDeviceId,
1561
- payload: {
1562
- jsonrpc: "2.0",
1563
- id: idValue,
1564
- error: {
1565
- code: -32000,
1566
- message: error instanceof Error ? error.message : String(error),
1567
- },
1568
- },
1569
- }));
1570
- }
1571
- }
1572
- break;
1573
- }
1574
1279
  case "agent.v2.capabilities.request":
1575
1280
  await this.initialize();
1576
1281
  this.sendCapabilities();
@@ -1588,8 +1293,8 @@ export class AgentWorkspaceProxy {
1588
1293
  );
1589
1294
  this.input.send(createEnvelope({
1590
1295
  type: "agent.v2.conversation.list.result",
1591
- hostDeviceId: this.input.hostDeviceId,
1592
- payload: { conversations: conversations.map((conversation) => this.conversationSnapshot(conversation)) },
1296
+ sessionId: this.input.sessionId,
1297
+ payload: { conversations },
1593
1298
  }));
1594
1299
  break;
1595
1300
  }
@@ -1598,16 +1303,6 @@ export class AgentWorkspaceProxy {
1598
1303
  this.sendSnapshot(payload.conversationId);
1599
1304
  break;
1600
1305
  }
1601
- case "agent.v2.history.request": {
1602
- const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
1603
- await this.sendHistoryPage(payload);
1604
- break;
1605
- }
1606
- case "agent.v2.delta.request": {
1607
- const payload = parseTypedPayload("agent.v2.delta.request", envelope.payload);
1608
- this.sendDelta(payload);
1609
- break;
1610
- }
1611
1306
  case "agent.v2.prompt": {
1612
1307
  const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
1613
1308
  await this.sendPrompt(payload);
@@ -1646,8 +1341,6 @@ export class AgentWorkspaceProxy {
1646
1341
  }
1647
1342
 
1648
1343
  stop(): void {
1649
- this.codexRpcBridge?.stop();
1650
- this.codexRpcBridge = undefined;
1651
1344
  for (const client of this.clients.values()) {
1652
1345
  client.stop();
1653
1346
  }
@@ -1658,17 +1351,6 @@ export class AgentWorkspaceProxy {
1658
1351
  return this.clients.get(provider);
1659
1352
  }
1660
1353
 
1661
- private codexRpc(): CodexRpcBridge {
1662
- this.codexRpcBridge ??= new CodexRpcBridge({
1663
- command: this.input.command,
1664
- cwd: this.input.cwd,
1665
- hostDeviceId: this.input.hostDeviceId,
1666
- send: this.input.send,
1667
- verbose: this.input.verbose,
1668
- });
1669
- return this.codexRpcBridge;
1670
- }
1671
-
1672
1354
  private protocolForProvider(provider: AgentProvider): AgentProtocol | undefined {
1673
1355
  return this.agentProtocols.get(provider);
1674
1356
  }
@@ -1677,9 +1359,7 @@ export class AgentWorkspaceProxy {
1677
1359
  if (this.initialized) return;
1678
1360
  this.initialized = true;
1679
1361
  // Eagerly start all detected providers so capabilities report real status
1680
- const startPromises = this.input.availableProviders
1681
- .filter((provider) => provider !== "codex")
1682
- .map((p) => this.ensureProviderClient(p));
1362
+ const startPromises = this.input.availableProviders.map((p) => this.ensureProviderClient(p));
1683
1363
  await Promise.allSettled(startPromises);
1684
1364
  this.status = "idle";
1685
1365
  this.error = undefined;
@@ -1789,12 +1469,8 @@ export class AgentWorkspaceProxy {
1789
1469
  try {
1790
1470
  const result = await listModels.call(client);
1791
1471
  const runtimeCapabilities = parseModelListCapabilities(result);
1792
- if (runtimeCapabilities) {
1793
- this.providerCapabilities.set(provider, runtimeCapabilities);
1794
- this.providerCapabilityErrors.delete(provider);
1795
- }
1472
+ if (runtimeCapabilities) this.providerCapabilities.set(provider, runtimeCapabilities);
1796
1473
  } catch (error) {
1797
- this.providerCapabilityErrors.set(provider, error instanceof Error ? error.message : String(error));
1798
1474
  if (this.input.verbose) {
1799
1475
  process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1800
1476
  }
@@ -1803,11 +1479,36 @@ export class AgentWorkspaceProxy {
1803
1479
 
1804
1480
  private async syncProviderSessions(): Promise<void> {
1805
1481
  await this.initialize();
1806
- this.upsertProviderSessions("claude", listClaudeStoredSessions(this.input.cwd));
1807
1482
  for (const [provider, client] of this.clients) {
1808
1483
  try {
1809
1484
  const result = await client.listSessions();
1810
- 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
+ }
1811
1512
  } catch (error) {
1812
1513
  if (this.input.verbose) {
1813
1514
  process.stderr.write(`[agent:v2] session list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -1816,55 +1517,16 @@ export class AgentWorkspaceProxy {
1816
1517
  }
1817
1518
  }
1818
1519
 
1819
- private upsertProviderSessions(provider: AgentProvider, result: unknown): void {
1820
- for (const remote of parseRemoteSessions(result)) {
1821
- const agentSessionId = remote.id;
1822
- const existingId = this.conversationByAgentSessionId.get(agentSessionId);
1823
- const now = Date.now();
1824
- const conversationId = existingId ?? `agent:${agentSessionId}`;
1825
- const existing = this.conversations.get(conversationId);
1826
- const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1827
- const conversation: AgentConversation = {
1828
- id: conversationId,
1829
- agentSessionId,
1830
- provider,
1831
- cwd,
1832
- title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
1833
- model: remote.model ?? existing?.model,
1834
- reasoningEffort: existing?.reasoningEffort,
1835
- serviceTier: existing?.serviceTier,
1836
- permissionMode: existing?.permissionMode,
1837
- collaborationMode: existing?.collaborationMode,
1838
- status: remote.status ?? existing?.status ?? "idle",
1839
- archived: remote.archived ?? existing?.archived ?? false,
1840
- timelineRevision: existing?.timelineRevision ?? this.getRevision(conversationId),
1841
- historyComplete: existing?.historyComplete ?? false,
1842
- runningTurnId: remote.runningTurnId ?? this.currentTurnIds.get(conversationId),
1843
- source: "device",
1844
- canonical: true,
1845
- lastMessagePreview: existing?.lastMessagePreview,
1846
- lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
1847
- createdAt: remote.createdAt ?? existing?.createdAt ?? now,
1848
- };
1849
- this.conversations.set(conversation.id, conversation);
1850
- this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1851
- if (remote.runningTurnId) this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
1852
- this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1853
- }
1854
- }
1855
-
1856
1520
  private sendCapabilities(): void {
1857
1521
  const providers = this.input.availableProviders.map((provider) => {
1858
1522
  const client = this.clients.get(provider);
1859
- const protocol = provider === "codex" ? "codex-app-server" : this.agentProtocols.get(provider);
1523
+ const protocol = this.agentProtocols.get(provider);
1860
1524
  const runtimeCapabilities = this.providerCapabilities.get(provider);
1861
- const enabled = provider === "codex" ? true : Boolean(client);
1862
- const hasRuntimeModels = Boolean(runtimeCapabilities?.models?.length);
1525
+ const enabled = Boolean(client);
1863
1526
  const supportsImages = enabled && protocolSupportsImages(protocol);
1864
- const isClaudeSdk = protocol === "claude-agent-sdk";
1865
1527
  const isClaudeFallback = protocol === "claude-stream-json";
1866
- const supportsPermission = enabled && (isClaudeSdk || !isClaudeFallback);
1867
- const supportsReasoningEffort = enabled;
1528
+ const supportsPermission = enabled && !isClaudeFallback;
1529
+ const supportsReasoningEffort = enabled && !isClaudeFallback;
1868
1530
  const commands = mergeCommands(
1869
1531
  defaultProviderCommands(provider, this.input.cwd, enabled),
1870
1532
  runtimeCapabilities?.commands,
@@ -1875,23 +1537,21 @@ export class AgentWorkspaceProxy {
1875
1537
  label: providerLabel(provider),
1876
1538
  enabled,
1877
1539
  reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
1878
- providerProtocol: protocol,
1879
1540
  supportsImages,
1880
1541
  supportsPermission,
1881
1542
  supportsPlan: enabled,
1882
1543
  supportsCancel: enabled,
1883
1544
  models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
1884
- defaultModel: runtimeCapabilities?.defaultModel,
1885
- modelsSource: hasRuntimeModels ? "runtime" : enabled ? "fallback" : "unavailable",
1886
- modelListError: this.providerCapabilityErrors.get(provider),
1545
+ defaultModel: runtimeCapabilities?.defaultModel ?? "default",
1887
1546
  reasoningEfforts: supportsReasoningEffort
1888
- ? runtimeCapabilities?.reasoningEfforts ?? (provider === "claude" ? [...CLAUDE_REASONING_EFFORTS] : [...ALL_REASONING_EFFORTS])
1547
+ ? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
1889
1548
  : [],
1890
- speedTiers: provider === "codex" ? runtimeCapabilities?.speedTiers ?? ["standard"] : [],
1891
- defaultServiceTier: provider === "codex" ? runtimeCapabilities?.defaultServiceTier : undefined,
1892
- permissionModes: supportsPermission ? AGENT_PERMISSION_MODES : [],
1549
+ permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
1893
1550
  commands,
1894
- 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
+ ] : []),
1895
1555
  currentMode,
1896
1556
  features: {
1897
1557
  images: supportsImages,
@@ -1899,8 +1559,6 @@ export class AgentWorkspaceProxy {
1899
1559
  plan: enabled,
1900
1560
  cancel: enabled,
1901
1561
  reasoningEffort: supportsReasoningEffort,
1902
- serviceTier: provider === "codex" && Boolean(runtimeCapabilities?.speedTiers?.includes("fast")),
1903
- claudeSdk: isClaudeSdk,
1904
1562
  streamJsonFallback: isClaudeFallback,
1905
1563
  },
1906
1564
  };
@@ -1909,14 +1567,13 @@ export class AgentWorkspaceProxy {
1909
1567
  const anyPermission = providers.some((p) => p.supportsPermission);
1910
1568
  this.input.send(createEnvelope({
1911
1569
  type: "agent.v2.capabilities",
1912
- hostDeviceId: this.input.hostDeviceId,
1570
+ sessionId: this.input.sessionId,
1913
1571
  payload: {
1914
1572
  enabled: anyEnabled,
1915
1573
  provider: this.input.availableProviders[0] ?? "codex",
1916
1574
  providers,
1917
1575
  protocolVersion: 1,
1918
1576
  workspaceProtocolVersion: 2,
1919
- capabilitiesRevision: ++this.capabilitiesRevision,
1920
1577
  error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
1921
1578
  supportsSessionList: anyEnabled,
1922
1579
  supportsSessionLoad: anyEnabled,
@@ -1936,12 +1593,29 @@ export class AgentWorkspaceProxy {
1936
1593
  provider?: AgentProvider;
1937
1594
  model?: string;
1938
1595
  reasoningEffort?: string;
1939
- serviceTier?: string;
1940
1596
  permissionMode?: AgentPermissionMode;
1941
1597
  collaborationMode?: AgentCollaborationMode;
1942
1598
  title?: string;
1943
1599
  }): Promise<AgentConversation | undefined> {
1944
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
+
1945
1619
  const cwd = payload.cwd ?? this.input.cwd;
1946
1620
  let agentSessionId = payload.agentSessionId;
1947
1621
  let existingConversation =
@@ -1952,43 +1626,18 @@ export class AgentWorkspaceProxy {
1952
1626
  if (payload.conversationId && existingConversation.id !== payload.conversationId) {
1953
1627
  existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
1954
1628
  }
1955
- this.hydrateStoredTimeline(existingConversation);
1956
1629
  this.activeConversationId = existingConversation.id;
1957
- const snapshot = this.latestSnapshot(existingConversation.id);
1958
1630
  this.input.send(createEnvelope({
1959
1631
  type: "agent.v2.conversation.opened",
1960
- hostDeviceId: this.input.hostDeviceId,
1632
+ sessionId: this.input.sessionId,
1961
1633
  payload: {
1962
- conversation: this.conversationSnapshot(existingConversation),
1963
- snapshot: snapshot.items,
1964
- revision: this.getRevision(existingConversation.id),
1965
- cursor: snapshot.cursor,
1966
- hasMore: snapshot.hasMore,
1967
- source: "device-history",
1968
- canonical: true,
1634
+ conversation: existingConversation,
1635
+ snapshot: this.timelines.get(existingConversation.id) ?? [],
1969
1636
  },
1970
1637
  }));
1971
1638
  return existingConversation;
1972
1639
  }
1973
1640
 
1974
- if (!provider) {
1975
- return this.openFailure(payload, "没有可用的 Agent provider。");
1976
- }
1977
- if (!this.input.availableProviders.includes(provider)) {
1978
- return this.openFailure(
1979
- payload,
1980
- `${providerLabel(provider)} 未安装或不可用。`,
1981
- );
1982
- }
1983
-
1984
- const client = await this.ensureProviderClient(provider);
1985
- if (!client) {
1986
- return this.openFailure(
1987
- payload,
1988
- `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`,
1989
- );
1990
- }
1991
-
1992
1641
  try {
1993
1642
  const result = agentSessionId
1994
1643
  ? await client.loadSession({ sessionId: agentSessionId, cwd })
@@ -2005,16 +1654,10 @@ export class AgentWorkspaceProxy {
2005
1654
  title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
2006
1655
  model: payload.model ?? existingConversation?.model,
2007
1656
  reasoningEffort: payload.reasoningEffort ?? existingConversation?.reasoningEffort,
2008
- serviceTier: payload.serviceTier ?? existingConversation?.serviceTier,
2009
1657
  permissionMode: payload.permissionMode ?? existingConversation?.permissionMode,
2010
1658
  collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
2011
1659
  status: "idle",
2012
1660
  archived: existingConversation?.archived ?? false,
2013
- timelineRevision: existingConversation?.timelineRevision ?? this.getRevision(conversationId),
2014
- historyComplete: existingConversation?.historyComplete ?? false,
2015
- runningTurnId: this.currentTurnIds.get(conversationId),
2016
- source: "device",
2017
- canonical: true,
2018
1661
  lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
2019
1662
  lastActivityAt: now,
2020
1663
  createdAt: existingConversation?.createdAt ?? now,
@@ -2023,20 +1666,10 @@ export class AgentWorkspaceProxy {
2023
1666
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
2024
1667
  this.activeConversationId = conversation.id;
2025
1668
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
2026
- this.hydrateStoredTimeline(conversation);
2027
- const snapshot = this.latestSnapshot(conversation.id);
2028
1669
  this.input.send(createEnvelope({
2029
1670
  type: "agent.v2.conversation.opened",
2030
- hostDeviceId: this.input.hostDeviceId,
2031
- payload: {
2032
- conversation: this.conversationSnapshot(conversation),
2033
- snapshot: snapshot.items,
2034
- revision: this.getRevision(conversation.id),
2035
- cursor: snapshot.cursor,
2036
- hasMore: snapshot.hasMore,
2037
- source: "device-history",
2038
- canonical: true,
2039
- },
1671
+ sessionId: this.input.sessionId,
1672
+ payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
2040
1673
  }));
2041
1674
  return conversation;
2042
1675
  } catch (error) {
@@ -2072,10 +1705,6 @@ export class AgentWorkspaceProxy {
2072
1705
  collaborationMode: payload.collaborationMode,
2073
1706
  status: "error",
2074
1707
  archived: false,
2075
- timelineRevision: this.getRevision(fallbackId),
2076
- historyComplete: false,
2077
- source: "device",
2078
- canonical: true,
2079
1708
  lastMessagePreview: message,
2080
1709
  lastActivityAt: now,
2081
1710
  createdAt: now,
@@ -2091,15 +1720,8 @@ export class AgentWorkspaceProxy {
2091
1720
  });
2092
1721
  this.input.send(createEnvelope({
2093
1722
  type: "agent.v2.conversation.opened",
2094
- hostDeviceId: this.input.hostDeviceId,
2095
- payload: {
2096
- conversation: this.conversationSnapshot(conversation),
2097
- snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []),
2098
- revision: this.getRevision(conversation.id),
2099
- hasMore: false,
2100
- source: "device",
2101
- canonical: true,
2102
- },
1723
+ sessionId: this.input.sessionId,
1724
+ payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
2103
1725
  }));
2104
1726
  return conversation;
2105
1727
  }
@@ -2108,11 +1730,10 @@ export class AgentWorkspaceProxy {
2108
1730
  conversationId: string;
2109
1731
  clientMessageId: string;
2110
1732
  contentBlocks: AgentContentBlock[];
2111
- model?: string | null;
2112
- reasoningEffort?: string | null;
2113
- serviceTier?: string | null;
2114
- permissionMode?: AgentPermissionMode | null;
2115
- collaborationMode?: AgentCollaborationMode | null;
1733
+ model?: string;
1734
+ reasoningEffort?: string;
1735
+ permissionMode?: AgentPermissionMode;
1736
+ collaborationMode?: AgentCollaborationMode;
2116
1737
  }): Promise<void> {
2117
1738
  const conversation =
2118
1739
  this.conversations.get(payload.conversationId) ??
@@ -2155,21 +1776,10 @@ export class AgentWorkspaceProxy {
2155
1776
  return;
2156
1777
  }
2157
1778
 
2158
- if (Object.prototype.hasOwnProperty.call(payload, "model")) {
2159
- conversation.model = payload.model ?? undefined;
2160
- }
2161
- if (Object.prototype.hasOwnProperty.call(payload, "reasoningEffort")) {
2162
- conversation.reasoningEffort = payload.reasoningEffort ?? undefined;
2163
- }
2164
- if (Object.prototype.hasOwnProperty.call(payload, "serviceTier")) {
2165
- conversation.serviceTier = payload.serviceTier ?? undefined;
2166
- }
2167
- if (Object.prototype.hasOwnProperty.call(payload, "permissionMode")) {
2168
- conversation.permissionMode = payload.permissionMode ?? undefined;
2169
- }
2170
- if (Object.prototype.hasOwnProperty.call(payload, "collaborationMode")) {
2171
- conversation.collaborationMode = payload.collaborationMode ?? "default";
2172
- }
1779
+ conversation.model = payload.model ?? conversation.model;
1780
+ conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
1781
+ conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
1782
+ conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
2173
1783
  conversation.status = "running";
2174
1784
  conversation.lastActivityAt = Date.now();
2175
1785
  this.activeConversationId = conversation.id;
@@ -2191,11 +1801,10 @@ export class AgentWorkspaceProxy {
2191
1801
  sessionId: conversation.agentSessionId,
2192
1802
  content: payload.contentBlocks,
2193
1803
  clientMessageId: payload.clientMessageId,
2194
- model: conversation.model,
2195
- reasoningEffort: conversation.reasoningEffort,
2196
- serviceTier: conversation.serviceTier,
2197
- permissionMode: conversation.permissionMode,
2198
- collaborationMode: conversation.collaborationMode,
1804
+ model: payload.model,
1805
+ reasoningEffort: payload.reasoningEffort,
1806
+ permissionMode: payload.permissionMode,
1807
+ collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
2199
1808
  cwd: conversation.cwd,
2200
1809
  });
2201
1810
  const nextAgentSessionId = this.extractSessionId(result);
@@ -2288,7 +1897,6 @@ export class AgentWorkspaceProxy {
2288
1897
  contentBlocks: [{ type: "text", text: rawText }],
2289
1898
  model: conversation.model,
2290
1899
  reasoningEffort: conversation.reasoningEffort,
2291
- serviceTier: conversation.serviceTier,
2292
1900
  permissionMode: conversation.permissionMode,
2293
1901
  collaborationMode: conversation.collaborationMode,
2294
1902
  });
@@ -2332,8 +1940,14 @@ export class AgentWorkspaceProxy {
2332
1940
  return;
2333
1941
  }
2334
1942
 
2335
- if (isGitNativeCommand(command.name)) {
2336
- await this.executeGitNativeCommand(conversation, command.name, args);
1943
+ if (conversation.provider !== "codex") {
1944
+ this.addItem(conversation.id, {
1945
+ id: id("error"),
1946
+ conversationId: conversation.id,
1947
+ type: "error",
1948
+ error: `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`,
1949
+ createdAt: now,
1950
+ });
2337
1951
  return;
2338
1952
  }
2339
1953
 
@@ -2350,23 +1964,6 @@ export class AgentWorkspaceProxy {
2350
1964
  return;
2351
1965
  }
2352
1966
 
2353
- if (command.name === "review" || command.name === "subagents") {
2354
- const prompt = command.name === "review"
2355
- ? args || "Review the current local changes."
2356
- : args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
2357
- await this.sendPrompt({
2358
- conversationId: conversation.id,
2359
- clientMessageId: id(command.name),
2360
- contentBlocks: [{ type: "text", text: prompt }],
2361
- model: conversation.model,
2362
- reasoningEffort: conversation.reasoningEffort,
2363
- serviceTier: conversation.serviceTier,
2364
- permissionMode: conversation.permissionMode,
2365
- collaborationMode: conversation.collaborationMode,
2366
- });
2367
- return;
2368
- }
2369
-
2370
1967
  if (command.name === "compact") {
2371
1968
  if (!(client instanceof AcpClient)) throw new Error("当前 Codex runtime 不支持原生 compact。");
2372
1969
  conversation.status = "running";
@@ -2387,7 +1984,7 @@ export class AgentWorkspaceProxy {
2387
1984
  return;
2388
1985
  }
2389
1986
 
2390
- if (command.name === "clear" || command.name === "new") {
1987
+ if (command.name === "clear") {
2391
1988
  if (!client) throw new Error("Agent provider 不在线。");
2392
1989
  const result = await client.newSession({ cwd: conversation.cwd });
2393
1990
  const nextAgentSessionId = this.extractSessionId(result) ?? id("agent-session");
@@ -2404,6 +2001,22 @@ export class AgentWorkspaceProxy {
2404
2001
  return;
2405
2002
  }
2406
2003
 
2004
+ if (command.name === "review" || command.name === "subagents") {
2005
+ const prompt = command.name === "review"
2006
+ ? args || "Review the current local changes."
2007
+ : args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
2008
+ await this.sendPrompt({
2009
+ conversationId: conversation.id,
2010
+ clientMessageId: id(command.name),
2011
+ contentBlocks: [{ type: "text", text: prompt }],
2012
+ model: conversation.model,
2013
+ reasoningEffort: conversation.reasoningEffort,
2014
+ permissionMode: conversation.permissionMode,
2015
+ collaborationMode: conversation.collaborationMode,
2016
+ });
2017
+ return;
2018
+ }
2019
+
2407
2020
  throw new Error(`命令暂未实现:/${command.name}`);
2408
2021
  } catch (error) {
2409
2022
  const message = error instanceof Error ? error.message : String(error);
@@ -2418,46 +2031,6 @@ export class AgentWorkspaceProxy {
2418
2031
  }
2419
2032
  }
2420
2033
 
2421
- private async executeGitNativeCommand(
2422
- conversation: AgentConversation,
2423
- commandName: string,
2424
- args?: string,
2425
- ): Promise<void> {
2426
- const git = gitCommandArgs(commandName, args);
2427
- const toolId = id(commandName);
2428
- const now = Date.now();
2429
- conversation.status = "running";
2430
- conversation.lastMessagePreview = git.display;
2431
- conversation.lastActivityAt = now;
2432
- this.emitConversation(conversation);
2433
- this.upsertTool(conversation.id, {
2434
- id: toolId,
2435
- name: "命令",
2436
- input: `${git.display}\n\ncwd: ${conversation.cwd}`,
2437
- createdAt: now,
2438
- status: "running",
2439
- });
2440
- const result = await runProcess("git", git.argv, {
2441
- cwd: conversation.cwd,
2442
- maxBytes: COMMAND_OUTPUT_MAX_BYTES,
2443
- });
2444
- const ok = result.exitCode === 0;
2445
- this.upsertTool(conversation.id, {
2446
- id: toolId,
2447
- name: "命令",
2448
- input: `${git.display}\n\ncwd: ${conversation.cwd}`,
2449
- output: result.output || (ok ? "完成" : `退出码 ${result.exitCode ?? "unknown"}`),
2450
- createdAt: now,
2451
- status: ok ? "completed" : "failed",
2452
- });
2453
- conversation.status = ok ? "idle" : "error";
2454
- conversation.lastMessagePreview = ok
2455
- ? `${git.display} 完成`
2456
- : `${git.display} 失败`;
2457
- conversation.lastActivityAt = Date.now();
2458
- this.emitConversation(conversation);
2459
- }
2460
-
2461
2034
  private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
2462
2035
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
2463
2036
  return this.handleStructuredInput(params, true);
@@ -2497,7 +2070,6 @@ export class AgentWorkspaceProxy {
2497
2070
  if (
2498
2071
  method.startsWith("account/") ||
2499
2072
  method.startsWith("mcpServer/startupStatus/") ||
2500
- method.startsWith("remoteControl/") ||
2501
2073
  method === "thread/status/changed" ||
2502
2074
  method === "thread/tokenUsage/updated" ||
2503
2075
  method === "serverRequest/resolved" ||
@@ -2525,9 +2097,6 @@ export class AgentWorkspaceProxy {
2525
2097
  this.conversationByAgentSessionId.set(agentSessionId, conversationId);
2526
2098
  const conversation = this.conversations.get(conversationId);
2527
2099
  if (conversation) {
2528
- if (conversation.agentSessionId && conversation.agentSessionId !== agentSessionId) {
2529
- this.conversationByAgentSessionId.delete(conversation.agentSessionId);
2530
- }
2531
2100
  conversation.agentSessionId = agentSessionId;
2532
2101
  conversation.lastActivityAt = Date.now();
2533
2102
  this.emitConversation(conversation);
@@ -3109,7 +2678,7 @@ export class AgentWorkspaceProxy {
3109
2678
  this.upsertItem(conversationId, item);
3110
2679
  this.input.send(createEnvelope({
3111
2680
  type: "agent.v2.permission.request",
3112
- hostDeviceId: this.input.hostDeviceId,
2681
+ sessionId: this.input.sessionId,
3113
2682
  payload: { conversationId, ...permission, item },
3114
2683
  }));
3115
2684
 
@@ -3120,14 +2689,6 @@ export class AgentWorkspaceProxy {
3120
2689
  this.permissionWaiters.delete(requestId);
3121
2690
  this.permissionSources.delete(requestId);
3122
2691
  resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
3123
- this.markPermission(conversationId, requestId, {
3124
- permissionOutcome: "cancelled",
3125
- optionId: "cancelled",
3126
- permissionLive: false,
3127
- permissionPending: false,
3128
- permissionExpired: true,
3129
- permissionError: "等待授权超时",
3130
- });
3131
2692
  this.updateConversationStatus(conversationId, "idle");
3132
2693
  }, PERMISSION_TIMEOUT_MS);
3133
2694
  this.permissionWaiters.set(requestId, { resolve, timer });
@@ -3167,8 +2728,6 @@ export class AgentWorkspaceProxy {
3167
2728
  this.markPermission(payload.conversationId, payload.requestId, {
3168
2729
  permissionOutcome: payload.outcome,
3169
2730
  optionId: selectedOptionId,
3170
- permissionLive: false,
3171
- permissionExpired: false,
3172
2731
  permissionError: undefined,
3173
2732
  permissionPending: false,
3174
2733
  });
@@ -3229,228 +2788,6 @@ export class AgentWorkspaceProxy {
3229
2788
  });
3230
2789
  }
3231
2790
 
3232
- private getRevision(conversationId: string): number {
3233
- return this.conversationRevisions.get(conversationId) ??
3234
- this.conversations.get(conversationId)?.timelineRevision ??
3235
- 0;
3236
- }
3237
-
3238
- private setRevisionFloor(conversationId: string, revision: number): number {
3239
- const nextRevision = Math.max(this.getRevision(conversationId), revision);
3240
- this.conversationRevisions.set(conversationId, nextRevision);
3241
- const conversation = this.conversations.get(conversationId);
3242
- if (conversation) {
3243
- conversation.timelineRevision = nextRevision;
3244
- conversation.runningTurnId = this.currentTurnIds.get(conversationId);
3245
- }
3246
- return nextRevision;
3247
- }
3248
-
3249
- private conversationSnapshot(conversation: AgentConversation): AgentConversation {
3250
- return {
3251
- ...conversation,
3252
- timelineRevision: this.getRevision(conversation.id),
3253
- historyComplete: conversation.historyComplete ?? false,
3254
- runningTurnId: this.currentTurnIds.get(conversation.id),
3255
- source: conversation.source ?? "device",
3256
- canonical: conversation.canonical ?? true,
3257
- };
3258
- }
3259
-
3260
- private annotateTimelineItem(
3261
- item: AgentTimelineItem,
3262
- revision: number | undefined,
3263
- source: AgentSyncSource,
3264
- ): AgentTimelineItem {
3265
- return {
3266
- ...item,
3267
- revision: revision ?? item.revision,
3268
- source: item.source ?? source,
3269
- canonical: item.canonical ?? true,
3270
- };
3271
- }
3272
-
3273
- private recordRevisionEvent(
3274
- conversationId: string,
3275
- change: { item?: AgentTimelineItem; conversation?: AgentConversation },
3276
- ): AgentRevisionEvent {
3277
- const revision = this.getRevision(conversationId) + 1;
3278
- this.conversationRevisions.set(conversationId, revision);
3279
- const conversation = this.conversations.get(conversationId);
3280
- if (conversation) {
3281
- conversation.timelineRevision = revision;
3282
- conversation.runningTurnId = this.currentTurnIds.get(conversationId);
3283
- }
3284
- const event: AgentRevisionEvent = {
3285
- revision,
3286
- item: change.item ? this.annotateTimelineItem(change.item, revision, "device-live") : undefined,
3287
- conversation: change.conversation ? this.conversationSnapshot(change.conversation) : undefined,
3288
- };
3289
- const events = this.revisionEvents.get(conversationId) ?? [];
3290
- events.push(event);
3291
- if (events.length > MAX_DELTA_EVENTS) {
3292
- events.splice(0, events.length - MAX_DELTA_EVENTS);
3293
- }
3294
- this.revisionEvents.set(conversationId, events);
3295
- return event;
3296
- }
3297
-
3298
- private storedTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): AgentTimelineItem[] {
3299
- if (!conversation.agentSessionId) return [];
3300
- const result = conversation.provider === "codex"
3301
- ? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd, { maxItems })
3302
- : conversation.provider === "claude"
3303
- ? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id, { maxItems })
3304
- : { items: [] };
3305
- return result.items
3306
- .sort((a, b) => a.createdAt - b.createdAt)
3307
- .map((item, index) => this.annotateTimelineItem(item as AgentTimelineItem, index + 1, "device-history"));
3308
- }
3309
-
3310
- private canonicalTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): AgentTimelineItem[] {
3311
- const merged = new Map<string, AgentTimelineItem>();
3312
- for (const item of this.storedTimeline(conversation, maxItems)) {
3313
- merged.set(item.id, item);
3314
- }
3315
- for (const item of this.timelines.get(conversation.id) ?? []) {
3316
- merged.set(item.id, this.annotateTimelineItem(item, item.revision, item.source ?? "device-live"));
3317
- }
3318
- const items = [...merged.values()]
3319
- .sort((a, b) => a.createdAt - b.createdAt)
3320
- .slice(-maxItems);
3321
- this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
3322
- return items;
3323
- }
3324
-
3325
- private async appServerTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): Promise<AgentTimelineItem[]> {
3326
- if (conversation.provider !== "codex" || !conversation.agentSessionId) return [];
3327
- if (this.protocolForProvider(conversation.provider) !== "codex-app-server") return [];
3328
- const client = this.clientForProvider(conversation.provider);
3329
- const listTurns = (client as { listTurns?: (input: { sessionId: string; limit?: number }) => Promise<unknown> } | undefined)?.listTurns;
3330
- if (typeof listTurns !== "function") return [];
3331
- try {
3332
- const result = await listTurns.call(client, { sessionId: conversation.agentSessionId, limit: maxItems });
3333
- return this.timelineFromAppServerTurns(conversation.id, conversation.agentSessionId, result);
3334
- } catch (error) {
3335
- if (this.input.verbose) {
3336
- process.stderr.write(`[agent:v2] thread/turns/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
3337
- }
3338
- return [];
3339
- }
3340
- }
3341
-
3342
- private timelineFromAppServerTurns(
3343
- conversationId: string,
3344
- agentSessionId: string,
3345
- value: unknown,
3346
- ): AgentTimelineItem[] {
3347
- const raw = asRecord(value);
3348
- const turns =
3349
- Array.isArray(value) ? value :
3350
- Array.isArray(raw?.turns) ? raw.turns :
3351
- Array.isArray(raw?.items) ? raw.items :
3352
- Array.isArray(raw?.entries) ? raw.entries :
3353
- [];
3354
- const items: AgentTimelineItem[] = [];
3355
- turns.forEach((entry, index) => {
3356
- const turn = asRecord(entry);
3357
- if (!turn) return;
3358
- const turnId = firstString(turn, ["id", "turnId", "turn_id"]) ?? `turn-${index + 1}`;
3359
- const createdAt = parseTimestamp(turn.createdAt ?? turn.created_at ?? turn.startedAt ?? turn.started_at) ?? Date.now() + index;
3360
- const updatedAt = parseTimestamp(turn.updatedAt ?? turn.updated_at ?? turn.completedAt ?? turn.completed_at);
3361
- const userText = appServerText(turn.input ?? turn.prompt ?? turn.user ?? turn.userMessage ?? turn.request);
3362
- if (userText) {
3363
- items.push({
3364
- id: `app-server:${agentSessionId}:${turnId}:user`,
3365
- conversationId,
3366
- type: "message",
3367
- kind: "chat",
3368
- turnId,
3369
- role: "user",
3370
- content: [{ type: "text", text: userText }],
3371
- text: userText,
3372
- createdAt,
3373
- updatedAt,
3374
- metadata: { source: "app-server", provider: "codex" },
3375
- });
3376
- }
3377
- const assistantText = appServerText(turn.output ?? turn.response ?? turn.assistant ?? turn.assistantMessage ?? turn.result);
3378
- if (assistantText) {
3379
- items.push({
3380
- id: `app-server:${agentSessionId}:${turnId}:assistant`,
3381
- conversationId,
3382
- type: "message",
3383
- kind: "chat",
3384
- turnId,
3385
- role: "assistant",
3386
- content: [{ type: "text", text: assistantText }],
3387
- text: assistantText,
3388
- createdAt: updatedAt ?? createdAt + 1,
3389
- updatedAt,
3390
- metadata: { source: "app-server", provider: "codex" },
3391
- });
3392
- }
3393
- const nestedItems = Array.isArray(turn.items) ? turn.items : Array.isArray(turn.messages) ? turn.messages : [];
3394
- for (const nested of nestedItems) {
3395
- const nestedRecord = asRecord(nested);
3396
- if (!nestedRecord) continue;
3397
- const text = appServerText(nestedRecord.content ?? nestedRecord.text ?? nestedRecord.message);
3398
- const role = nestedRecord.role === "user" || nestedRecord.role === "assistant" || nestedRecord.role === "system"
3399
- ? nestedRecord.role
3400
- : undefined;
3401
- if (!text || !role) continue;
3402
- const itemId = firstString(nestedRecord, ["id", "itemId", "messageId"]) ?? `${role}-${items.length + 1}`;
3403
- items.push({
3404
- id: `app-server:${agentSessionId}:${turnId}:${itemId}`,
3405
- conversationId,
3406
- type: "message",
3407
- kind: "chat",
3408
- turnId,
3409
- itemId,
3410
- role,
3411
- content: [{ type: "text", text }],
3412
- text,
3413
- createdAt: parseTimestamp(nestedRecord.createdAt ?? nestedRecord.created_at) ?? createdAt + items.length,
3414
- updatedAt: parseTimestamp(nestedRecord.updatedAt ?? nestedRecord.updated_at),
3415
- metadata: { source: "app-server", provider: "codex" },
3416
- });
3417
- }
3418
- });
3419
- return items
3420
- .sort((a, b) => a.createdAt - b.createdAt)
3421
- .map((item, index) => this.annotateTimelineItem(item, index + 1, "app-server"));
3422
- }
3423
-
3424
- private mergeCanonicalTimelineItems(items: AgentTimelineItem[], maxItems: number): AgentTimelineItem[] {
3425
- const byKey = new Map<string, AgentTimelineItem>();
3426
- for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
3427
- const key = item.type === "message" && item.role && item.text
3428
- ? `${item.role}:${item.text.replace(/\s+/g, " ").trim().slice(0, 500)}`
3429
- : item.id;
3430
- const existing = byKey.get(key);
3431
- if (!existing || item.source === "app-server" || (item.updatedAt ?? item.createdAt) >= (existing.updatedAt ?? existing.createdAt)) {
3432
- byKey.set(key, item);
3433
- }
3434
- }
3435
- return [...byKey.values()]
3436
- .sort((a, b) => a.createdAt - b.createdAt)
3437
- .slice(-maxItems);
3438
- }
3439
-
3440
- private latestSnapshot(conversationId: string): {
3441
- items: AgentTimelineItem[];
3442
- cursor?: string;
3443
- hasMore: boolean;
3444
- } {
3445
- const timeline = this.timelines.get(conversationId) ?? [];
3446
- const start = Math.max(0, timeline.length - MAX_SNAPSHOT_ITEMS);
3447
- return {
3448
- items: timeline.slice(start).map((item) => snapshotTimelineItem(item)),
3449
- cursor: start > 0 ? String(start) : undefined,
3450
- hasMore: start > 0,
3451
- };
3452
- }
3453
-
3454
2791
  private addItem(conversationId: string, item: AgentTimelineItem): void {
3455
2792
  this.rememberItemConversationId(conversationId, item);
3456
2793
  const timeline = this.timelines.get(conversationId) ?? [];
@@ -3488,8 +2825,6 @@ export class AgentWorkspaceProxy {
3488
2825
  ...existing,
3489
2826
  ...toolCall,
3490
2827
  id: targetToolId,
3491
- input: toolCall.input ?? existing?.input,
3492
- output: toolCall.output ?? existing?.output,
3493
2828
  createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
3494
2829
  };
3495
2830
  this.toolConversationIds.set(toolCall.id, conversationId);
@@ -3568,7 +2903,6 @@ export class AgentWorkspaceProxy {
3568
2903
  conversation.title = conversation.title ?? target.title;
3569
2904
  conversation.model = conversation.model ?? target.model;
3570
2905
  conversation.reasoningEffort = conversation.reasoningEffort ?? target.reasoningEffort;
3571
- conversation.serviceTier = conversation.serviceTier ?? target.serviceTier;
3572
2906
  conversation.permissionMode = conversation.permissionMode ?? target.permissionMode;
3573
2907
  conversation.lastMessagePreview = conversation.lastMessagePreview ?? target.lastMessagePreview;
3574
2908
  conversation.createdAt = Math.min(conversation.createdAt, target.createdAt);
@@ -3585,17 +2919,6 @@ export class AgentWorkspaceProxy {
3585
2919
  .sort((a, b) => a.createdAt - b.createdAt)
3586
2920
  .slice(-MAX_TIMELINE_ITEMS),
3587
2921
  );
3588
- const oldRevision = this.conversationRevisions.get(oldId) ?? 0;
3589
- if (oldRevision > 0) {
3590
- this.conversationRevisions.delete(oldId);
3591
- this.setRevisionFloor(newId, oldRevision);
3592
- }
3593
- const oldEvents = this.revisionEvents.get(oldId);
3594
- if (oldEvents) {
3595
- this.revisionEvents.delete(oldId);
3596
- const existingEvents = this.revisionEvents.get(newId) ?? [];
3597
- this.revisionEvents.set(newId, [...existingEvents, ...oldEvents].slice(-MAX_DELTA_EVENTS));
3598
- }
3599
2922
 
3600
2923
  for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
3601
2924
  if (conversationId === oldId) {
@@ -3631,48 +2954,18 @@ export class AgentWorkspaceProxy {
3631
2954
 
3632
2955
  private emitItem(conversationId: string, item: AgentTimelineItem): void {
3633
2956
  const conversation = this.conversations.get(conversationId);
3634
- const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
3635
- const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
3636
2957
  this.input.send(createEnvelope({
3637
2958
  type: "agent.v2.event",
3638
- hostDeviceId: this.input.hostDeviceId,
3639
- payload: {
3640
- conversationId,
3641
- conversation: event.conversation,
3642
- item: event.item,
3643
- revision: event.revision,
3644
- source: "device-live",
3645
- canonical: true,
3646
- },
2959
+ sessionId: this.input.sessionId,
2960
+ payload: { conversationId, conversation, item },
3647
2961
  }));
3648
2962
  }
3649
2963
 
3650
2964
  private emitConversation(conversation: AgentConversation): void {
3651
- const event = this.recordRevisionEvent(conversation.id, { conversation });
3652
2965
  this.input.send(createEnvelope({
3653
2966
  type: "agent.v2.event",
3654
- hostDeviceId: this.input.hostDeviceId,
3655
- payload: {
3656
- conversationId: conversation.id,
3657
- conversation: event.conversation,
3658
- revision: event.revision,
3659
- source: "device-live",
3660
- canonical: true,
3661
- },
3662
- }));
3663
- this.input.send(createEnvelope({
3664
- type: "agent.v2.running_state",
3665
- hostDeviceId: this.input.hostDeviceId,
3666
- payload: {
3667
- conversationId: conversation.id,
3668
- status: conversation.status,
3669
- runningTurnId: this.currentTurnIds.get(conversation.id),
3670
- revision: event.revision,
3671
- error: conversation.status === "error" ? conversation.lastMessagePreview : undefined,
3672
- updatedAt: conversation.lastActivityAt,
3673
- source: "device-live",
3674
- canonical: true,
3675
- },
2967
+ sessionId: this.input.sessionId,
2968
+ payload: { conversationId: conversation.id, conversation },
3676
2969
  }));
3677
2970
  }
3678
2971
 
@@ -3714,186 +3007,21 @@ export class AgentWorkspaceProxy {
3714
3007
  }
3715
3008
 
3716
3009
  private sendSnapshot(conversationId?: string): void {
3717
- if (conversationId) {
3718
- const conversation = this.conversations.get(conversationId);
3719
- if (conversation) this.hydrateStoredTimeline(conversation);
3720
- } else if (this.activeConversationId) {
3721
- const conversation = this.conversations.get(this.activeConversationId);
3722
- if (conversation) this.hydrateStoredTimeline(conversation);
3723
- }
3724
- const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
3725
- const snapshot = conversationId ? this.latestSnapshot(conversationId) : { items: [], cursor: undefined, hasMore: false };
3010
+ const conversations = [...this.conversations.values()];
3011
+ const items = conversationId
3012
+ ? this.timelines.get(conversationId) ?? []
3013
+ : [...this.timelines.values()].flat();
3726
3014
  this.input.send(createEnvelope({
3727
3015
  type: "agent.v2.snapshot",
3728
- hostDeviceId: this.input.hostDeviceId,
3016
+ sessionId: this.input.sessionId,
3729
3017
  payload: {
3730
3018
  conversations,
3731
3019
  activeConversationId: this.activeConversationId,
3732
- items: snapshot.items,
3733
- revision: conversationId ? this.getRevision(conversationId) : undefined,
3734
- cursor: snapshot.cursor,
3735
- hasMore: snapshot.hasMore,
3736
- source: "device-history",
3737
- canonical: true,
3738
- },
3739
- }));
3740
- }
3741
-
3742
- private async sendHistoryPage(payload: {
3743
- conversationId: string;
3744
- cursor?: string;
3745
- limit?: number;
3746
- direction?: "older" | "newer";
3747
- }): Promise<void> {
3748
- const conversation = this.conversations.get(payload.conversationId);
3749
- if (!conversation) return;
3750
- const limit = Math.min(Math.max(payload.limit ?? MAX_SNAPSHOT_ITEMS, 1), 200);
3751
- const items = this.mergeCanonicalTimelineItems([
3752
- ...this.canonicalTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
3753
- ...await this.appServerTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
3754
- ], HISTORY_PAGE_MAX_ITEMS);
3755
- this.timelines.set(
3756
- conversation.id,
3757
- items.slice(-MAX_TIMELINE_ITEMS).map((item) => this.annotateTimelineItem(item, item.revision, item.source ?? "device-history")),
3758
- );
3759
- for (const item of this.timelines.get(conversation.id) ?? []) {
3760
- this.rememberItemConversationId(conversation.id, item);
3761
- }
3762
-
3763
- const direction = payload.direction ?? "older";
3764
- let page: AgentTimelineItem[];
3765
- let cursor: string | undefined;
3766
- let hasMore = false;
3767
- if (direction === "newer") {
3768
- const start = clampHistoryCursor(payload.cursor, 0, items.length);
3769
- const end = Math.min(items.length, start + limit);
3770
- page = items.slice(start, end);
3771
- hasMore = end < items.length;
3772
- cursor = hasMore ? String(end) : undefined;
3773
- } else {
3774
- const end = clampHistoryCursor(payload.cursor, items.length, items.length);
3775
- const start = Math.max(0, end - limit);
3776
- page = items.slice(start, end);
3777
- hasMore = start > 0;
3778
- cursor = hasMore ? String(start) : undefined;
3779
- }
3780
- conversation.historyComplete = !hasMore;
3781
- const revision = this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
3782
- this.input.send(createEnvelope({
3783
- type: "agent.v2.history.page",
3784
- hostDeviceId: this.input.hostDeviceId,
3785
- payload: {
3786
- conversationId: conversation.id,
3787
- conversation: this.conversationSnapshot(conversation),
3788
- items: page.map((item) => snapshotTimelineItem(item)),
3789
- revision,
3790
- cursor,
3791
- hasMore,
3792
- source: "device-history",
3793
- canonical: true,
3020
+ items,
3794
3021
  },
3795
3022
  }));
3796
3023
  }
3797
3024
 
3798
- private sendDelta(payload: {
3799
- conversationId: string;
3800
- sinceRevision?: number;
3801
- limit?: number;
3802
- }): void {
3803
- const conversation = this.conversations.get(payload.conversationId);
3804
- if (!conversation) return;
3805
- this.hydrateStoredTimeline(conversation);
3806
- const sinceRevision = payload.sinceRevision ?? 0;
3807
- const limit = Math.min(Math.max(payload.limit ?? 100, 1), 500);
3808
- const events = this.revisionEvents.get(conversation.id) ?? [];
3809
- const oldestAvailable = events[0]?.revision ?? this.getRevision(conversation.id);
3810
- const newestRevision = this.getRevision(conversation.id);
3811
- const reset = sinceRevision > 0 &&
3812
- (
3813
- sinceRevision > newestRevision ||
3814
- (
3815
- sinceRevision < newestRevision &&
3816
- (events.length === 0 || sinceRevision < oldestAvailable - 1)
3817
- )
3818
- );
3819
-
3820
- if (reset) {
3821
- const snapshot = this.latestSnapshot(conversation.id);
3822
- this.input.send(createEnvelope({
3823
- type: "agent.v2.delta",
3824
- hostDeviceId: this.input.hostDeviceId,
3825
- payload: {
3826
- conversationId: conversation.id,
3827
- conversation: this.conversationSnapshot(conversation),
3828
- items: snapshot.items,
3829
- sinceRevision,
3830
- revision: newestRevision,
3831
- reset: true,
3832
- cursor: snapshot.cursor,
3833
- hasMore: snapshot.hasMore,
3834
- source: "device-history",
3835
- canonical: true,
3836
- },
3837
- }));
3838
- return;
3839
- }
3840
-
3841
- const changed = events
3842
- .filter((event) => event.revision > sinceRevision)
3843
- .slice(-limit);
3844
- const itemsById = new Map<string, AgentTimelineItem>();
3845
- for (const event of changed) {
3846
- if (event.item) itemsById.set(event.item.id, event.item);
3847
- }
3848
- this.input.send(createEnvelope({
3849
- type: "agent.v2.delta",
3850
- hostDeviceId: this.input.hostDeviceId,
3851
- payload: {
3852
- conversationId: conversation.id,
3853
- conversation: this.conversationSnapshot(conversation),
3854
- items: [...itemsById.values()].map((item) => snapshotTimelineItem(item)),
3855
- sinceRevision,
3856
- revision: newestRevision,
3857
- reset: false,
3858
- hasMore: changed.length === limit && events.some((event) => event.revision > sinceRevision && event.revision < changed[0]!.revision),
3859
- source: "device-live",
3860
- canonical: true,
3861
- },
3862
- }));
3863
- }
3864
-
3865
- private hydrateStoredTimeline(conversation: AgentConversation): void {
3866
- if (!conversation.agentSessionId) return;
3867
- const existing = this.timelines.get(conversation.id) ?? [];
3868
- if (existing.length > 0) {
3869
- this.setRevisionFloor(conversation.id, Math.max(
3870
- existing.length,
3871
- ...existing.map((item) => item.revision ?? 0),
3872
- ));
3873
- return;
3874
- }
3875
- const stored = this.storedTimeline(conversation, MAX_TIMELINE_ITEMS);
3876
- conversation.historyComplete = stored.length <= MAX_SNAPSHOT_ITEMS;
3877
- if (stored.length === 0) {
3878
- this.setRevisionFloor(conversation.id, this.getRevision(conversation.id));
3879
- return;
3880
- }
3881
- const items = stored
3882
- .sort((a, b) => a.createdAt - b.createdAt)
3883
- .slice(-MAX_TIMELINE_ITEMS);
3884
- this.timelines.set(conversation.id, items);
3885
- for (const item of items) this.rememberItemConversationId(conversation.id, item);
3886
- this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
3887
- const lastMessage = [...items].reverse().find((item) => item.text?.trim());
3888
- if (lastMessage?.text && !conversation.lastMessagePreview) {
3889
- conversation.lastMessagePreview = previewText(lastMessage.text);
3890
- }
3891
- const lastActivityAt = items.at(-1)?.createdAt;
3892
- if (lastActivityAt) {
3893
- conversation.lastActivityAt = Math.max(conversation.lastActivityAt, lastActivityAt);
3894
- }
3895
- }
3896
-
3897
3025
  private conversationIdFromParams(params: unknown): string | undefined {
3898
3026
  const raw = asRecord(params);
3899
3027
  if (!raw) return undefined;
@@ -3951,8 +3079,6 @@ export class AgentWorkspaceProxy {
3951
3079
  private rememberTurnConversationId(conversationId: string, turnId: string): void {
3952
3080
  this.currentTurnIds.set(conversationId, turnId);
3953
3081
  this.turnConversationIds.set(turnId, conversationId);
3954
- const conversation = this.conversations.get(conversationId);
3955
- if (conversation) conversation.runningTurnId = turnId;
3956
3082
  }
3957
3083
 
3958
3084
  private forgetCurrentTurn(conversationId: string, turnId?: string): void {
@@ -3960,8 +3086,6 @@ export class AgentWorkspaceProxy {
3960
3086
  this.currentTurnIds.delete(conversationId);
3961
3087
  if (turnId) this.turnConversationIds.delete(turnId);
3962
3088
  if (currentTurnId && currentTurnId !== turnId) this.turnConversationIds.delete(currentTurnId);
3963
- const conversation = this.conversations.get(conversationId);
3964
- if (conversation) conversation.runningTurnId = undefined;
3965
3089
  }
3966
3090
 
3967
3091
  private rememberItemConversationId(conversationId: string, item: AgentTimelineItem): void {
@@ -4003,54 +3127,32 @@ export class AgentWorkspaceProxy {
4003
3127
 
4004
3128
  private cancelPendingPermissions(conversationId?: string): void {
4005
3129
  for (const [requestId, waiter] of this.permissionWaiters) {
4006
- const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
4007
- if (conversationId && ownerConversationId && ownerConversationId !== conversationId) continue;
4008
3130
  clearTimeout(waiter.timer);
4009
3131
  waiter.resolve(formatPermissionResponse(
4010
3132
  this.permissionSources.get(requestId),
4011
3133
  "cancelled",
4012
3134
  "cancelled",
4013
3135
  ));
4014
- if (ownerConversationId) {
4015
- this.markPermission(ownerConversationId, requestId, {
4016
- permissionOutcome: "cancelled",
4017
- optionId: "cancelled",
4018
- permissionLive: false,
4019
- permissionPending: false,
4020
- permissionExpired: false,
4021
- permissionError: "已停止",
4022
- });
4023
- }
4024
- this.permissionWaiters.delete(requestId);
4025
3136
  this.pendingPermissions.delete(requestId);
4026
3137
  this.permissionSources.delete(requestId);
4027
3138
  }
3139
+ this.permissionWaiters.clear();
4028
3140
  for (const [requestId, waiter] of this.structuredInputWaiters) {
4029
- const pending = this.pendingStructuredInputs.get(requestId);
4030
- if (conversationId && pending?.conversationId && pending.conversationId !== conversationId) continue;
4031
3141
  clearTimeout(waiter.timer);
4032
3142
  waiter.resolve(formatStructuredInputResponse({}));
3143
+ const pending = this.pendingStructuredInputs.get(requestId);
4033
3144
  if (pending) {
4034
3145
  this.markStructuredInput(pending.conversationId, requestId, {
4035
3146
  inputPending: false,
4036
3147
  inputError: "已停止",
4037
3148
  });
4038
3149
  }
4039
- this.structuredInputWaiters.delete(requestId);
4040
3150
  this.pendingStructuredInputs.delete(requestId);
4041
3151
  }
3152
+ this.structuredInputWaiters.clear();
4042
3153
  if (conversationId) this.updateConversationStatus(conversationId, "idle");
4043
3154
  }
4044
3155
 
4045
- private conversationIdForPermissionRequest(requestId: string): string | undefined {
4046
- for (const [conversationId, timeline] of this.timelines) {
4047
- if (timeline.some((item) => item.type === "permission" && item.permission?.requestId === requestId)) {
4048
- return conversationId;
4049
- }
4050
- }
4051
- return undefined;
4052
- }
4053
-
4054
3156
  private extractSessionId(value: unknown): string | undefined {
4055
3157
  const raw = asRecord(value);
4056
3158
  if (!raw) return undefined;