linkshell-cli 0.3.28 → 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 (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 +1 -27
  17. package/dist/cli/src/runtime/acp/agent-workspace.js +168 -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 +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/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 +11940 -3423
  42. package/dist/shared-protocol/src/index.js +98 -164
  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 +176 -1094
  52. package/src/runtime/acp/claude-sdk-client.ts +37 -85
  53. package/src/runtime/acp/claude-stream-json-client.ts +41 -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,56 +1,16 @@
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 { createEnvelope, parseTypedPayload, } from "@linkshell/protocol";
6
5
  import { AcpClient } from "./acp-client.js";
7
6
  import { ClaudeSdkClient } from "./claude-sdk-client.js";
8
7
  import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
9
- import { CodexRpcBridge } from "./codex-rpc-bridge.js";
10
- import { listClaudeStoredSessions, loadClaudeStoredTimeline } from "./claude-sessions.js";
11
- import { loadCodexStoredTimeline } from "./codex-sessions.js";
12
8
  import { resolveAgentCommand } from "./provider-resolver.js";
13
9
  const PERMISSION_TIMEOUT_MS = 5 * 60_000;
14
10
  const MAX_TIMELINE_ITEMS = 200;
15
- const MAX_SNAPSHOT_ITEMS = 80;
16
- const MAX_SNAPSHOT_TEXT_BYTES = 128 * 1024;
17
- const MAX_DELTA_EVENTS = 500;
18
- const HISTORY_PAGE_MAX_ITEMS = 500;
19
11
  function id(prefix) {
20
12
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
21
13
  }
22
- function clampHistoryCursor(value, fallback, max) {
23
- if (!value)
24
- return fallback;
25
- const parsed = Number.parseInt(value, 10);
26
- if (!Number.isFinite(parsed))
27
- return fallback;
28
- return Math.max(0, Math.min(max, parsed));
29
- }
30
- function appServerText(value) {
31
- if (typeof value === "string") {
32
- const text = value.trim();
33
- return text || undefined;
34
- }
35
- if (Array.isArray(value)) {
36
- const text = value
37
- .map((part) => appServerText(part))
38
- .filter(Boolean)
39
- .join("\n")
40
- .trim();
41
- return text || undefined;
42
- }
43
- const record = asRecord(value);
44
- if (!record)
45
- return undefined;
46
- if (typeof record.text === "string")
47
- return appServerText(record.text);
48
- if (typeof record.content === "string")
49
- return appServerText(record.content);
50
- if (typeof record.message === "string")
51
- return appServerText(record.message);
52
- return appServerText(record.content ?? record.message ?? record.parts);
53
- }
54
14
  function stringify(value) {
55
15
  if (typeof value === "string")
56
16
  return value;
@@ -61,62 +21,6 @@ function stringify(value) {
61
21
  return String(value);
62
22
  }
63
23
  }
64
- function truncateUtf8(value, maxBytes = MAX_SNAPSHOT_TEXT_BYTES) {
65
- if (!value)
66
- return value;
67
- if (Buffer.byteLength(value, "utf8") <= maxBytes)
68
- return value;
69
- let end = Math.min(value.length, maxBytes);
70
- while (end > 0 && Buffer.byteLength(value.slice(0, end), "utf8") > maxBytes) {
71
- end = Math.floor(end * 0.9);
72
- }
73
- return `${value.slice(0, end)}\n\n[truncated by LinkShell: original ${Buffer.byteLength(value, "utf8")} bytes]`;
74
- }
75
- function snapshotContentBlocks(blocks, options = {}) {
76
- if (!blocks)
77
- return undefined;
78
- return blocks.map((block) => block.type === "image" && options.stripImages !== false
79
- ? { ...block, data: undefined, text: block.text || "图片附件" }
80
- : { ...block, text: truncateUtf8(block.text) });
81
- }
82
- function snapshotTimelineItem(item, options = {}) {
83
- return {
84
- ...item,
85
- content: snapshotContentBlocks(item.content, options),
86
- text: truncateUtf8(item.text),
87
- toolCall: item.toolCall
88
- ? {
89
- ...item.toolCall,
90
- input: truncateUtf8(item.toolCall.input),
91
- output: truncateUtf8(item.toolCall.output),
92
- }
93
- : undefined,
94
- commandExecution: item.commandExecution
95
- ? {
96
- ...item.commandExecution,
97
- command: truncateUtf8(item.commandExecution.command, 16 * 1024),
98
- output: truncateUtf8(item.commandExecution.output),
99
- }
100
- : undefined,
101
- fileChange: item.fileChange
102
- ? {
103
- ...item.fileChange,
104
- diff: truncateUtf8(item.fileChange.diff),
105
- summary: truncateUtf8(item.fileChange.summary),
106
- }
107
- : undefined,
108
- permission: item.permission
109
- ? {
110
- ...item.permission,
111
- toolInput: truncateUtf8(item.permission.toolInput),
112
- context: truncateUtf8(item.permission.context),
113
- }
114
- : undefined,
115
- };
116
- }
117
- function snapshotTimelineItems(items) {
118
- return items.slice(-MAX_SNAPSHOT_ITEMS).map((item) => snapshotTimelineItem(item));
119
- }
120
24
  function asRecord(value) {
121
25
  return typeof value === "object" && value ? value : undefined;
122
26
  }
@@ -422,15 +326,7 @@ function titleFromCwd(cwd) {
422
326
  }
423
327
  function textFromBlocks(blocks) {
424
328
  return blocks
425
- .map((block) => {
426
- if (block.type === "text")
427
- return block.text ?? "";
428
- if (block.type === "image")
429
- return `[${block.mimeType ?? "image"} attachment]`;
430
- if (block.type === "file" && block.path)
431
- return `@${block.path}`;
432
- return "";
433
- })
329
+ .map((block) => block.type === "text" ? block.text ?? "" : `[${block.mimeType ?? "image"} attachment]`)
434
330
  .filter(Boolean)
435
331
  .join("\n");
436
332
  }
@@ -463,11 +359,6 @@ function contentBlocksFromValue(value) {
463
359
  const text = firstString(raw, ["text", "content", "message"]);
464
360
  return text ? [{ type: "text", text }] : [];
465
361
  }
466
- if (normalizedType === "file" || normalizedType === "inputfile" || normalizedType === "filereference") {
467
- const path = firstString(raw, ["path", "file", "filePath", "file_path", "absolutePath", "relativePath"]);
468
- const text = firstString(raw, ["text", "name", "label"]);
469
- return path ? [{ type: "file", path, name: text, text: text ?? `@${path}` }] : [];
470
- }
471
362
  const nested = raw.content ?? raw.contentItems ?? raw.parts;
472
363
  if (Array.isArray(nested))
473
364
  return contentBlocksFromValue(nested);
@@ -663,28 +554,8 @@ function providerLabel(provider) {
663
554
  return "Custom";
664
555
  }
665
556
  const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"];
666
- const CLAUDE_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
667
- const ALL_SERVICE_TIERS = ["standard", "fast"];
668
- const AGENT_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"];
669
- const COMMAND_OUTPUT_MAX_BYTES = 96 * 1024;
670
- const LINKSHELL_NATIVE_COMMANDS = [
671
- { name: "status", description: "Show current Agent and workspace status", category: "LinkShell", argsMode: "none" },
672
- { name: "plan", description: "Enter Plan mode for the next turn", category: "Agent", argsMode: "none" },
673
- { name: "exit-plan", description: "Exit Plan mode", category: "Agent", argsMode: "none" },
674
- { name: "review", description: "Ask the Agent to review current local changes", category: "Agent", argsMode: "optional" },
675
- { name: "subagents", description: "Ask the Agent to split work across subagents when useful", category: "Agent", argsMode: "optional" },
676
- { name: "compact", description: "Compact the active Codex context", category: "Codex", argsMode: "none", providers: ["codex"] },
677
- { name: "new", description: "Start a fresh Codex thread", category: "Codex", argsMode: "none", providers: ["codex"], destructive: true },
678
- { name: "clear", description: "Start a fresh Agent context for this conversation", category: "Agent", argsMode: "none", destructive: true },
679
- { name: "git-status", description: "Show branch and working tree status", category: "Git", argsMode: "none" },
680
- { name: "git-diff", description: "Show a compact diffstat for current changes", category: "Git", argsMode: "none" },
681
- { name: "git-commit", description: "Commit staged changes with the given message", category: "Git", argsMode: "required" },
682
- { name: "git-pull", description: "Pull with fast-forward only", category: "Git", argsMode: "none" },
683
- { name: "git-push", description: "Push the current branch", category: "Git", argsMode: "none" },
684
- { name: "git-stash", description: "Stash current working tree changes", category: "Git", argsMode: "optional" },
685
- { name: "git-stash-pop", description: "Pop the latest stash", category: "Git", argsMode: "none" },
686
- ];
687
- const CODEX_JSON_RPC_COMMANDS = new Set(["plan", "exit-plan", "review", "compact", "new"]);
557
+ const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"];
558
+ const CODEX_COMMAND_NAMES = ["plan", "exit-plan", "compact", "clear", "status", "review", "subagents"];
688
559
  const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
689
560
  "add-dir",
690
561
  "agents",
@@ -946,31 +817,57 @@ function customClaudeCommands(cwd) {
946
817
  }
947
818
  function defaultProviderCommands(provider, cwd, enabled) {
948
819
  const disabledReason = enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`;
949
- const linkshellCommands = LINKSHELL_NATIVE_COMMANDS
950
- .filter((command) => provider !== "codex" || CODEX_JSON_RPC_COMMANDS.has(command.name))
951
- .filter((command) => !command.providers || command.providers.includes(provider))
952
- .map((command) => makeCommand({
953
- provider,
954
- name: command.name,
955
- description: command.description,
956
- source: "linkshell",
957
- category: command.category,
958
- argsMode: command.argsMode ?? "optional",
959
- destructive: command.destructive,
960
- disabledReason,
961
- executionKind: "native",
962
- }));
963
820
  if (provider === "codex") {
964
- return linkshellCommands;
821
+ return CODEX_COMMAND_NAMES.map((name) => makeCommand({
822
+ provider,
823
+ name,
824
+ source: "linkshell",
825
+ category: name === "plan" || name === "exit-plan" ? "Modes" : "Codex",
826
+ description: {
827
+ "plan": "Enter Codex plan mode",
828
+ "exit-plan": "Exit Codex plan mode",
829
+ compact: "Compact the current thread",
830
+ clear: "Start a fresh Codex thread",
831
+ status: "Show LinkShell agent status",
832
+ review: "Ask Codex to review local changes",
833
+ subagents: "Insert a delegation prompt",
834
+ }[name],
835
+ argsMode: name === "review" || name === "subagents" ? "optional" : "none",
836
+ destructive: name === "clear",
837
+ disabledReason,
838
+ executionKind: name === "review" || name === "subagents" ? "prompt" : "native",
839
+ }));
965
840
  }
966
841
  if (provider === "claude") {
842
+ const builtIns = CLAUDE_BUILT_IN_COMMANDS
843
+ .filter((entry) => isClaudeRemoteFriendlyCommand(entry.name))
844
+ .map((entry) => makeCommand({
845
+ provider,
846
+ name: entry.name,
847
+ description: entry.description,
848
+ argsMode: entry.argsMode,
849
+ destructive: entry.destructive,
850
+ disabledReason,
851
+ executionKind: "prompt",
852
+ }));
967
853
  const custom = customClaudeCommands(cwd).map((command) => ({
968
854
  ...command,
969
855
  disabledReason: command.disabledReason ?? disabledReason,
970
856
  }));
971
- return [...linkshellCommands, ...custom];
857
+ return [...builtIns, ...custom];
972
858
  }
973
- return linkshellCommands;
859
+ return [
860
+ makeCommand({
861
+ provider,
862
+ name: "status",
863
+ source: "linkshell",
864
+ category: "LinkShell",
865
+ description: "Show LinkShell agent status",
866
+ argsMode: "none",
867
+ disabledReason,
868
+ executionKind: "native",
869
+ }),
870
+ ];
974
871
  }
975
872
  function mergeCommands(...groups) {
976
873
  const map = new Map();
@@ -987,76 +884,6 @@ function mergeCommands(...groups) {
987
884
  }
988
885
  return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
989
886
  }
990
- function isGitNativeCommand(name) {
991
- return name === "git-status" ||
992
- name === "git-diff" ||
993
- name === "git-commit" ||
994
- name === "git-pull" ||
995
- name === "git-push" ||
996
- name === "git-stash" ||
997
- name === "git-stash-pop";
998
- }
999
- function gitCommandArgs(name, args) {
1000
- const message = args?.trim();
1001
- switch (name) {
1002
- case "git-status":
1003
- return { display: "git status --short --branch", argv: ["status", "--short", "--branch"] };
1004
- case "git-diff":
1005
- return { display: "git diff --stat", argv: ["diff", "--stat"] };
1006
- case "git-commit":
1007
- if (!message)
1008
- throw new Error("请先输入提交信息,例如 /git-commit fix mobile agent timeline");
1009
- return { display: `git commit -m ${JSON.stringify(message)}`, argv: ["commit", "-m", message] };
1010
- case "git-pull":
1011
- return { display: "git pull --ff-only", argv: ["pull", "--ff-only"] };
1012
- case "git-push":
1013
- return { display: "git push", argv: ["push"] };
1014
- case "git-stash":
1015
- return {
1016
- display: "git stash push -u",
1017
- argv: ["stash", "push", "-u", "-m", message || "LinkShell mobile stash"],
1018
- };
1019
- case "git-stash-pop":
1020
- return { display: "git stash pop", argv: ["stash", "pop"] };
1021
- default:
1022
- throw new Error(`未知 Git 命令:/${name}`);
1023
- }
1024
- }
1025
- function runProcess(command, args, options) {
1026
- return new Promise((resolve, reject) => {
1027
- const child = spawn(command, args, {
1028
- cwd: options.cwd,
1029
- stdio: ["ignore", "pipe", "pipe"],
1030
- windowsHide: true,
1031
- });
1032
- const maxBytes = options.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES;
1033
- let output = "";
1034
- let bytes = 0;
1035
- let truncated = false;
1036
- const append = (chunk) => {
1037
- if (bytes >= maxBytes) {
1038
- truncated = true;
1039
- return;
1040
- }
1041
- const remaining = maxBytes - bytes;
1042
- const slice = chunk.byteLength > remaining ? chunk.subarray(0, remaining) : chunk;
1043
- output += slice.toString("utf8");
1044
- bytes += slice.byteLength;
1045
- if (slice.byteLength < chunk.byteLength)
1046
- truncated = true;
1047
- };
1048
- child.stdout.on("data", append);
1049
- child.stderr.on("data", append);
1050
- child.once("error", reject);
1051
- child.once("close", (exitCode, signal) => {
1052
- resolve({
1053
- output: `${output.trimEnd()}${truncated ? "\n\n[truncated by LinkShell]" : ""}`,
1054
- exitCode,
1055
- signal,
1056
- });
1057
- });
1058
- });
1059
- }
1060
887
  function runtimeCommands(provider, value) {
1061
888
  const raw = asRecord(value);
1062
889
  const commandsValue = Array.isArray(value) ? value :
@@ -1100,14 +927,10 @@ function runtimeCommands(provider, value) {
1100
927
  function parseModelListCapabilities(value) {
1101
928
  const raw = asRecord(value);
1102
929
  const modelsValue = Array.isArray(value) ? value :
1103
- Array.isArray(raw?.data) ? raw.data :
1104
- Array.isArray(raw?.models) ? raw.models :
1105
- Array.isArray(raw?.items) ? raw.items :
1106
- Array.isArray(raw?.modelOptions) ? raw.modelOptions :
1107
- [];
1108
- const effortSet = new Set();
1109
- const speedTierSet = new Set(["standard"]);
1110
- let defaultModelFromEntry;
930
+ Array.isArray(raw?.models) ? raw.models :
931
+ Array.isArray(raw?.items) ? raw.items :
932
+ Array.isArray(raw?.modelOptions) ? raw.modelOptions :
933
+ [];
1111
934
  const models = modelsValue
1112
935
  .map((entry, index) => {
1113
936
  const model = asRecord(entry);
@@ -1117,56 +940,24 @@ function parseModelListCapabilities(value) {
1117
940
  : undefined;
1118
941
  }
1119
942
  const modelId = firstString(model, ["id", "model", "name", "value"]) ?? `model-${index + 1}`;
1120
- const label = firstString(model, ["displayName", "label", "title", "name", "model"]) ?? modelId;
1121
- if (model.hidden === true)
1122
- return undefined;
1123
- const modelEfforts = arrayFromKeys(model, ["supportedReasoningEfforts", "reasoningEfforts"])
1124
- .map((effort) => {
1125
- const rawEffort = asRecord(effort);
1126
- return firstString(rawEffort, ["reasoningEffort", "id", "value"]) ?? (typeof effort === "string" ? effort : undefined);
1127
- })
1128
- .filter((effort) => Boolean(effort) && ALL_REASONING_EFFORTS.includes(effort));
1129
- modelEfforts.forEach((effort) => effortSet.add(effort));
1130
- const defaultReasoningEffort = firstString(model, ["defaultReasoningEffort", "default_reasoning_effort"]);
1131
- if (defaultReasoningEffort && ALL_REASONING_EFFORTS.includes(defaultReasoningEffort)) {
1132
- effortSet.add(defaultReasoningEffort);
1133
- }
1134
- const modalities = arrayFromKeys(model, ["inputModalities", "modalities"]);
1135
- const speedTiers = arrayFromKeys(model, ["additionalSpeedTiers", "additional_speed_tiers", "serviceTiers", "service_tiers"])
1136
- .map((tier) => typeof tier === "string" ? tier : firstString(asRecord(tier), ["id", "tier", "value"]))
1137
- .filter((tier) => Boolean(tier) && ALL_SERVICE_TIERS.includes(tier));
1138
- speedTiers.forEach((tier) => speedTierSet.add(tier));
1139
- if (model.isDefault === true)
1140
- defaultModelFromEntry = modelId;
1141
- return {
1142
- id: modelId,
1143
- label,
1144
- ...(modelEfforts.length > 0 ? { reasoningEfforts: modelEfforts } : {}),
1145
- ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
1146
- ...(speedTiers.length > 0 ? { speedTiers: ["standard", ...speedTiers.filter((tier) => tier !== "standard")] } : {}),
1147
- ...(modalities.length > 0 ? { supportsImages: modalities.includes("image") } : {}),
1148
- ...(firstString(model, ["description"]) ? { description: firstString(model, ["description"]) } : {}),
1149
- };
943
+ const label = firstString(model, ["label", "title", "displayName", "name"]) ?? modelId;
944
+ return { id: modelId, label };
1150
945
  })
1151
946
  .filter((entry) => Boolean(entry));
1152
947
  const defaultModel = firstString(raw, ["defaultModel", "default_model", "currentModel"]) ??
1153
- firstString(asRecord(raw?.defaults), ["model"]) ??
1154
- defaultModelFromEntry;
948
+ firstString(asRecord(raw?.defaults), ["model"]);
1155
949
  const effortsValue = Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
1156
950
  Array.isArray(raw?.reasoning_efforts) ? raw.reasoning_efforts :
1157
951
  Array.isArray(raw?.efforts) ? raw.efforts :
1158
952
  undefined;
1159
953
  const reasoningEfforts = effortsValue
1160
954
  ?.filter((entry) => typeof entry === "string" && ALL_REASONING_EFFORTS.includes(entry));
1161
- const resolvedReasoningEfforts = reasoningEfforts?.length ? reasoningEfforts : [...effortSet];
1162
- const resolvedSpeedTiers = [...speedTierSet];
1163
- if (models.length === 0 && !defaultModel && resolvedReasoningEfforts.length === 0 && resolvedSpeedTiers.length <= 1)
955
+ if (models.length === 0 && !defaultModel && !reasoningEfforts?.length)
1164
956
  return undefined;
1165
957
  return {
1166
- ...(models.length > 0 ? { models } : {}),
958
+ ...(models.length > 0 ? { models: [{ id: "default", label: "默认模型" }, ...models] } : {}),
1167
959
  ...(defaultModel ? { defaultModel } : {}),
1168
- ...(resolvedReasoningEfforts.length > 0 ? { reasoningEfforts: resolvedReasoningEfforts } : {}),
1169
- ...(resolvedSpeedTiers.length > 0 ? { speedTiers: resolvedSpeedTiers } : {}),
960
+ ...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
1170
961
  };
1171
962
  }
1172
963
  function parseTimestamp(value) {
@@ -1206,38 +997,16 @@ function parseRemoteSessions(value) {
1206
997
  model: firstString(source, ["model", "modelId"]),
1207
998
  createdAt: parseTimestamp(source.createdAt ?? source.created_at),
1208
999
  lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
1209
- archived: typeof source.archived === "boolean" ? source.archived : undefined,
1210
- status: normalizeAgentStatus(firstString(source, ["status", "state", "phase"])),
1211
- runningTurnId: firstString(source, ["runningTurnId", "running_turn_id", "turnId", "activeTurnId"]),
1212
1000
  });
1213
1001
  }
1214
1002
  return result;
1215
1003
  }
1216
- function normalizeAgentStatus(value) {
1217
- if (!value)
1218
- return undefined;
1219
- const normalized = value.toLowerCase();
1220
- if (normalized === "running" || normalized === "in_progress" || normalized === "busy")
1221
- return "running";
1222
- if (normalized === "waiting_permission" || normalized === "waiting" || normalized === "blocked")
1223
- return "waiting_permission";
1224
- if (normalized === "error" || normalized === "failed")
1225
- return "error";
1226
- if (normalized === "idle" || normalized === "completed" || normalized === "done")
1227
- return "idle";
1228
- return undefined;
1229
- }
1230
1004
  export class AgentWorkspaceProxy {
1231
1005
  input;
1232
1006
  clients = new Map();
1233
- codexRpcBridge;
1234
1007
  agentProtocols = new Map();
1235
1008
  providerCapabilities = new Map();
1236
- providerCapabilityErrors = new Map();
1237
- capabilitiesRevision = 0;
1238
1009
  initialized = false;
1239
- initializePromise;
1240
- providerStartPromises = new Map();
1241
1010
  status = "unavailable";
1242
1011
  error;
1243
1012
  activeConversationId;
@@ -1246,8 +1015,6 @@ export class AgentWorkspaceProxy {
1246
1015
  conversations = new Map();
1247
1016
  conversationByAgentSessionId = new Map();
1248
1017
  timelines = new Map();
1249
- conversationRevisions = new Map();
1250
- revisionEvents = new Map();
1251
1018
  toolOutputBuffers = new Map();
1252
1019
  pendingPermissions = new Map();
1253
1020
  permissionWaiters = new Map();
@@ -1261,32 +1028,6 @@ export class AgentWorkspaceProxy {
1261
1028
  }
1262
1029
  async handleEnvelope(envelope) {
1263
1030
  switch (envelope.type) {
1264
- case "agent.codex.rpc": {
1265
- const payload = parseTypedPayload("agent.codex.rpc", envelope.payload);
1266
- try {
1267
- this.codexRpc().send(payload);
1268
- }
1269
- catch (error) {
1270
- const idValue = payload && typeof payload === "object" && "id" in payload
1271
- ? payload.id
1272
- : undefined;
1273
- if (typeof idValue === "string" || typeof idValue === "number") {
1274
- this.input.send(createEnvelope({
1275
- type: "agent.codex.rpc",
1276
- hostDeviceId: this.input.hostDeviceId,
1277
- payload: {
1278
- jsonrpc: "2.0",
1279
- id: idValue,
1280
- error: {
1281
- code: -32000,
1282
- message: error instanceof Error ? error.message : String(error),
1283
- },
1284
- },
1285
- }));
1286
- }
1287
- }
1288
- break;
1289
- }
1290
1031
  case "agent.v2.capabilities.request":
1291
1032
  await this.initialize();
1292
1033
  this.sendCapabilities();
@@ -1302,8 +1043,8 @@ export class AgentWorkspaceProxy {
1302
1043
  const conversations = [...this.conversations.values()].filter((conversation) => payload.includeArchived ? true : !conversation.archived);
1303
1044
  this.input.send(createEnvelope({
1304
1045
  type: "agent.v2.conversation.list.result",
1305
- hostDeviceId: this.input.hostDeviceId,
1306
- payload: { conversations: conversations.map((conversation) => this.conversationSnapshot(conversation)) },
1046
+ sessionId: this.input.sessionId,
1047
+ payload: { conversations },
1307
1048
  }));
1308
1049
  break;
1309
1050
  }
@@ -1312,16 +1053,6 @@ export class AgentWorkspaceProxy {
1312
1053
  this.sendSnapshot(payload.conversationId);
1313
1054
  break;
1314
1055
  }
1315
- case "agent.v2.history.request": {
1316
- const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
1317
- await this.sendHistoryPage(payload);
1318
- break;
1319
- }
1320
- case "agent.v2.delta.request": {
1321
- const payload = parseTypedPayload("agent.v2.delta.request", envelope.payload);
1322
- this.sendDelta(payload);
1323
- break;
1324
- }
1325
1056
  case "agent.v2.prompt": {
1326
1057
  const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
1327
1058
  await this.sendPrompt(payload);
@@ -1359,65 +1090,32 @@ export class AgentWorkspaceProxy {
1359
1090
  }
1360
1091
  }
1361
1092
  stop() {
1362
- this.codexRpcBridge?.stop();
1363
- this.codexRpcBridge = undefined;
1364
1093
  for (const client of this.clients.values()) {
1365
1094
  client.stop();
1366
1095
  }
1367
1096
  this.clients.clear();
1368
- this.providerStartPromises.clear();
1369
- this.initializePromise = undefined;
1370
1097
  }
1371
1098
  clientForProvider(provider) {
1372
1099
  return this.clients.get(provider);
1373
1100
  }
1374
- codexRpc() {
1375
- this.codexRpcBridge ??= new CodexRpcBridge({
1376
- command: this.input.command,
1377
- cwd: this.input.cwd,
1378
- hostDeviceId: this.input.hostDeviceId,
1379
- send: this.input.send,
1380
- verbose: this.input.verbose,
1381
- });
1382
- return this.codexRpcBridge;
1383
- }
1384
1101
  protocolForProvider(provider) {
1385
1102
  return this.agentProtocols.get(provider);
1386
1103
  }
1387
1104
  async initialize() {
1388
1105
  if (this.initialized)
1389
1106
  return;
1390
- if (this.initializePromise)
1391
- return this.initializePromise;
1392
- this.initializePromise = (async () => {
1393
- // Eagerly start all detected providers so capabilities report real status.
1394
- const startPromises = this.input.availableProviders
1395
- .filter((provider) => provider !== "codex")
1396
- .map((p) => this.ensureProviderClient(p));
1397
- await Promise.allSettled(startPromises);
1398
- this.initialized = true;
1399
- this.status = "idle";
1400
- this.error = undefined;
1401
- this.sendCapabilities();
1402
- })().finally(() => {
1403
- this.initializePromise = undefined;
1404
- });
1405
- return this.initializePromise;
1107
+ this.initialized = true;
1108
+ // Eagerly start all detected providers so capabilities report real status
1109
+ const startPromises = this.input.availableProviders.map((p) => this.ensureProviderClient(p));
1110
+ await Promise.allSettled(startPromises);
1111
+ this.status = "idle";
1112
+ this.error = undefined;
1113
+ this.sendCapabilities();
1406
1114
  }
1407
1115
  async ensureProviderClient(provider) {
1408
1116
  const existing = this.clients.get(provider);
1409
1117
  if (existing)
1410
1118
  return existing;
1411
- const starting = this.providerStartPromises.get(provider);
1412
- if (starting)
1413
- return starting;
1414
- const startPromise = this.startProviderClient(provider).finally(() => {
1415
- this.providerStartPromises.delete(provider);
1416
- });
1417
- this.providerStartPromises.set(provider, startPromise);
1418
- return startPromise;
1419
- }
1420
- async startProviderClient(provider) {
1421
1119
  const resolved = resolveAgentCommand({
1422
1120
  provider,
1423
1121
  command: this.input.command,
@@ -1514,13 +1212,10 @@ export class AgentWorkspaceProxy {
1514
1212
  try {
1515
1213
  const result = await listModels.call(client);
1516
1214
  const runtimeCapabilities = parseModelListCapabilities(result);
1517
- if (runtimeCapabilities) {
1215
+ if (runtimeCapabilities)
1518
1216
  this.providerCapabilities.set(provider, runtimeCapabilities);
1519
- this.providerCapabilityErrors.delete(provider);
1520
- }
1521
1217
  }
1522
1218
  catch (error) {
1523
- this.providerCapabilityErrors.set(provider, error instanceof Error ? error.message : String(error));
1524
1219
  if (this.input.verbose) {
1525
1220
  process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1526
1221
  }
@@ -1528,11 +1223,36 @@ export class AgentWorkspaceProxy {
1528
1223
  }
1529
1224
  async syncProviderSessions() {
1530
1225
  await this.initialize();
1531
- this.upsertProviderSessions("claude", listClaudeStoredSessions(this.input.cwd));
1532
1226
  for (const [provider, client] of this.clients) {
1533
1227
  try {
1534
1228
  const result = await client.listSessions();
1535
- this.upsertProviderSessions(provider, result);
1229
+ for (const remote of parseRemoteSessions(result)) {
1230
+ const agentSessionId = remote.id;
1231
+ const existingId = this.conversationByAgentSessionId.get(agentSessionId);
1232
+ const now = Date.now();
1233
+ const conversationId = existingId ?? `agent:${agentSessionId}`;
1234
+ const existing = this.conversations.get(conversationId);
1235
+ const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1236
+ const conversation = {
1237
+ id: conversationId,
1238
+ agentSessionId,
1239
+ provider,
1240
+ cwd,
1241
+ title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
1242
+ model: remote.model ?? existing?.model,
1243
+ reasoningEffort: existing?.reasoningEffort,
1244
+ permissionMode: existing?.permissionMode,
1245
+ collaborationMode: existing?.collaborationMode,
1246
+ status: existing?.status ?? "idle",
1247
+ archived: existing?.archived ?? false,
1248
+ lastMessagePreview: existing?.lastMessagePreview,
1249
+ lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
1250
+ createdAt: remote.createdAt ?? existing?.createdAt ?? now,
1251
+ };
1252
+ this.conversations.set(conversation.id, conversation);
1253
+ this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1254
+ this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1255
+ }
1536
1256
  }
1537
1257
  catch (error) {
1538
1258
  if (this.input.verbose) {
@@ -1541,55 +1261,16 @@ export class AgentWorkspaceProxy {
1541
1261
  }
1542
1262
  }
1543
1263
  }
1544
- upsertProviderSessions(provider, result) {
1545
- for (const remote of parseRemoteSessions(result)) {
1546
- const agentSessionId = remote.id;
1547
- const existingId = this.conversationByAgentSessionId.get(agentSessionId);
1548
- const now = Date.now();
1549
- const conversationId = existingId ?? `agent:${agentSessionId}`;
1550
- const existing = this.conversations.get(conversationId);
1551
- const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1552
- const conversation = {
1553
- id: conversationId,
1554
- agentSessionId,
1555
- provider,
1556
- cwd,
1557
- title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
1558
- model: remote.model ?? existing?.model,
1559
- reasoningEffort: existing?.reasoningEffort,
1560
- serviceTier: existing?.serviceTier,
1561
- permissionMode: existing?.permissionMode,
1562
- collaborationMode: existing?.collaborationMode,
1563
- status: remote.status ?? existing?.status ?? "idle",
1564
- archived: remote.archived ?? existing?.archived ?? false,
1565
- timelineRevision: existing?.timelineRevision ?? this.getRevision(conversationId),
1566
- historyComplete: existing?.historyComplete ?? false,
1567
- runningTurnId: remote.runningTurnId ?? this.currentTurnIds.get(conversationId),
1568
- source: "device",
1569
- canonical: true,
1570
- lastMessagePreview: existing?.lastMessagePreview,
1571
- lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
1572
- createdAt: remote.createdAt ?? existing?.createdAt ?? now,
1573
- };
1574
- this.conversations.set(conversation.id, conversation);
1575
- this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1576
- if (remote.runningTurnId)
1577
- this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
1578
- this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1579
- }
1580
- }
1581
1264
  sendCapabilities() {
1582
1265
  const providers = this.input.availableProviders.map((provider) => {
1583
1266
  const client = this.clients.get(provider);
1584
- const protocol = provider === "codex" ? "codex-app-server" : this.agentProtocols.get(provider);
1267
+ const protocol = this.agentProtocols.get(provider);
1585
1268
  const runtimeCapabilities = this.providerCapabilities.get(provider);
1586
- const enabled = provider === "codex" ? true : Boolean(client);
1587
- const hasRuntimeModels = Boolean(runtimeCapabilities?.models?.length);
1269
+ const enabled = Boolean(client);
1588
1270
  const supportsImages = enabled && protocolSupportsImages(protocol);
1589
- const isClaudeSdk = protocol === "claude-agent-sdk";
1590
1271
  const isClaudeFallback = protocol === "claude-stream-json";
1591
- const supportsPermission = enabled && (isClaudeSdk || !isClaudeFallback);
1592
- const supportsReasoningEffort = enabled;
1272
+ const supportsPermission = enabled && !isClaudeFallback;
1273
+ const supportsReasoningEffort = enabled && !isClaudeFallback;
1593
1274
  const commands = mergeCommands(defaultProviderCommands(provider, this.input.cwd, enabled), runtimeCapabilities?.commands);
1594
1275
  const currentMode = [...this.conversations.values()].find((conversation) => conversation.provider === provider)?.collaborationMode;
1595
1276
  return {
@@ -1597,23 +1278,21 @@ export class AgentWorkspaceProxy {
1597
1278
  label: providerLabel(provider),
1598
1279
  enabled,
1599
1280
  reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
1600
- providerProtocol: protocol,
1601
1281
  supportsImages,
1602
1282
  supportsPermission,
1603
1283
  supportsPlan: enabled,
1604
1284
  supportsCancel: enabled,
1605
1285
  models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
1606
- defaultModel: runtimeCapabilities?.defaultModel,
1607
- modelsSource: hasRuntimeModels ? "runtime" : enabled ? "fallback" : "unavailable",
1608
- modelListError: this.providerCapabilityErrors.get(provider),
1286
+ defaultModel: runtimeCapabilities?.defaultModel ?? "default",
1609
1287
  reasoningEfforts: supportsReasoningEffort
1610
- ? runtimeCapabilities?.reasoningEfforts ?? (provider === "claude" ? [...CLAUDE_REASONING_EFFORTS] : [...ALL_REASONING_EFFORTS])
1288
+ ? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
1611
1289
  : [],
1612
- speedTiers: provider === "codex" ? runtimeCapabilities?.speedTiers ?? ["standard"] : [],
1613
- defaultServiceTier: provider === "codex" ? runtimeCapabilities?.defaultServiceTier : undefined,
1614
- permissionModes: supportsPermission ? AGENT_PERMISSION_MODES : [],
1290
+ permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
1615
1291
  commands,
1616
- modes: runtimeCapabilities?.modes ?? [],
1292
+ modes: runtimeCapabilities?.modes ?? (provider === "codex" ? [
1293
+ { id: "default", title: "Default", description: "Run normal implementation turns" },
1294
+ { id: "plan", title: "Plan", description: "Discuss and produce an implementation plan first" },
1295
+ ] : []),
1617
1296
  currentMode,
1618
1297
  features: {
1619
1298
  images: supportsImages,
@@ -1621,8 +1300,6 @@ export class AgentWorkspaceProxy {
1621
1300
  plan: enabled,
1622
1301
  cancel: enabled,
1623
1302
  reasoningEffort: supportsReasoningEffort,
1624
- serviceTier: provider === "codex" && Boolean(runtimeCapabilities?.speedTiers?.includes("fast")),
1625
- claudeSdk: isClaudeSdk,
1626
1303
  streamJsonFallback: isClaudeFallback,
1627
1304
  },
1628
1305
  };
@@ -1631,14 +1308,13 @@ export class AgentWorkspaceProxy {
1631
1308
  const anyPermission = providers.some((p) => p.supportsPermission);
1632
1309
  this.input.send(createEnvelope({
1633
1310
  type: "agent.v2.capabilities",
1634
- hostDeviceId: this.input.hostDeviceId,
1311
+ sessionId: this.input.sessionId,
1635
1312
  payload: {
1636
1313
  enabled: anyEnabled,
1637
1314
  provider: this.input.availableProviders[0] ?? "codex",
1638
1315
  providers,
1639
1316
  protocolVersion: 1,
1640
1317
  workspaceProtocolVersion: 2,
1641
- capabilitiesRevision: ++this.capabilitiesRevision,
1642
1318
  error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
1643
1319
  supportsSessionList: anyEnabled,
1644
1320
  supportsSessionLoad: anyEnabled,
@@ -1652,6 +1328,16 @@ export class AgentWorkspaceProxy {
1652
1328
  }
1653
1329
  async openConversation(payload) {
1654
1330
  const provider = payload.provider ?? this.input.availableProviders[0];
1331
+ if (!provider) {
1332
+ return this.openFailure(payload, "没有可用的 Agent provider。");
1333
+ }
1334
+ if (!this.input.availableProviders.includes(provider)) {
1335
+ return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
1336
+ }
1337
+ const client = await this.ensureProviderClient(provider);
1338
+ if (!client) {
1339
+ return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
1340
+ }
1655
1341
  const cwd = payload.cwd ?? this.input.cwd;
1656
1342
  let agentSessionId = payload.agentSessionId;
1657
1343
  let existingConversation = (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
@@ -1660,34 +1346,17 @@ export class AgentWorkspaceProxy {
1660
1346
  if (payload.conversationId && existingConversation.id !== payload.conversationId) {
1661
1347
  existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
1662
1348
  }
1663
- this.hydrateStoredTimeline(existingConversation);
1664
1349
  this.activeConversationId = existingConversation.id;
1665
- const snapshot = this.latestSnapshot(existingConversation.id);
1666
1350
  this.input.send(createEnvelope({
1667
1351
  type: "agent.v2.conversation.opened",
1668
- hostDeviceId: this.input.hostDeviceId,
1352
+ sessionId: this.input.sessionId,
1669
1353
  payload: {
1670
- conversation: this.conversationSnapshot(existingConversation),
1671
- snapshot: snapshot.items,
1672
- revision: this.getRevision(existingConversation.id),
1673
- cursor: snapshot.cursor,
1674
- hasMore: snapshot.hasMore,
1675
- source: "device-history",
1676
- canonical: true,
1354
+ conversation: existingConversation,
1355
+ snapshot: this.timelines.get(existingConversation.id) ?? [],
1677
1356
  },
1678
1357
  }));
1679
1358
  return existingConversation;
1680
1359
  }
1681
- if (!provider) {
1682
- return this.openFailure(payload, "没有可用的 Agent provider。");
1683
- }
1684
- if (!this.input.availableProviders.includes(provider)) {
1685
- return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
1686
- }
1687
- const client = await this.ensureProviderClient(provider);
1688
- if (!client) {
1689
- return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
1690
- }
1691
1360
  try {
1692
1361
  const result = agentSessionId
1693
1362
  ? await client.loadSession({ sessionId: agentSessionId, cwd })
@@ -1704,16 +1373,10 @@ export class AgentWorkspaceProxy {
1704
1373
  title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
1705
1374
  model: payload.model ?? existingConversation?.model,
1706
1375
  reasoningEffort: payload.reasoningEffort ?? existingConversation?.reasoningEffort,
1707
- serviceTier: payload.serviceTier ?? existingConversation?.serviceTier,
1708
1376
  permissionMode: payload.permissionMode ?? existingConversation?.permissionMode,
1709
1377
  collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
1710
1378
  status: "idle",
1711
1379
  archived: existingConversation?.archived ?? false,
1712
- timelineRevision: existingConversation?.timelineRevision ?? this.getRevision(conversationId),
1713
- historyComplete: existingConversation?.historyComplete ?? false,
1714
- runningTurnId: this.currentTurnIds.get(conversationId),
1715
- source: "device",
1716
- canonical: true,
1717
1380
  lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
1718
1381
  lastActivityAt: now,
1719
1382
  createdAt: existingConversation?.createdAt ?? now,
@@ -1722,20 +1385,10 @@ export class AgentWorkspaceProxy {
1722
1385
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1723
1386
  this.activeConversationId = conversation.id;
1724
1387
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1725
- this.hydrateStoredTimeline(conversation);
1726
- const snapshot = this.latestSnapshot(conversation.id);
1727
1388
  this.input.send(createEnvelope({
1728
1389
  type: "agent.v2.conversation.opened",
1729
- hostDeviceId: this.input.hostDeviceId,
1730
- payload: {
1731
- conversation: this.conversationSnapshot(conversation),
1732
- snapshot: snapshot.items,
1733
- revision: this.getRevision(conversation.id),
1734
- cursor: snapshot.cursor,
1735
- hasMore: snapshot.hasMore,
1736
- source: "device-history",
1737
- canonical: true,
1738
- },
1390
+ sessionId: this.input.sessionId,
1391
+ payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
1739
1392
  }));
1740
1393
  return conversation;
1741
1394
  }
@@ -1758,10 +1411,6 @@ export class AgentWorkspaceProxy {
1758
1411
  collaborationMode: payload.collaborationMode,
1759
1412
  status: "error",
1760
1413
  archived: false,
1761
- timelineRevision: this.getRevision(fallbackId),
1762
- historyComplete: false,
1763
- source: "device",
1764
- canonical: true,
1765
1414
  lastMessagePreview: message,
1766
1415
  lastActivityAt: now,
1767
1416
  createdAt: now,
@@ -1777,15 +1426,8 @@ export class AgentWorkspaceProxy {
1777
1426
  });
1778
1427
  this.input.send(createEnvelope({
1779
1428
  type: "agent.v2.conversation.opened",
1780
- hostDeviceId: this.input.hostDeviceId,
1781
- payload: {
1782
- conversation: this.conversationSnapshot(conversation),
1783
- snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []),
1784
- revision: this.getRevision(conversation.id),
1785
- hasMore: false,
1786
- source: "device",
1787
- canonical: true,
1788
- },
1429
+ sessionId: this.input.sessionId,
1430
+ payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
1789
1431
  }));
1790
1432
  return conversation;
1791
1433
  }
@@ -1829,21 +1471,10 @@ export class AgentWorkspaceProxy {
1829
1471
  });
1830
1472
  return;
1831
1473
  }
1832
- if (Object.prototype.hasOwnProperty.call(payload, "model")) {
1833
- conversation.model = payload.model ?? undefined;
1834
- }
1835
- if (Object.prototype.hasOwnProperty.call(payload, "reasoningEffort")) {
1836
- conversation.reasoningEffort = payload.reasoningEffort ?? undefined;
1837
- }
1838
- if (Object.prototype.hasOwnProperty.call(payload, "serviceTier")) {
1839
- conversation.serviceTier = payload.serviceTier ?? undefined;
1840
- }
1841
- if (Object.prototype.hasOwnProperty.call(payload, "permissionMode")) {
1842
- conversation.permissionMode = payload.permissionMode ?? undefined;
1843
- }
1844
- if (Object.prototype.hasOwnProperty.call(payload, "collaborationMode")) {
1845
- conversation.collaborationMode = payload.collaborationMode ?? "default";
1846
- }
1474
+ conversation.model = payload.model ?? conversation.model;
1475
+ conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
1476
+ conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
1477
+ conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
1847
1478
  conversation.status = "running";
1848
1479
  conversation.lastActivityAt = Date.now();
1849
1480
  this.activeConversationId = conversation.id;
@@ -1863,11 +1494,10 @@ export class AgentWorkspaceProxy {
1863
1494
  sessionId: conversation.agentSessionId,
1864
1495
  content: payload.contentBlocks,
1865
1496
  clientMessageId: payload.clientMessageId,
1866
- model: conversation.model,
1867
- reasoningEffort: conversation.reasoningEffort,
1868
- serviceTier: conversation.serviceTier,
1869
- permissionMode: conversation.permissionMode,
1870
- collaborationMode: conversation.collaborationMode,
1497
+ model: payload.model,
1498
+ reasoningEffort: payload.reasoningEffort,
1499
+ permissionMode: payload.permissionMode,
1500
+ collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
1871
1501
  cwd: conversation.cwd,
1872
1502
  });
1873
1503
  const nextAgentSessionId = this.extractSessionId(result);
@@ -1947,7 +1577,6 @@ export class AgentWorkspaceProxy {
1947
1577
  contentBlocks: [{ type: "text", text: rawText }],
1948
1578
  model: conversation.model,
1949
1579
  reasoningEffort: conversation.reasoningEffort,
1950
- serviceTier: conversation.serviceTier,
1951
1580
  permissionMode: conversation.permissionMode,
1952
1581
  collaborationMode: conversation.collaborationMode,
1953
1582
  });
@@ -1978,8 +1607,14 @@ export class AgentWorkspaceProxy {
1978
1607
  this.emitStatus(conversation.id, conversation.status, `${providerLabel(conversation.provider)} · ${conversation.collaborationMode === "plan" ? "Plan mode" : "Default mode"} · ${conversation.cwd}`);
1979
1608
  return;
1980
1609
  }
1981
- if (isGitNativeCommand(command.name)) {
1982
- await this.executeGitNativeCommand(conversation, command.name, args);
1610
+ if (conversation.provider !== "codex") {
1611
+ this.addItem(conversation.id, {
1612
+ id: id("error"),
1613
+ conversationId: conversation.id,
1614
+ type: "error",
1615
+ error: `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`,
1616
+ createdAt: now,
1617
+ });
1983
1618
  return;
1984
1619
  }
1985
1620
  if (command.name === "plan" || command.name === "exit-plan") {
@@ -1994,22 +1629,6 @@ export class AgentWorkspaceProxy {
1994
1629
  : "已退出 Plan mode。");
1995
1630
  return;
1996
1631
  }
1997
- if (command.name === "review" || command.name === "subagents") {
1998
- const prompt = command.name === "review"
1999
- ? args || "Review the current local changes."
2000
- : args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
2001
- await this.sendPrompt({
2002
- conversationId: conversation.id,
2003
- clientMessageId: id(command.name),
2004
- contentBlocks: [{ type: "text", text: prompt }],
2005
- model: conversation.model,
2006
- reasoningEffort: conversation.reasoningEffort,
2007
- serviceTier: conversation.serviceTier,
2008
- permissionMode: conversation.permissionMode,
2009
- collaborationMode: conversation.collaborationMode,
2010
- });
2011
- return;
2012
- }
2013
1632
  if (command.name === "compact") {
2014
1633
  if (!(client instanceof AcpClient))
2015
1634
  throw new Error("当前 Codex runtime 不支持原生 compact。");
@@ -2030,7 +1649,7 @@ export class AgentWorkspaceProxy {
2030
1649
  this.emitStatus(conversation.id, "idle", "上下文压缩完成。");
2031
1650
  return;
2032
1651
  }
2033
- if (command.name === "clear" || command.name === "new") {
1652
+ if (command.name === "clear") {
2034
1653
  if (!client)
2035
1654
  throw new Error("Agent provider 不在线。");
2036
1655
  const result = await client.newSession({ cwd: conversation.cwd });
@@ -2048,6 +1667,21 @@ export class AgentWorkspaceProxy {
2048
1667
  this.emitStatus(conversation.id, "idle", "上下文已重置,已创建新的 Codex thread。");
2049
1668
  return;
2050
1669
  }
1670
+ if (command.name === "review" || command.name === "subagents") {
1671
+ const prompt = command.name === "review"
1672
+ ? args || "Review the current local changes."
1673
+ : args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
1674
+ await this.sendPrompt({
1675
+ conversationId: conversation.id,
1676
+ clientMessageId: id(command.name),
1677
+ contentBlocks: [{ type: "text", text: prompt }],
1678
+ model: conversation.model,
1679
+ reasoningEffort: conversation.reasoningEffort,
1680
+ permissionMode: conversation.permissionMode,
1681
+ collaborationMode: conversation.collaborationMode,
1682
+ });
1683
+ return;
1684
+ }
2051
1685
  throw new Error(`命令暂未实现:/${command.name}`);
2052
1686
  }
2053
1687
  catch (error) {
@@ -2062,41 +1696,6 @@ export class AgentWorkspaceProxy {
2062
1696
  });
2063
1697
  }
2064
1698
  }
2065
- async executeGitNativeCommand(conversation, commandName, args) {
2066
- const git = gitCommandArgs(commandName, args);
2067
- const toolId = id(commandName);
2068
- const now = Date.now();
2069
- conversation.status = "running";
2070
- conversation.lastMessagePreview = git.display;
2071
- conversation.lastActivityAt = now;
2072
- this.emitConversation(conversation);
2073
- this.upsertTool(conversation.id, {
2074
- id: toolId,
2075
- name: "命令",
2076
- input: `${git.display}\n\ncwd: ${conversation.cwd}`,
2077
- createdAt: now,
2078
- status: "running",
2079
- });
2080
- const result = await runProcess("git", git.argv, {
2081
- cwd: conversation.cwd,
2082
- maxBytes: COMMAND_OUTPUT_MAX_BYTES,
2083
- });
2084
- const ok = result.exitCode === 0;
2085
- this.upsertTool(conversation.id, {
2086
- id: toolId,
2087
- name: "命令",
2088
- input: `${git.display}\n\ncwd: ${conversation.cwd}`,
2089
- output: result.output || (ok ? "完成" : `退出码 ${result.exitCode ?? "unknown"}`),
2090
- createdAt: now,
2091
- status: ok ? "completed" : "failed",
2092
- });
2093
- conversation.status = ok ? "idle" : "error";
2094
- conversation.lastMessagePreview = ok
2095
- ? `${git.display} 完成`
2096
- : `${git.display} 失败`;
2097
- conversation.lastActivityAt = Date.now();
2098
- this.emitConversation(conversation);
2099
- }
2100
1699
  handleRequest(method, params) {
2101
1700
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
2102
1701
  return this.handleStructuredInput(params, true);
@@ -2134,7 +1733,6 @@ export class AgentWorkspaceProxy {
2134
1733
  }
2135
1734
  if (method.startsWith("account/") ||
2136
1735
  method.startsWith("mcpServer/startupStatus/") ||
2137
- method.startsWith("remoteControl/") ||
2138
1736
  method === "thread/status/changed" ||
2139
1737
  method === "thread/tokenUsage/updated" ||
2140
1738
  method === "serverRequest/resolved" ||
@@ -2160,9 +1758,6 @@ export class AgentWorkspaceProxy {
2160
1758
  this.conversationByAgentSessionId.set(agentSessionId, conversationId);
2161
1759
  const conversation = this.conversations.get(conversationId);
2162
1760
  if (conversation) {
2163
- if (conversation.agentSessionId && conversation.agentSessionId !== agentSessionId) {
2164
- this.conversationByAgentSessionId.delete(conversation.agentSessionId);
2165
- }
2166
1761
  conversation.agentSessionId = agentSessionId;
2167
1762
  conversation.lastActivityAt = Date.now();
2168
1763
  this.emitConversation(conversation);
@@ -2741,7 +2336,7 @@ export class AgentWorkspaceProxy {
2741
2336
  this.upsertItem(conversationId, item);
2742
2337
  this.input.send(createEnvelope({
2743
2338
  type: "agent.v2.permission.request",
2744
- hostDeviceId: this.input.hostDeviceId,
2339
+ sessionId: this.input.sessionId,
2745
2340
  payload: { conversationId, ...permission, item },
2746
2341
  }));
2747
2342
  if (!waitForResponse)
@@ -2752,14 +2347,6 @@ export class AgentWorkspaceProxy {
2752
2347
  this.permissionWaiters.delete(requestId);
2753
2348
  this.permissionSources.delete(requestId);
2754
2349
  resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
2755
- this.markPermission(conversationId, requestId, {
2756
- permissionOutcome: "cancelled",
2757
- optionId: "cancelled",
2758
- permissionLive: false,
2759
- permissionPending: false,
2760
- permissionExpired: true,
2761
- permissionError: "等待授权超时",
2762
- });
2763
2350
  this.updateConversationStatus(conversationId, "idle");
2764
2351
  }, PERMISSION_TIMEOUT_MS);
2765
2352
  this.permissionWaiters.set(requestId, { resolve, timer });
@@ -2789,8 +2376,6 @@ export class AgentWorkspaceProxy {
2789
2376
  this.markPermission(payload.conversationId, payload.requestId, {
2790
2377
  permissionOutcome: payload.outcome,
2791
2378
  optionId: selectedOptionId,
2792
- permissionLive: false,
2793
- permissionExpired: false,
2794
2379
  permissionError: undefined,
2795
2380
  permissionPending: false,
2796
2381
  });
@@ -2837,209 +2422,6 @@ export class AgentWorkspaceProxy {
2837
2422
  updatedAt: Date.now(),
2838
2423
  });
2839
2424
  }
2840
- getRevision(conversationId) {
2841
- return this.conversationRevisions.get(conversationId) ??
2842
- this.conversations.get(conversationId)?.timelineRevision ??
2843
- 0;
2844
- }
2845
- setRevisionFloor(conversationId, revision) {
2846
- const nextRevision = Math.max(this.getRevision(conversationId), revision);
2847
- this.conversationRevisions.set(conversationId, nextRevision);
2848
- const conversation = this.conversations.get(conversationId);
2849
- if (conversation) {
2850
- conversation.timelineRevision = nextRevision;
2851
- conversation.runningTurnId = this.currentTurnIds.get(conversationId);
2852
- }
2853
- return nextRevision;
2854
- }
2855
- conversationSnapshot(conversation) {
2856
- return {
2857
- ...conversation,
2858
- timelineRevision: this.getRevision(conversation.id),
2859
- historyComplete: conversation.historyComplete ?? false,
2860
- runningTurnId: this.currentTurnIds.get(conversation.id),
2861
- source: conversation.source ?? "device",
2862
- canonical: conversation.canonical ?? true,
2863
- };
2864
- }
2865
- annotateTimelineItem(item, revision, source) {
2866
- return {
2867
- ...item,
2868
- revision: revision ?? item.revision,
2869
- source: item.source ?? source,
2870
- canonical: item.canonical ?? true,
2871
- };
2872
- }
2873
- recordRevisionEvent(conversationId, change) {
2874
- const revision = this.getRevision(conversationId) + 1;
2875
- this.conversationRevisions.set(conversationId, revision);
2876
- const conversation = this.conversations.get(conversationId);
2877
- if (conversation) {
2878
- conversation.timelineRevision = revision;
2879
- conversation.runningTurnId = this.currentTurnIds.get(conversationId);
2880
- }
2881
- const event = {
2882
- revision,
2883
- item: change.item ? this.annotateTimelineItem(change.item, revision, "device-live") : undefined,
2884
- conversation: change.conversation ? this.conversationSnapshot(change.conversation) : undefined,
2885
- };
2886
- const events = this.revisionEvents.get(conversationId) ?? [];
2887
- events.push(event);
2888
- if (events.length > MAX_DELTA_EVENTS) {
2889
- events.splice(0, events.length - MAX_DELTA_EVENTS);
2890
- }
2891
- this.revisionEvents.set(conversationId, events);
2892
- return event;
2893
- }
2894
- storedTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
2895
- if (!conversation.agentSessionId)
2896
- return [];
2897
- const result = conversation.provider === "codex"
2898
- ? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd, { maxItems })
2899
- : conversation.provider === "claude"
2900
- ? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id, { maxItems })
2901
- : { items: [] };
2902
- return result.items
2903
- .sort((a, b) => a.createdAt - b.createdAt)
2904
- .map((item, index) => this.annotateTimelineItem(item, index + 1, "device-history"));
2905
- }
2906
- canonicalTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
2907
- const merged = new Map();
2908
- for (const item of this.storedTimeline(conversation, maxItems)) {
2909
- merged.set(item.id, item);
2910
- }
2911
- for (const item of this.timelines.get(conversation.id) ?? []) {
2912
- merged.set(item.id, this.annotateTimelineItem(item, item.revision, item.source ?? "device-live"));
2913
- }
2914
- const items = [...merged.values()]
2915
- .sort((a, b) => a.createdAt - b.createdAt)
2916
- .slice(-maxItems);
2917
- this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
2918
- return items;
2919
- }
2920
- async appServerTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
2921
- if (conversation.provider !== "codex" || !conversation.agentSessionId)
2922
- return [];
2923
- if (this.protocolForProvider(conversation.provider) !== "codex-app-server")
2924
- return [];
2925
- const client = this.clientForProvider(conversation.provider);
2926
- const listTurns = client?.listTurns;
2927
- if (typeof listTurns !== "function")
2928
- return [];
2929
- try {
2930
- const result = await listTurns.call(client, { sessionId: conversation.agentSessionId, limit: maxItems });
2931
- return this.timelineFromAppServerTurns(conversation.id, conversation.agentSessionId, result);
2932
- }
2933
- catch (error) {
2934
- if (this.input.verbose) {
2935
- process.stderr.write(`[agent:v2] thread/turns/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
2936
- }
2937
- return [];
2938
- }
2939
- }
2940
- timelineFromAppServerTurns(conversationId, agentSessionId, value) {
2941
- const raw = asRecord(value);
2942
- const turns = Array.isArray(value) ? value :
2943
- Array.isArray(raw?.turns) ? raw.turns :
2944
- Array.isArray(raw?.items) ? raw.items :
2945
- Array.isArray(raw?.entries) ? raw.entries :
2946
- [];
2947
- const items = [];
2948
- turns.forEach((entry, index) => {
2949
- const turn = asRecord(entry);
2950
- if (!turn)
2951
- return;
2952
- const turnId = firstString(turn, ["id", "turnId", "turn_id"]) ?? `turn-${index + 1}`;
2953
- const createdAt = parseTimestamp(turn.createdAt ?? turn.created_at ?? turn.startedAt ?? turn.started_at) ?? Date.now() + index;
2954
- const updatedAt = parseTimestamp(turn.updatedAt ?? turn.updated_at ?? turn.completedAt ?? turn.completed_at);
2955
- const userText = appServerText(turn.input ?? turn.prompt ?? turn.user ?? turn.userMessage ?? turn.request);
2956
- if (userText) {
2957
- items.push({
2958
- id: `app-server:${agentSessionId}:${turnId}:user`,
2959
- conversationId,
2960
- type: "message",
2961
- kind: "chat",
2962
- turnId,
2963
- role: "user",
2964
- content: [{ type: "text", text: userText }],
2965
- text: userText,
2966
- createdAt,
2967
- updatedAt,
2968
- metadata: { source: "app-server", provider: "codex" },
2969
- });
2970
- }
2971
- const assistantText = appServerText(turn.output ?? turn.response ?? turn.assistant ?? turn.assistantMessage ?? turn.result);
2972
- if (assistantText) {
2973
- items.push({
2974
- id: `app-server:${agentSessionId}:${turnId}:assistant`,
2975
- conversationId,
2976
- type: "message",
2977
- kind: "chat",
2978
- turnId,
2979
- role: "assistant",
2980
- content: [{ type: "text", text: assistantText }],
2981
- text: assistantText,
2982
- createdAt: updatedAt ?? createdAt + 1,
2983
- updatedAt,
2984
- metadata: { source: "app-server", provider: "codex" },
2985
- });
2986
- }
2987
- const nestedItems = Array.isArray(turn.items) ? turn.items : Array.isArray(turn.messages) ? turn.messages : [];
2988
- for (const nested of nestedItems) {
2989
- const nestedRecord = asRecord(nested);
2990
- if (!nestedRecord)
2991
- continue;
2992
- const text = appServerText(nestedRecord.content ?? nestedRecord.text ?? nestedRecord.message);
2993
- const role = nestedRecord.role === "user" || nestedRecord.role === "assistant" || nestedRecord.role === "system"
2994
- ? nestedRecord.role
2995
- : undefined;
2996
- if (!text || !role)
2997
- continue;
2998
- const itemId = firstString(nestedRecord, ["id", "itemId", "messageId"]) ?? `${role}-${items.length + 1}`;
2999
- items.push({
3000
- id: `app-server:${agentSessionId}:${turnId}:${itemId}`,
3001
- conversationId,
3002
- type: "message",
3003
- kind: "chat",
3004
- turnId,
3005
- itemId,
3006
- role,
3007
- content: [{ type: "text", text }],
3008
- text,
3009
- createdAt: parseTimestamp(nestedRecord.createdAt ?? nestedRecord.created_at) ?? createdAt + items.length,
3010
- updatedAt: parseTimestamp(nestedRecord.updatedAt ?? nestedRecord.updated_at),
3011
- metadata: { source: "app-server", provider: "codex" },
3012
- });
3013
- }
3014
- });
3015
- return items
3016
- .sort((a, b) => a.createdAt - b.createdAt)
3017
- .map((item, index) => this.annotateTimelineItem(item, index + 1, "app-server"));
3018
- }
3019
- mergeCanonicalTimelineItems(items, maxItems) {
3020
- const byKey = new Map();
3021
- for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
3022
- const key = item.type === "message" && item.role && item.text
3023
- ? `${item.role}:${item.text.replace(/\s+/g, " ").trim().slice(0, 500)}`
3024
- : item.id;
3025
- const existing = byKey.get(key);
3026
- if (!existing || item.source === "app-server" || (item.updatedAt ?? item.createdAt) >= (existing.updatedAt ?? existing.createdAt)) {
3027
- byKey.set(key, item);
3028
- }
3029
- }
3030
- return [...byKey.values()]
3031
- .sort((a, b) => a.createdAt - b.createdAt)
3032
- .slice(-maxItems);
3033
- }
3034
- latestSnapshot(conversationId) {
3035
- const timeline = this.timelines.get(conversationId) ?? [];
3036
- const start = Math.max(0, timeline.length - MAX_SNAPSHOT_ITEMS);
3037
- return {
3038
- items: timeline.slice(start).map((item) => snapshotTimelineItem(item)),
3039
- cursor: start > 0 ? String(start) : undefined,
3040
- hasMore: start > 0,
3041
- };
3042
- }
3043
2425
  addItem(conversationId, item) {
3044
2426
  this.rememberItemConversationId(conversationId, item);
3045
2427
  const timeline = this.timelines.get(conversationId) ?? [];
@@ -3077,8 +2459,6 @@ export class AgentWorkspaceProxy {
3077
2459
  ...existing,
3078
2460
  ...toolCall,
3079
2461
  id: targetToolId,
3080
- input: toolCall.input ?? existing?.input,
3081
- output: toolCall.output ?? existing?.output,
3082
2462
  createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
3083
2463
  };
3084
2464
  this.toolConversationIds.set(toolCall.id, conversationId);
@@ -3147,7 +2527,6 @@ export class AgentWorkspaceProxy {
3147
2527
  conversation.title = conversation.title ?? target.title;
3148
2528
  conversation.model = conversation.model ?? target.model;
3149
2529
  conversation.reasoningEffort = conversation.reasoningEffort ?? target.reasoningEffort;
3150
- conversation.serviceTier = conversation.serviceTier ?? target.serviceTier;
3151
2530
  conversation.permissionMode = conversation.permissionMode ?? target.permissionMode;
3152
2531
  conversation.lastMessagePreview = conversation.lastMessagePreview ?? target.lastMessagePreview;
3153
2532
  conversation.createdAt = Math.min(conversation.createdAt, target.createdAt);
@@ -3160,17 +2539,6 @@ export class AgentWorkspaceProxy {
3160
2539
  this.timelines.set(newId, [...mergedTimeline.values()]
3161
2540
  .sort((a, b) => a.createdAt - b.createdAt)
3162
2541
  .slice(-MAX_TIMELINE_ITEMS));
3163
- const oldRevision = this.conversationRevisions.get(oldId) ?? 0;
3164
- if (oldRevision > 0) {
3165
- this.conversationRevisions.delete(oldId);
3166
- this.setRevisionFloor(newId, oldRevision);
3167
- }
3168
- const oldEvents = this.revisionEvents.get(oldId);
3169
- if (oldEvents) {
3170
- this.revisionEvents.delete(oldId);
3171
- const existingEvents = this.revisionEvents.get(newId) ?? [];
3172
- this.revisionEvents.set(newId, [...existingEvents, ...oldEvents].slice(-MAX_DELTA_EVENTS));
3173
- }
3174
2542
  for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
3175
2543
  if (conversationId === oldId) {
3176
2544
  this.conversationByAgentSessionId.set(agentSessionId, newId);
@@ -3204,47 +2572,17 @@ export class AgentWorkspaceProxy {
3204
2572
  }
3205
2573
  emitItem(conversationId, item) {
3206
2574
  const conversation = this.conversations.get(conversationId);
3207
- const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
3208
- const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
3209
2575
  this.input.send(createEnvelope({
3210
2576
  type: "agent.v2.event",
3211
- hostDeviceId: this.input.hostDeviceId,
3212
- payload: {
3213
- conversationId,
3214
- conversation: event.conversation,
3215
- item: event.item,
3216
- revision: event.revision,
3217
- source: "device-live",
3218
- canonical: true,
3219
- },
2577
+ sessionId: this.input.sessionId,
2578
+ payload: { conversationId, conversation, item },
3220
2579
  }));
3221
2580
  }
3222
2581
  emitConversation(conversation) {
3223
- const event = this.recordRevisionEvent(conversation.id, { conversation });
3224
2582
  this.input.send(createEnvelope({
3225
2583
  type: "agent.v2.event",
3226
- hostDeviceId: this.input.hostDeviceId,
3227
- payload: {
3228
- conversationId: conversation.id,
3229
- conversation: event.conversation,
3230
- revision: event.revision,
3231
- source: "device-live",
3232
- canonical: true,
3233
- },
3234
- }));
3235
- this.input.send(createEnvelope({
3236
- type: "agent.v2.running_state",
3237
- hostDeviceId: this.input.hostDeviceId,
3238
- payload: {
3239
- conversationId: conversation.id,
3240
- status: conversation.status,
3241
- runningTurnId: this.currentTurnIds.get(conversation.id),
3242
- revision: event.revision,
3243
- error: conversation.status === "error" ? conversation.lastMessagePreview : undefined,
3244
- updatedAt: conversation.lastActivityAt,
3245
- source: "device-live",
3246
- canonical: true,
3247
- },
2584
+ sessionId: this.input.sessionId,
2585
+ payload: { conversationId: conversation.id, conversation },
3248
2586
  }));
3249
2587
  }
3250
2588
  emitStatus(conversationId, status, text) {
@@ -3278,169 +2616,20 @@ export class AgentWorkspaceProxy {
3278
2616
  this.emitConversation(conversation);
3279
2617
  }
3280
2618
  sendSnapshot(conversationId) {
3281
- if (conversationId) {
3282
- const conversation = this.conversations.get(conversationId);
3283
- if (conversation)
3284
- this.hydrateStoredTimeline(conversation);
3285
- }
3286
- else if (this.activeConversationId) {
3287
- const conversation = this.conversations.get(this.activeConversationId);
3288
- if (conversation)
3289
- this.hydrateStoredTimeline(conversation);
3290
- }
3291
- const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
3292
- const snapshot = conversationId ? this.latestSnapshot(conversationId) : { items: [], cursor: undefined, hasMore: false };
2619
+ const conversations = [...this.conversations.values()];
2620
+ const items = conversationId
2621
+ ? this.timelines.get(conversationId) ?? []
2622
+ : [...this.timelines.values()].flat();
3293
2623
  this.input.send(createEnvelope({
3294
2624
  type: "agent.v2.snapshot",
3295
- hostDeviceId: this.input.hostDeviceId,
2625
+ sessionId: this.input.sessionId,
3296
2626
  payload: {
3297
2627
  conversations,
3298
2628
  activeConversationId: this.activeConversationId,
3299
- items: snapshot.items,
3300
- revision: conversationId ? this.getRevision(conversationId) : undefined,
3301
- cursor: snapshot.cursor,
3302
- hasMore: snapshot.hasMore,
3303
- source: "device-history",
3304
- canonical: true,
3305
- },
3306
- }));
3307
- }
3308
- async sendHistoryPage(payload) {
3309
- const conversation = this.conversations.get(payload.conversationId);
3310
- if (!conversation)
3311
- return;
3312
- const limit = Math.min(Math.max(payload.limit ?? MAX_SNAPSHOT_ITEMS, 1), 200);
3313
- const items = this.mergeCanonicalTimelineItems([
3314
- ...this.canonicalTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
3315
- ...await this.appServerTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
3316
- ], HISTORY_PAGE_MAX_ITEMS);
3317
- this.timelines.set(conversation.id, items.slice(-MAX_TIMELINE_ITEMS).map((item) => this.annotateTimelineItem(item, item.revision, item.source ?? "device-history")));
3318
- for (const item of this.timelines.get(conversation.id) ?? []) {
3319
- this.rememberItemConversationId(conversation.id, item);
3320
- }
3321
- const direction = payload.direction ?? "older";
3322
- let page;
3323
- let cursor;
3324
- let hasMore = false;
3325
- if (direction === "newer") {
3326
- const start = clampHistoryCursor(payload.cursor, 0, items.length);
3327
- const end = Math.min(items.length, start + limit);
3328
- page = items.slice(start, end);
3329
- hasMore = end < items.length;
3330
- cursor = hasMore ? String(end) : undefined;
3331
- }
3332
- else {
3333
- const end = clampHistoryCursor(payload.cursor, items.length, items.length);
3334
- const start = Math.max(0, end - limit);
3335
- page = items.slice(start, end);
3336
- hasMore = start > 0;
3337
- cursor = hasMore ? String(start) : undefined;
3338
- }
3339
- conversation.historyComplete = !hasMore;
3340
- const revision = this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
3341
- this.input.send(createEnvelope({
3342
- type: "agent.v2.history.page",
3343
- hostDeviceId: this.input.hostDeviceId,
3344
- payload: {
3345
- conversationId: conversation.id,
3346
- conversation: this.conversationSnapshot(conversation),
3347
- items: page.map((item) => snapshotTimelineItem(item)),
3348
- revision,
3349
- cursor,
3350
- hasMore,
3351
- source: "device-history",
3352
- canonical: true,
3353
- },
3354
- }));
3355
- }
3356
- sendDelta(payload) {
3357
- const conversation = this.conversations.get(payload.conversationId);
3358
- if (!conversation)
3359
- return;
3360
- this.hydrateStoredTimeline(conversation);
3361
- const sinceRevision = payload.sinceRevision ?? 0;
3362
- const limit = Math.min(Math.max(payload.limit ?? 100, 1), 500);
3363
- const events = this.revisionEvents.get(conversation.id) ?? [];
3364
- const oldestAvailable = events[0]?.revision ?? this.getRevision(conversation.id);
3365
- const newestRevision = this.getRevision(conversation.id);
3366
- const reset = sinceRevision > 0 &&
3367
- (sinceRevision > newestRevision ||
3368
- (sinceRevision < newestRevision &&
3369
- (events.length === 0 || sinceRevision < oldestAvailable - 1)));
3370
- if (reset) {
3371
- const snapshot = this.latestSnapshot(conversation.id);
3372
- this.input.send(createEnvelope({
3373
- type: "agent.v2.delta",
3374
- hostDeviceId: this.input.hostDeviceId,
3375
- payload: {
3376
- conversationId: conversation.id,
3377
- conversation: this.conversationSnapshot(conversation),
3378
- items: snapshot.items,
3379
- sinceRevision,
3380
- revision: newestRevision,
3381
- reset: true,
3382
- cursor: snapshot.cursor,
3383
- hasMore: snapshot.hasMore,
3384
- source: "device-history",
3385
- canonical: true,
3386
- },
3387
- }));
3388
- return;
3389
- }
3390
- const changed = events
3391
- .filter((event) => event.revision > sinceRevision)
3392
- .slice(-limit);
3393
- const itemsById = new Map();
3394
- for (const event of changed) {
3395
- if (event.item)
3396
- itemsById.set(event.item.id, event.item);
3397
- }
3398
- this.input.send(createEnvelope({
3399
- type: "agent.v2.delta",
3400
- hostDeviceId: this.input.hostDeviceId,
3401
- payload: {
3402
- conversationId: conversation.id,
3403
- conversation: this.conversationSnapshot(conversation),
3404
- items: [...itemsById.values()].map((item) => snapshotTimelineItem(item)),
3405
- sinceRevision,
3406
- revision: newestRevision,
3407
- reset: false,
3408
- hasMore: changed.length === limit && events.some((event) => event.revision > sinceRevision && event.revision < changed[0].revision),
3409
- source: "device-live",
3410
- canonical: true,
2629
+ items,
3411
2630
  },
3412
2631
  }));
3413
2632
  }
3414
- hydrateStoredTimeline(conversation) {
3415
- if (!conversation.agentSessionId)
3416
- return;
3417
- const existing = this.timelines.get(conversation.id) ?? [];
3418
- if (existing.length > 0) {
3419
- this.setRevisionFloor(conversation.id, Math.max(existing.length, ...existing.map((item) => item.revision ?? 0)));
3420
- return;
3421
- }
3422
- const stored = this.storedTimeline(conversation, MAX_TIMELINE_ITEMS);
3423
- conversation.historyComplete = stored.length <= MAX_SNAPSHOT_ITEMS;
3424
- if (stored.length === 0) {
3425
- this.setRevisionFloor(conversation.id, this.getRevision(conversation.id));
3426
- return;
3427
- }
3428
- const items = stored
3429
- .sort((a, b) => a.createdAt - b.createdAt)
3430
- .slice(-MAX_TIMELINE_ITEMS);
3431
- this.timelines.set(conversation.id, items);
3432
- for (const item of items)
3433
- this.rememberItemConversationId(conversation.id, item);
3434
- this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
3435
- const lastMessage = [...items].reverse().find((item) => item.text?.trim());
3436
- if (lastMessage?.text && !conversation.lastMessagePreview) {
3437
- conversation.lastMessagePreview = previewText(lastMessage.text);
3438
- }
3439
- const lastActivityAt = items.at(-1)?.createdAt;
3440
- if (lastActivityAt) {
3441
- conversation.lastActivityAt = Math.max(conversation.lastActivityAt, lastActivityAt);
3442
- }
3443
- }
3444
2633
  conversationIdFromParams(params) {
3445
2634
  const raw = asRecord(params);
3446
2635
  if (!raw)
@@ -3500,9 +2689,6 @@ export class AgentWorkspaceProxy {
3500
2689
  rememberTurnConversationId(conversationId, turnId) {
3501
2690
  this.currentTurnIds.set(conversationId, turnId);
3502
2691
  this.turnConversationIds.set(turnId, conversationId);
3503
- const conversation = this.conversations.get(conversationId);
3504
- if (conversation)
3505
- conversation.runningTurnId = turnId;
3506
2692
  }
3507
2693
  forgetCurrentTurn(conversationId, turnId) {
3508
2694
  const currentTurnId = this.currentTurnIds.get(conversationId);
@@ -3511,9 +2697,6 @@ export class AgentWorkspaceProxy {
3511
2697
  this.turnConversationIds.delete(turnId);
3512
2698
  if (currentTurnId && currentTurnId !== turnId)
3513
2699
  this.turnConversationIds.delete(currentTurnId);
3514
- const conversation = this.conversations.get(conversationId);
3515
- if (conversation)
3516
- conversation.runningTurnId = undefined;
3517
2700
  }
3518
2701
  rememberItemConversationId(conversationId, item) {
3519
2702
  const keys = [
@@ -3553,51 +2736,28 @@ export class AgentWorkspaceProxy {
3553
2736
  }
3554
2737
  cancelPendingPermissions(conversationId) {
3555
2738
  for (const [requestId, waiter] of this.permissionWaiters) {
3556
- const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
3557
- if (conversationId && ownerConversationId && ownerConversationId !== conversationId)
3558
- continue;
3559
2739
  clearTimeout(waiter.timer);
3560
2740
  waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
3561
- if (ownerConversationId) {
3562
- this.markPermission(ownerConversationId, requestId, {
3563
- permissionOutcome: "cancelled",
3564
- optionId: "cancelled",
3565
- permissionLive: false,
3566
- permissionPending: false,
3567
- permissionExpired: false,
3568
- permissionError: "已停止",
3569
- });
3570
- }
3571
- this.permissionWaiters.delete(requestId);
3572
2741
  this.pendingPermissions.delete(requestId);
3573
2742
  this.permissionSources.delete(requestId);
3574
2743
  }
2744
+ this.permissionWaiters.clear();
3575
2745
  for (const [requestId, waiter] of this.structuredInputWaiters) {
3576
- const pending = this.pendingStructuredInputs.get(requestId);
3577
- if (conversationId && pending?.conversationId && pending.conversationId !== conversationId)
3578
- continue;
3579
2746
  clearTimeout(waiter.timer);
3580
2747
  waiter.resolve(formatStructuredInputResponse({}));
2748
+ const pending = this.pendingStructuredInputs.get(requestId);
3581
2749
  if (pending) {
3582
2750
  this.markStructuredInput(pending.conversationId, requestId, {
3583
2751
  inputPending: false,
3584
2752
  inputError: "已停止",
3585
2753
  });
3586
2754
  }
3587
- this.structuredInputWaiters.delete(requestId);
3588
2755
  this.pendingStructuredInputs.delete(requestId);
3589
2756
  }
2757
+ this.structuredInputWaiters.clear();
3590
2758
  if (conversationId)
3591
2759
  this.updateConversationStatus(conversationId, "idle");
3592
2760
  }
3593
- conversationIdForPermissionRequest(requestId) {
3594
- for (const [conversationId, timeline] of this.timelines) {
3595
- if (timeline.some((item) => item.type === "permission" && item.permission?.requestId === requestId)) {
3596
- return conversationId;
3597
- }
3598
- }
3599
- return undefined;
3600
- }
3601
2761
  extractSessionId(value) {
3602
2762
  const raw = asRecord(value);
3603
2763
  if (!raw)