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,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,35 +997,15 @@ 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
1010
  status = "unavailable";
1240
1011
  error;
@@ -1244,8 +1015,6 @@ export class AgentWorkspaceProxy {
1244
1015
  conversations = new Map();
1245
1016
  conversationByAgentSessionId = new Map();
1246
1017
  timelines = new Map();
1247
- conversationRevisions = new Map();
1248
- revisionEvents = new Map();
1249
1018
  toolOutputBuffers = new Map();
1250
1019
  pendingPermissions = new Map();
1251
1020
  permissionWaiters = new Map();
@@ -1259,32 +1028,6 @@ export class AgentWorkspaceProxy {
1259
1028
  }
1260
1029
  async handleEnvelope(envelope) {
1261
1030
  switch (envelope.type) {
1262
- case "agent.codex.rpc": {
1263
- const payload = parseTypedPayload("agent.codex.rpc", envelope.payload);
1264
- try {
1265
- this.codexRpc().send(payload);
1266
- }
1267
- catch (error) {
1268
- const idValue = payload && typeof payload === "object" && "id" in payload
1269
- ? payload.id
1270
- : undefined;
1271
- if (typeof idValue === "string" || typeof idValue === "number") {
1272
- this.input.send(createEnvelope({
1273
- type: "agent.codex.rpc",
1274
- hostDeviceId: this.input.hostDeviceId,
1275
- payload: {
1276
- jsonrpc: "2.0",
1277
- id: idValue,
1278
- error: {
1279
- code: -32000,
1280
- message: error instanceof Error ? error.message : String(error),
1281
- },
1282
- },
1283
- }));
1284
- }
1285
- }
1286
- break;
1287
- }
1288
1031
  case "agent.v2.capabilities.request":
1289
1032
  await this.initialize();
1290
1033
  this.sendCapabilities();
@@ -1300,8 +1043,8 @@ export class AgentWorkspaceProxy {
1300
1043
  const conversations = [...this.conversations.values()].filter((conversation) => payload.includeArchived ? true : !conversation.archived);
1301
1044
  this.input.send(createEnvelope({
1302
1045
  type: "agent.v2.conversation.list.result",
1303
- hostDeviceId: this.input.hostDeviceId,
1304
- payload: { conversations: conversations.map((conversation) => this.conversationSnapshot(conversation)) },
1046
+ sessionId: this.input.sessionId,
1047
+ payload: { conversations },
1305
1048
  }));
1306
1049
  break;
1307
1050
  }
@@ -1310,16 +1053,6 @@ export class AgentWorkspaceProxy {
1310
1053
  this.sendSnapshot(payload.conversationId);
1311
1054
  break;
1312
1055
  }
1313
- case "agent.v2.history.request": {
1314
- const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
1315
- await this.sendHistoryPage(payload);
1316
- break;
1317
- }
1318
- case "agent.v2.delta.request": {
1319
- const payload = parseTypedPayload("agent.v2.delta.request", envelope.payload);
1320
- this.sendDelta(payload);
1321
- break;
1322
- }
1323
1056
  case "agent.v2.prompt": {
1324
1057
  const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
1325
1058
  await this.sendPrompt(payload);
@@ -1357,8 +1090,6 @@ export class AgentWorkspaceProxy {
1357
1090
  }
1358
1091
  }
1359
1092
  stop() {
1360
- this.codexRpcBridge?.stop();
1361
- this.codexRpcBridge = undefined;
1362
1093
  for (const client of this.clients.values()) {
1363
1094
  client.stop();
1364
1095
  }
@@ -1367,16 +1098,6 @@ export class AgentWorkspaceProxy {
1367
1098
  clientForProvider(provider) {
1368
1099
  return this.clients.get(provider);
1369
1100
  }
1370
- codexRpc() {
1371
- this.codexRpcBridge ??= new CodexRpcBridge({
1372
- command: this.input.command,
1373
- cwd: this.input.cwd,
1374
- hostDeviceId: this.input.hostDeviceId,
1375
- send: this.input.send,
1376
- verbose: this.input.verbose,
1377
- });
1378
- return this.codexRpcBridge;
1379
- }
1380
1101
  protocolForProvider(provider) {
1381
1102
  return this.agentProtocols.get(provider);
1382
1103
  }
@@ -1385,9 +1106,7 @@ export class AgentWorkspaceProxy {
1385
1106
  return;
1386
1107
  this.initialized = true;
1387
1108
  // Eagerly start all detected providers so capabilities report real status
1388
- const startPromises = this.input.availableProviders
1389
- .filter((provider) => provider !== "codex")
1390
- .map((p) => this.ensureProviderClient(p));
1109
+ const startPromises = this.input.availableProviders.map((p) => this.ensureProviderClient(p));
1391
1110
  await Promise.allSettled(startPromises);
1392
1111
  this.status = "idle";
1393
1112
  this.error = undefined;
@@ -1493,13 +1212,10 @@ export class AgentWorkspaceProxy {
1493
1212
  try {
1494
1213
  const result = await listModels.call(client);
1495
1214
  const runtimeCapabilities = parseModelListCapabilities(result);
1496
- if (runtimeCapabilities) {
1215
+ if (runtimeCapabilities)
1497
1216
  this.providerCapabilities.set(provider, runtimeCapabilities);
1498
- this.providerCapabilityErrors.delete(provider);
1499
- }
1500
1217
  }
1501
1218
  catch (error) {
1502
- this.providerCapabilityErrors.set(provider, error instanceof Error ? error.message : String(error));
1503
1219
  if (this.input.verbose) {
1504
1220
  process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1505
1221
  }
@@ -1507,11 +1223,36 @@ export class AgentWorkspaceProxy {
1507
1223
  }
1508
1224
  async syncProviderSessions() {
1509
1225
  await this.initialize();
1510
- this.upsertProviderSessions("claude", listClaudeStoredSessions(this.input.cwd));
1511
1226
  for (const [provider, client] of this.clients) {
1512
1227
  try {
1513
1228
  const result = await client.listSessions();
1514
- 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
+ }
1515
1256
  }
1516
1257
  catch (error) {
1517
1258
  if (this.input.verbose) {
@@ -1520,55 +1261,16 @@ export class AgentWorkspaceProxy {
1520
1261
  }
1521
1262
  }
1522
1263
  }
1523
- upsertProviderSessions(provider, result) {
1524
- for (const remote of parseRemoteSessions(result)) {
1525
- const agentSessionId = remote.id;
1526
- const existingId = this.conversationByAgentSessionId.get(agentSessionId);
1527
- const now = Date.now();
1528
- const conversationId = existingId ?? `agent:${agentSessionId}`;
1529
- const existing = this.conversations.get(conversationId);
1530
- const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1531
- const conversation = {
1532
- id: conversationId,
1533
- agentSessionId,
1534
- provider,
1535
- cwd,
1536
- title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
1537
- model: remote.model ?? existing?.model,
1538
- reasoningEffort: existing?.reasoningEffort,
1539
- serviceTier: existing?.serviceTier,
1540
- permissionMode: existing?.permissionMode,
1541
- collaborationMode: existing?.collaborationMode,
1542
- status: remote.status ?? existing?.status ?? "idle",
1543
- archived: remote.archived ?? existing?.archived ?? false,
1544
- timelineRevision: existing?.timelineRevision ?? this.getRevision(conversationId),
1545
- historyComplete: existing?.historyComplete ?? false,
1546
- runningTurnId: remote.runningTurnId ?? this.currentTurnIds.get(conversationId),
1547
- source: "device",
1548
- canonical: true,
1549
- lastMessagePreview: existing?.lastMessagePreview,
1550
- lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
1551
- createdAt: remote.createdAt ?? existing?.createdAt ?? now,
1552
- };
1553
- this.conversations.set(conversation.id, conversation);
1554
- this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1555
- if (remote.runningTurnId)
1556
- this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
1557
- this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1558
- }
1559
- }
1560
1264
  sendCapabilities() {
1561
1265
  const providers = this.input.availableProviders.map((provider) => {
1562
1266
  const client = this.clients.get(provider);
1563
- const protocol = provider === "codex" ? "codex-app-server" : this.agentProtocols.get(provider);
1267
+ const protocol = this.agentProtocols.get(provider);
1564
1268
  const runtimeCapabilities = this.providerCapabilities.get(provider);
1565
- const enabled = provider === "codex" ? true : Boolean(client);
1566
- const hasRuntimeModels = Boolean(runtimeCapabilities?.models?.length);
1269
+ const enabled = Boolean(client);
1567
1270
  const supportsImages = enabled && protocolSupportsImages(protocol);
1568
- const isClaudeSdk = protocol === "claude-agent-sdk";
1569
1271
  const isClaudeFallback = protocol === "claude-stream-json";
1570
- const supportsPermission = enabled && (isClaudeSdk || !isClaudeFallback);
1571
- const supportsReasoningEffort = enabled;
1272
+ const supportsPermission = enabled && !isClaudeFallback;
1273
+ const supportsReasoningEffort = enabled && !isClaudeFallback;
1572
1274
  const commands = mergeCommands(defaultProviderCommands(provider, this.input.cwd, enabled), runtimeCapabilities?.commands);
1573
1275
  const currentMode = [...this.conversations.values()].find((conversation) => conversation.provider === provider)?.collaborationMode;
1574
1276
  return {
@@ -1576,23 +1278,21 @@ export class AgentWorkspaceProxy {
1576
1278
  label: providerLabel(provider),
1577
1279
  enabled,
1578
1280
  reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
1579
- providerProtocol: protocol,
1580
1281
  supportsImages,
1581
1282
  supportsPermission,
1582
1283
  supportsPlan: enabled,
1583
1284
  supportsCancel: enabled,
1584
1285
  models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
1585
- defaultModel: runtimeCapabilities?.defaultModel,
1586
- modelsSource: hasRuntimeModels ? "runtime" : enabled ? "fallback" : "unavailable",
1587
- modelListError: this.providerCapabilityErrors.get(provider),
1286
+ defaultModel: runtimeCapabilities?.defaultModel ?? "default",
1588
1287
  reasoningEfforts: supportsReasoningEffort
1589
- ? runtimeCapabilities?.reasoningEfforts ?? (provider === "claude" ? [...CLAUDE_REASONING_EFFORTS] : [...ALL_REASONING_EFFORTS])
1288
+ ? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
1590
1289
  : [],
1591
- speedTiers: provider === "codex" ? runtimeCapabilities?.speedTiers ?? ["standard"] : [],
1592
- defaultServiceTier: provider === "codex" ? runtimeCapabilities?.defaultServiceTier : undefined,
1593
- permissionModes: supportsPermission ? AGENT_PERMISSION_MODES : [],
1290
+ permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
1594
1291
  commands,
1595
- 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
+ ] : []),
1596
1296
  currentMode,
1597
1297
  features: {
1598
1298
  images: supportsImages,
@@ -1600,8 +1300,6 @@ export class AgentWorkspaceProxy {
1600
1300
  plan: enabled,
1601
1301
  cancel: enabled,
1602
1302
  reasoningEffort: supportsReasoningEffort,
1603
- serviceTier: provider === "codex" && Boolean(runtimeCapabilities?.speedTiers?.includes("fast")),
1604
- claudeSdk: isClaudeSdk,
1605
1303
  streamJsonFallback: isClaudeFallback,
1606
1304
  },
1607
1305
  };
@@ -1610,14 +1308,13 @@ export class AgentWorkspaceProxy {
1610
1308
  const anyPermission = providers.some((p) => p.supportsPermission);
1611
1309
  this.input.send(createEnvelope({
1612
1310
  type: "agent.v2.capabilities",
1613
- hostDeviceId: this.input.hostDeviceId,
1311
+ sessionId: this.input.sessionId,
1614
1312
  payload: {
1615
1313
  enabled: anyEnabled,
1616
1314
  provider: this.input.availableProviders[0] ?? "codex",
1617
1315
  providers,
1618
1316
  protocolVersion: 1,
1619
1317
  workspaceProtocolVersion: 2,
1620
- capabilitiesRevision: ++this.capabilitiesRevision,
1621
1318
  error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
1622
1319
  supportsSessionList: anyEnabled,
1623
1320
  supportsSessionLoad: anyEnabled,
@@ -1631,6 +1328,16 @@ export class AgentWorkspaceProxy {
1631
1328
  }
1632
1329
  async openConversation(payload) {
1633
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
+ }
1634
1341
  const cwd = payload.cwd ?? this.input.cwd;
1635
1342
  let agentSessionId = payload.agentSessionId;
1636
1343
  let existingConversation = (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
@@ -1639,34 +1346,17 @@ export class AgentWorkspaceProxy {
1639
1346
  if (payload.conversationId && existingConversation.id !== payload.conversationId) {
1640
1347
  existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
1641
1348
  }
1642
- this.hydrateStoredTimeline(existingConversation);
1643
1349
  this.activeConversationId = existingConversation.id;
1644
- const snapshot = this.latestSnapshot(existingConversation.id);
1645
1350
  this.input.send(createEnvelope({
1646
1351
  type: "agent.v2.conversation.opened",
1647
- hostDeviceId: this.input.hostDeviceId,
1352
+ sessionId: this.input.sessionId,
1648
1353
  payload: {
1649
- conversation: this.conversationSnapshot(existingConversation),
1650
- snapshot: snapshot.items,
1651
- revision: this.getRevision(existingConversation.id),
1652
- cursor: snapshot.cursor,
1653
- hasMore: snapshot.hasMore,
1654
- source: "device-history",
1655
- canonical: true,
1354
+ conversation: existingConversation,
1355
+ snapshot: this.timelines.get(existingConversation.id) ?? [],
1656
1356
  },
1657
1357
  }));
1658
1358
  return existingConversation;
1659
1359
  }
1660
- if (!provider) {
1661
- return this.openFailure(payload, "没有可用的 Agent provider。");
1662
- }
1663
- if (!this.input.availableProviders.includes(provider)) {
1664
- return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
1665
- }
1666
- const client = await this.ensureProviderClient(provider);
1667
- if (!client) {
1668
- return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
1669
- }
1670
1360
  try {
1671
1361
  const result = agentSessionId
1672
1362
  ? await client.loadSession({ sessionId: agentSessionId, cwd })
@@ -1683,16 +1373,10 @@ export class AgentWorkspaceProxy {
1683
1373
  title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
1684
1374
  model: payload.model ?? existingConversation?.model,
1685
1375
  reasoningEffort: payload.reasoningEffort ?? existingConversation?.reasoningEffort,
1686
- serviceTier: payload.serviceTier ?? existingConversation?.serviceTier,
1687
1376
  permissionMode: payload.permissionMode ?? existingConversation?.permissionMode,
1688
1377
  collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
1689
1378
  status: "idle",
1690
1379
  archived: existingConversation?.archived ?? false,
1691
- timelineRevision: existingConversation?.timelineRevision ?? this.getRevision(conversationId),
1692
- historyComplete: existingConversation?.historyComplete ?? false,
1693
- runningTurnId: this.currentTurnIds.get(conversationId),
1694
- source: "device",
1695
- canonical: true,
1696
1380
  lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
1697
1381
  lastActivityAt: now,
1698
1382
  createdAt: existingConversation?.createdAt ?? now,
@@ -1701,20 +1385,10 @@ export class AgentWorkspaceProxy {
1701
1385
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1702
1386
  this.activeConversationId = conversation.id;
1703
1387
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1704
- this.hydrateStoredTimeline(conversation);
1705
- const snapshot = this.latestSnapshot(conversation.id);
1706
1388
  this.input.send(createEnvelope({
1707
1389
  type: "agent.v2.conversation.opened",
1708
- hostDeviceId: this.input.hostDeviceId,
1709
- payload: {
1710
- conversation: this.conversationSnapshot(conversation),
1711
- snapshot: snapshot.items,
1712
- revision: this.getRevision(conversation.id),
1713
- cursor: snapshot.cursor,
1714
- hasMore: snapshot.hasMore,
1715
- source: "device-history",
1716
- canonical: true,
1717
- },
1390
+ sessionId: this.input.sessionId,
1391
+ payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
1718
1392
  }));
1719
1393
  return conversation;
1720
1394
  }
@@ -1737,10 +1411,6 @@ export class AgentWorkspaceProxy {
1737
1411
  collaborationMode: payload.collaborationMode,
1738
1412
  status: "error",
1739
1413
  archived: false,
1740
- timelineRevision: this.getRevision(fallbackId),
1741
- historyComplete: false,
1742
- source: "device",
1743
- canonical: true,
1744
1414
  lastMessagePreview: message,
1745
1415
  lastActivityAt: now,
1746
1416
  createdAt: now,
@@ -1756,15 +1426,8 @@ export class AgentWorkspaceProxy {
1756
1426
  });
1757
1427
  this.input.send(createEnvelope({
1758
1428
  type: "agent.v2.conversation.opened",
1759
- hostDeviceId: this.input.hostDeviceId,
1760
- payload: {
1761
- conversation: this.conversationSnapshot(conversation),
1762
- snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []),
1763
- revision: this.getRevision(conversation.id),
1764
- hasMore: false,
1765
- source: "device",
1766
- canonical: true,
1767
- },
1429
+ sessionId: this.input.sessionId,
1430
+ payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
1768
1431
  }));
1769
1432
  return conversation;
1770
1433
  }
@@ -1808,21 +1471,10 @@ export class AgentWorkspaceProxy {
1808
1471
  });
1809
1472
  return;
1810
1473
  }
1811
- if (Object.prototype.hasOwnProperty.call(payload, "model")) {
1812
- conversation.model = payload.model ?? undefined;
1813
- }
1814
- if (Object.prototype.hasOwnProperty.call(payload, "reasoningEffort")) {
1815
- conversation.reasoningEffort = payload.reasoningEffort ?? undefined;
1816
- }
1817
- if (Object.prototype.hasOwnProperty.call(payload, "serviceTier")) {
1818
- conversation.serviceTier = payload.serviceTier ?? undefined;
1819
- }
1820
- if (Object.prototype.hasOwnProperty.call(payload, "permissionMode")) {
1821
- conversation.permissionMode = payload.permissionMode ?? undefined;
1822
- }
1823
- if (Object.prototype.hasOwnProperty.call(payload, "collaborationMode")) {
1824
- conversation.collaborationMode = payload.collaborationMode ?? "default";
1825
- }
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;
1826
1478
  conversation.status = "running";
1827
1479
  conversation.lastActivityAt = Date.now();
1828
1480
  this.activeConversationId = conversation.id;
@@ -1842,11 +1494,10 @@ export class AgentWorkspaceProxy {
1842
1494
  sessionId: conversation.agentSessionId,
1843
1495
  content: payload.contentBlocks,
1844
1496
  clientMessageId: payload.clientMessageId,
1845
- model: conversation.model,
1846
- reasoningEffort: conversation.reasoningEffort,
1847
- serviceTier: conversation.serviceTier,
1848
- permissionMode: conversation.permissionMode,
1849
- collaborationMode: conversation.collaborationMode,
1497
+ model: payload.model,
1498
+ reasoningEffort: payload.reasoningEffort,
1499
+ permissionMode: payload.permissionMode,
1500
+ collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
1850
1501
  cwd: conversation.cwd,
1851
1502
  });
1852
1503
  const nextAgentSessionId = this.extractSessionId(result);
@@ -1926,7 +1577,6 @@ export class AgentWorkspaceProxy {
1926
1577
  contentBlocks: [{ type: "text", text: rawText }],
1927
1578
  model: conversation.model,
1928
1579
  reasoningEffort: conversation.reasoningEffort,
1929
- serviceTier: conversation.serviceTier,
1930
1580
  permissionMode: conversation.permissionMode,
1931
1581
  collaborationMode: conversation.collaborationMode,
1932
1582
  });
@@ -1957,8 +1607,14 @@ export class AgentWorkspaceProxy {
1957
1607
  this.emitStatus(conversation.id, conversation.status, `${providerLabel(conversation.provider)} · ${conversation.collaborationMode === "plan" ? "Plan mode" : "Default mode"} · ${conversation.cwd}`);
1958
1608
  return;
1959
1609
  }
1960
- if (isGitNativeCommand(command.name)) {
1961
- 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
+ });
1962
1618
  return;
1963
1619
  }
1964
1620
  if (command.name === "plan" || command.name === "exit-plan") {
@@ -1973,22 +1629,6 @@ export class AgentWorkspaceProxy {
1973
1629
  : "已退出 Plan mode。");
1974
1630
  return;
1975
1631
  }
1976
- if (command.name === "review" || command.name === "subagents") {
1977
- const prompt = command.name === "review"
1978
- ? args || "Review the current local changes."
1979
- : args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
1980
- await this.sendPrompt({
1981
- conversationId: conversation.id,
1982
- clientMessageId: id(command.name),
1983
- contentBlocks: [{ type: "text", text: prompt }],
1984
- model: conversation.model,
1985
- reasoningEffort: conversation.reasoningEffort,
1986
- serviceTier: conversation.serviceTier,
1987
- permissionMode: conversation.permissionMode,
1988
- collaborationMode: conversation.collaborationMode,
1989
- });
1990
- return;
1991
- }
1992
1632
  if (command.name === "compact") {
1993
1633
  if (!(client instanceof AcpClient))
1994
1634
  throw new Error("当前 Codex runtime 不支持原生 compact。");
@@ -2009,7 +1649,7 @@ export class AgentWorkspaceProxy {
2009
1649
  this.emitStatus(conversation.id, "idle", "上下文压缩完成。");
2010
1650
  return;
2011
1651
  }
2012
- if (command.name === "clear" || command.name === "new") {
1652
+ if (command.name === "clear") {
2013
1653
  if (!client)
2014
1654
  throw new Error("Agent provider 不在线。");
2015
1655
  const result = await client.newSession({ cwd: conversation.cwd });
@@ -2027,6 +1667,21 @@ export class AgentWorkspaceProxy {
2027
1667
  this.emitStatus(conversation.id, "idle", "上下文已重置,已创建新的 Codex thread。");
2028
1668
  return;
2029
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
+ }
2030
1685
  throw new Error(`命令暂未实现:/${command.name}`);
2031
1686
  }
2032
1687
  catch (error) {
@@ -2041,41 +1696,6 @@ export class AgentWorkspaceProxy {
2041
1696
  });
2042
1697
  }
2043
1698
  }
2044
- async executeGitNativeCommand(conversation, commandName, args) {
2045
- const git = gitCommandArgs(commandName, args);
2046
- const toolId = id(commandName);
2047
- const now = Date.now();
2048
- conversation.status = "running";
2049
- conversation.lastMessagePreview = git.display;
2050
- conversation.lastActivityAt = now;
2051
- this.emitConversation(conversation);
2052
- this.upsertTool(conversation.id, {
2053
- id: toolId,
2054
- name: "命令",
2055
- input: `${git.display}\n\ncwd: ${conversation.cwd}`,
2056
- createdAt: now,
2057
- status: "running",
2058
- });
2059
- const result = await runProcess("git", git.argv, {
2060
- cwd: conversation.cwd,
2061
- maxBytes: COMMAND_OUTPUT_MAX_BYTES,
2062
- });
2063
- const ok = result.exitCode === 0;
2064
- this.upsertTool(conversation.id, {
2065
- id: toolId,
2066
- name: "命令",
2067
- input: `${git.display}\n\ncwd: ${conversation.cwd}`,
2068
- output: result.output || (ok ? "完成" : `退出码 ${result.exitCode ?? "unknown"}`),
2069
- createdAt: now,
2070
- status: ok ? "completed" : "failed",
2071
- });
2072
- conversation.status = ok ? "idle" : "error";
2073
- conversation.lastMessagePreview = ok
2074
- ? `${git.display} 完成`
2075
- : `${git.display} 失败`;
2076
- conversation.lastActivityAt = Date.now();
2077
- this.emitConversation(conversation);
2078
- }
2079
1699
  handleRequest(method, params) {
2080
1700
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
2081
1701
  return this.handleStructuredInput(params, true);
@@ -2113,7 +1733,6 @@ export class AgentWorkspaceProxy {
2113
1733
  }
2114
1734
  if (method.startsWith("account/") ||
2115
1735
  method.startsWith("mcpServer/startupStatus/") ||
2116
- method.startsWith("remoteControl/") ||
2117
1736
  method === "thread/status/changed" ||
2118
1737
  method === "thread/tokenUsage/updated" ||
2119
1738
  method === "serverRequest/resolved" ||
@@ -2139,9 +1758,6 @@ export class AgentWorkspaceProxy {
2139
1758
  this.conversationByAgentSessionId.set(agentSessionId, conversationId);
2140
1759
  const conversation = this.conversations.get(conversationId);
2141
1760
  if (conversation) {
2142
- if (conversation.agentSessionId && conversation.agentSessionId !== agentSessionId) {
2143
- this.conversationByAgentSessionId.delete(conversation.agentSessionId);
2144
- }
2145
1761
  conversation.agentSessionId = agentSessionId;
2146
1762
  conversation.lastActivityAt = Date.now();
2147
1763
  this.emitConversation(conversation);
@@ -2720,7 +2336,7 @@ export class AgentWorkspaceProxy {
2720
2336
  this.upsertItem(conversationId, item);
2721
2337
  this.input.send(createEnvelope({
2722
2338
  type: "agent.v2.permission.request",
2723
- hostDeviceId: this.input.hostDeviceId,
2339
+ sessionId: this.input.sessionId,
2724
2340
  payload: { conversationId, ...permission, item },
2725
2341
  }));
2726
2342
  if (!waitForResponse)
@@ -2731,14 +2347,6 @@ export class AgentWorkspaceProxy {
2731
2347
  this.permissionWaiters.delete(requestId);
2732
2348
  this.permissionSources.delete(requestId);
2733
2349
  resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
2734
- this.markPermission(conversationId, requestId, {
2735
- permissionOutcome: "cancelled",
2736
- optionId: "cancelled",
2737
- permissionLive: false,
2738
- permissionPending: false,
2739
- permissionExpired: true,
2740
- permissionError: "等待授权超时",
2741
- });
2742
2350
  this.updateConversationStatus(conversationId, "idle");
2743
2351
  }, PERMISSION_TIMEOUT_MS);
2744
2352
  this.permissionWaiters.set(requestId, { resolve, timer });
@@ -2768,8 +2376,6 @@ export class AgentWorkspaceProxy {
2768
2376
  this.markPermission(payload.conversationId, payload.requestId, {
2769
2377
  permissionOutcome: payload.outcome,
2770
2378
  optionId: selectedOptionId,
2771
- permissionLive: false,
2772
- permissionExpired: false,
2773
2379
  permissionError: undefined,
2774
2380
  permissionPending: false,
2775
2381
  });
@@ -2816,209 +2422,6 @@ export class AgentWorkspaceProxy {
2816
2422
  updatedAt: Date.now(),
2817
2423
  });
2818
2424
  }
2819
- getRevision(conversationId) {
2820
- return this.conversationRevisions.get(conversationId) ??
2821
- this.conversations.get(conversationId)?.timelineRevision ??
2822
- 0;
2823
- }
2824
- setRevisionFloor(conversationId, revision) {
2825
- const nextRevision = Math.max(this.getRevision(conversationId), revision);
2826
- this.conversationRevisions.set(conversationId, nextRevision);
2827
- const conversation = this.conversations.get(conversationId);
2828
- if (conversation) {
2829
- conversation.timelineRevision = nextRevision;
2830
- conversation.runningTurnId = this.currentTurnIds.get(conversationId);
2831
- }
2832
- return nextRevision;
2833
- }
2834
- conversationSnapshot(conversation) {
2835
- return {
2836
- ...conversation,
2837
- timelineRevision: this.getRevision(conversation.id),
2838
- historyComplete: conversation.historyComplete ?? false,
2839
- runningTurnId: this.currentTurnIds.get(conversation.id),
2840
- source: conversation.source ?? "device",
2841
- canonical: conversation.canonical ?? true,
2842
- };
2843
- }
2844
- annotateTimelineItem(item, revision, source) {
2845
- return {
2846
- ...item,
2847
- revision: revision ?? item.revision,
2848
- source: item.source ?? source,
2849
- canonical: item.canonical ?? true,
2850
- };
2851
- }
2852
- recordRevisionEvent(conversationId, change) {
2853
- const revision = this.getRevision(conversationId) + 1;
2854
- this.conversationRevisions.set(conversationId, revision);
2855
- const conversation = this.conversations.get(conversationId);
2856
- if (conversation) {
2857
- conversation.timelineRevision = revision;
2858
- conversation.runningTurnId = this.currentTurnIds.get(conversationId);
2859
- }
2860
- const event = {
2861
- revision,
2862
- item: change.item ? this.annotateTimelineItem(change.item, revision, "device-live") : undefined,
2863
- conversation: change.conversation ? this.conversationSnapshot(change.conversation) : undefined,
2864
- };
2865
- const events = this.revisionEvents.get(conversationId) ?? [];
2866
- events.push(event);
2867
- if (events.length > MAX_DELTA_EVENTS) {
2868
- events.splice(0, events.length - MAX_DELTA_EVENTS);
2869
- }
2870
- this.revisionEvents.set(conversationId, events);
2871
- return event;
2872
- }
2873
- storedTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
2874
- if (!conversation.agentSessionId)
2875
- return [];
2876
- const result = conversation.provider === "codex"
2877
- ? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd, { maxItems })
2878
- : conversation.provider === "claude"
2879
- ? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id, { maxItems })
2880
- : { items: [] };
2881
- return result.items
2882
- .sort((a, b) => a.createdAt - b.createdAt)
2883
- .map((item, index) => this.annotateTimelineItem(item, index + 1, "device-history"));
2884
- }
2885
- canonicalTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
2886
- const merged = new Map();
2887
- for (const item of this.storedTimeline(conversation, maxItems)) {
2888
- merged.set(item.id, item);
2889
- }
2890
- for (const item of this.timelines.get(conversation.id) ?? []) {
2891
- merged.set(item.id, this.annotateTimelineItem(item, item.revision, item.source ?? "device-live"));
2892
- }
2893
- const items = [...merged.values()]
2894
- .sort((a, b) => a.createdAt - b.createdAt)
2895
- .slice(-maxItems);
2896
- this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
2897
- return items;
2898
- }
2899
- async appServerTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
2900
- if (conversation.provider !== "codex" || !conversation.agentSessionId)
2901
- return [];
2902
- if (this.protocolForProvider(conversation.provider) !== "codex-app-server")
2903
- return [];
2904
- const client = this.clientForProvider(conversation.provider);
2905
- const listTurns = client?.listTurns;
2906
- if (typeof listTurns !== "function")
2907
- return [];
2908
- try {
2909
- const result = await listTurns.call(client, { sessionId: conversation.agentSessionId, limit: maxItems });
2910
- return this.timelineFromAppServerTurns(conversation.id, conversation.agentSessionId, result);
2911
- }
2912
- catch (error) {
2913
- if (this.input.verbose) {
2914
- process.stderr.write(`[agent:v2] thread/turns/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
2915
- }
2916
- return [];
2917
- }
2918
- }
2919
- timelineFromAppServerTurns(conversationId, agentSessionId, value) {
2920
- const raw = asRecord(value);
2921
- const turns = Array.isArray(value) ? value :
2922
- Array.isArray(raw?.turns) ? raw.turns :
2923
- Array.isArray(raw?.items) ? raw.items :
2924
- Array.isArray(raw?.entries) ? raw.entries :
2925
- [];
2926
- const items = [];
2927
- turns.forEach((entry, index) => {
2928
- const turn = asRecord(entry);
2929
- if (!turn)
2930
- return;
2931
- const turnId = firstString(turn, ["id", "turnId", "turn_id"]) ?? `turn-${index + 1}`;
2932
- const createdAt = parseTimestamp(turn.createdAt ?? turn.created_at ?? turn.startedAt ?? turn.started_at) ?? Date.now() + index;
2933
- const updatedAt = parseTimestamp(turn.updatedAt ?? turn.updated_at ?? turn.completedAt ?? turn.completed_at);
2934
- const userText = appServerText(turn.input ?? turn.prompt ?? turn.user ?? turn.userMessage ?? turn.request);
2935
- if (userText) {
2936
- items.push({
2937
- id: `app-server:${agentSessionId}:${turnId}:user`,
2938
- conversationId,
2939
- type: "message",
2940
- kind: "chat",
2941
- turnId,
2942
- role: "user",
2943
- content: [{ type: "text", text: userText }],
2944
- text: userText,
2945
- createdAt,
2946
- updatedAt,
2947
- metadata: { source: "app-server", provider: "codex" },
2948
- });
2949
- }
2950
- const assistantText = appServerText(turn.output ?? turn.response ?? turn.assistant ?? turn.assistantMessage ?? turn.result);
2951
- if (assistantText) {
2952
- items.push({
2953
- id: `app-server:${agentSessionId}:${turnId}:assistant`,
2954
- conversationId,
2955
- type: "message",
2956
- kind: "chat",
2957
- turnId,
2958
- role: "assistant",
2959
- content: [{ type: "text", text: assistantText }],
2960
- text: assistantText,
2961
- createdAt: updatedAt ?? createdAt + 1,
2962
- updatedAt,
2963
- metadata: { source: "app-server", provider: "codex" },
2964
- });
2965
- }
2966
- const nestedItems = Array.isArray(turn.items) ? turn.items : Array.isArray(turn.messages) ? turn.messages : [];
2967
- for (const nested of nestedItems) {
2968
- const nestedRecord = asRecord(nested);
2969
- if (!nestedRecord)
2970
- continue;
2971
- const text = appServerText(nestedRecord.content ?? nestedRecord.text ?? nestedRecord.message);
2972
- const role = nestedRecord.role === "user" || nestedRecord.role === "assistant" || nestedRecord.role === "system"
2973
- ? nestedRecord.role
2974
- : undefined;
2975
- if (!text || !role)
2976
- continue;
2977
- const itemId = firstString(nestedRecord, ["id", "itemId", "messageId"]) ?? `${role}-${items.length + 1}`;
2978
- items.push({
2979
- id: `app-server:${agentSessionId}:${turnId}:${itemId}`,
2980
- conversationId,
2981
- type: "message",
2982
- kind: "chat",
2983
- turnId,
2984
- itemId,
2985
- role,
2986
- content: [{ type: "text", text }],
2987
- text,
2988
- createdAt: parseTimestamp(nestedRecord.createdAt ?? nestedRecord.created_at) ?? createdAt + items.length,
2989
- updatedAt: parseTimestamp(nestedRecord.updatedAt ?? nestedRecord.updated_at),
2990
- metadata: { source: "app-server", provider: "codex" },
2991
- });
2992
- }
2993
- });
2994
- return items
2995
- .sort((a, b) => a.createdAt - b.createdAt)
2996
- .map((item, index) => this.annotateTimelineItem(item, index + 1, "app-server"));
2997
- }
2998
- mergeCanonicalTimelineItems(items, maxItems) {
2999
- const byKey = new Map();
3000
- for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
3001
- const key = item.type === "message" && item.role && item.text
3002
- ? `${item.role}:${item.text.replace(/\s+/g, " ").trim().slice(0, 500)}`
3003
- : item.id;
3004
- const existing = byKey.get(key);
3005
- if (!existing || item.source === "app-server" || (item.updatedAt ?? item.createdAt) >= (existing.updatedAt ?? existing.createdAt)) {
3006
- byKey.set(key, item);
3007
- }
3008
- }
3009
- return [...byKey.values()]
3010
- .sort((a, b) => a.createdAt - b.createdAt)
3011
- .slice(-maxItems);
3012
- }
3013
- latestSnapshot(conversationId) {
3014
- const timeline = this.timelines.get(conversationId) ?? [];
3015
- const start = Math.max(0, timeline.length - MAX_SNAPSHOT_ITEMS);
3016
- return {
3017
- items: timeline.slice(start).map((item) => snapshotTimelineItem(item)),
3018
- cursor: start > 0 ? String(start) : undefined,
3019
- hasMore: start > 0,
3020
- };
3021
- }
3022
2425
  addItem(conversationId, item) {
3023
2426
  this.rememberItemConversationId(conversationId, item);
3024
2427
  const timeline = this.timelines.get(conversationId) ?? [];
@@ -3056,8 +2459,6 @@ export class AgentWorkspaceProxy {
3056
2459
  ...existing,
3057
2460
  ...toolCall,
3058
2461
  id: targetToolId,
3059
- input: toolCall.input ?? existing?.input,
3060
- output: toolCall.output ?? existing?.output,
3061
2462
  createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
3062
2463
  };
3063
2464
  this.toolConversationIds.set(toolCall.id, conversationId);
@@ -3126,7 +2527,6 @@ export class AgentWorkspaceProxy {
3126
2527
  conversation.title = conversation.title ?? target.title;
3127
2528
  conversation.model = conversation.model ?? target.model;
3128
2529
  conversation.reasoningEffort = conversation.reasoningEffort ?? target.reasoningEffort;
3129
- conversation.serviceTier = conversation.serviceTier ?? target.serviceTier;
3130
2530
  conversation.permissionMode = conversation.permissionMode ?? target.permissionMode;
3131
2531
  conversation.lastMessagePreview = conversation.lastMessagePreview ?? target.lastMessagePreview;
3132
2532
  conversation.createdAt = Math.min(conversation.createdAt, target.createdAt);
@@ -3139,17 +2539,6 @@ export class AgentWorkspaceProxy {
3139
2539
  this.timelines.set(newId, [...mergedTimeline.values()]
3140
2540
  .sort((a, b) => a.createdAt - b.createdAt)
3141
2541
  .slice(-MAX_TIMELINE_ITEMS));
3142
- const oldRevision = this.conversationRevisions.get(oldId) ?? 0;
3143
- if (oldRevision > 0) {
3144
- this.conversationRevisions.delete(oldId);
3145
- this.setRevisionFloor(newId, oldRevision);
3146
- }
3147
- const oldEvents = this.revisionEvents.get(oldId);
3148
- if (oldEvents) {
3149
- this.revisionEvents.delete(oldId);
3150
- const existingEvents = this.revisionEvents.get(newId) ?? [];
3151
- this.revisionEvents.set(newId, [...existingEvents, ...oldEvents].slice(-MAX_DELTA_EVENTS));
3152
- }
3153
2542
  for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
3154
2543
  if (conversationId === oldId) {
3155
2544
  this.conversationByAgentSessionId.set(agentSessionId, newId);
@@ -3183,47 +2572,17 @@ export class AgentWorkspaceProxy {
3183
2572
  }
3184
2573
  emitItem(conversationId, item) {
3185
2574
  const conversation = this.conversations.get(conversationId);
3186
- const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
3187
- const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
3188
2575
  this.input.send(createEnvelope({
3189
2576
  type: "agent.v2.event",
3190
- hostDeviceId: this.input.hostDeviceId,
3191
- payload: {
3192
- conversationId,
3193
- conversation: event.conversation,
3194
- item: event.item,
3195
- revision: event.revision,
3196
- source: "device-live",
3197
- canonical: true,
3198
- },
2577
+ sessionId: this.input.sessionId,
2578
+ payload: { conversationId, conversation, item },
3199
2579
  }));
3200
2580
  }
3201
2581
  emitConversation(conversation) {
3202
- const event = this.recordRevisionEvent(conversation.id, { conversation });
3203
2582
  this.input.send(createEnvelope({
3204
2583
  type: "agent.v2.event",
3205
- hostDeviceId: this.input.hostDeviceId,
3206
- payload: {
3207
- conversationId: conversation.id,
3208
- conversation: event.conversation,
3209
- revision: event.revision,
3210
- source: "device-live",
3211
- canonical: true,
3212
- },
3213
- }));
3214
- this.input.send(createEnvelope({
3215
- type: "agent.v2.running_state",
3216
- hostDeviceId: this.input.hostDeviceId,
3217
- payload: {
3218
- conversationId: conversation.id,
3219
- status: conversation.status,
3220
- runningTurnId: this.currentTurnIds.get(conversation.id),
3221
- revision: event.revision,
3222
- error: conversation.status === "error" ? conversation.lastMessagePreview : undefined,
3223
- updatedAt: conversation.lastActivityAt,
3224
- source: "device-live",
3225
- canonical: true,
3226
- },
2584
+ sessionId: this.input.sessionId,
2585
+ payload: { conversationId: conversation.id, conversation },
3227
2586
  }));
3228
2587
  }
3229
2588
  emitStatus(conversationId, status, text) {
@@ -3257,169 +2616,20 @@ export class AgentWorkspaceProxy {
3257
2616
  this.emitConversation(conversation);
3258
2617
  }
3259
2618
  sendSnapshot(conversationId) {
3260
- if (conversationId) {
3261
- const conversation = this.conversations.get(conversationId);
3262
- if (conversation)
3263
- this.hydrateStoredTimeline(conversation);
3264
- }
3265
- else if (this.activeConversationId) {
3266
- const conversation = this.conversations.get(this.activeConversationId);
3267
- if (conversation)
3268
- this.hydrateStoredTimeline(conversation);
3269
- }
3270
- const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
3271
- 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();
3272
2623
  this.input.send(createEnvelope({
3273
2624
  type: "agent.v2.snapshot",
3274
- hostDeviceId: this.input.hostDeviceId,
2625
+ sessionId: this.input.sessionId,
3275
2626
  payload: {
3276
2627
  conversations,
3277
2628
  activeConversationId: this.activeConversationId,
3278
- items: snapshot.items,
3279
- revision: conversationId ? this.getRevision(conversationId) : undefined,
3280
- cursor: snapshot.cursor,
3281
- hasMore: snapshot.hasMore,
3282
- source: "device-history",
3283
- canonical: true,
3284
- },
3285
- }));
3286
- }
3287
- async sendHistoryPage(payload) {
3288
- const conversation = this.conversations.get(payload.conversationId);
3289
- if (!conversation)
3290
- return;
3291
- const limit = Math.min(Math.max(payload.limit ?? MAX_SNAPSHOT_ITEMS, 1), 200);
3292
- const items = this.mergeCanonicalTimelineItems([
3293
- ...this.canonicalTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
3294
- ...await this.appServerTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
3295
- ], HISTORY_PAGE_MAX_ITEMS);
3296
- this.timelines.set(conversation.id, items.slice(-MAX_TIMELINE_ITEMS).map((item) => this.annotateTimelineItem(item, item.revision, item.source ?? "device-history")));
3297
- for (const item of this.timelines.get(conversation.id) ?? []) {
3298
- this.rememberItemConversationId(conversation.id, item);
3299
- }
3300
- const direction = payload.direction ?? "older";
3301
- let page;
3302
- let cursor;
3303
- let hasMore = false;
3304
- if (direction === "newer") {
3305
- const start = clampHistoryCursor(payload.cursor, 0, items.length);
3306
- const end = Math.min(items.length, start + limit);
3307
- page = items.slice(start, end);
3308
- hasMore = end < items.length;
3309
- cursor = hasMore ? String(end) : undefined;
3310
- }
3311
- else {
3312
- const end = clampHistoryCursor(payload.cursor, items.length, items.length);
3313
- const start = Math.max(0, end - limit);
3314
- page = items.slice(start, end);
3315
- hasMore = start > 0;
3316
- cursor = hasMore ? String(start) : undefined;
3317
- }
3318
- conversation.historyComplete = !hasMore;
3319
- const revision = this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
3320
- this.input.send(createEnvelope({
3321
- type: "agent.v2.history.page",
3322
- hostDeviceId: this.input.hostDeviceId,
3323
- payload: {
3324
- conversationId: conversation.id,
3325
- conversation: this.conversationSnapshot(conversation),
3326
- items: page.map((item) => snapshotTimelineItem(item)),
3327
- revision,
3328
- cursor,
3329
- hasMore,
3330
- source: "device-history",
3331
- canonical: true,
3332
- },
3333
- }));
3334
- }
3335
- sendDelta(payload) {
3336
- const conversation = this.conversations.get(payload.conversationId);
3337
- if (!conversation)
3338
- return;
3339
- this.hydrateStoredTimeline(conversation);
3340
- const sinceRevision = payload.sinceRevision ?? 0;
3341
- const limit = Math.min(Math.max(payload.limit ?? 100, 1), 500);
3342
- const events = this.revisionEvents.get(conversation.id) ?? [];
3343
- const oldestAvailable = events[0]?.revision ?? this.getRevision(conversation.id);
3344
- const newestRevision = this.getRevision(conversation.id);
3345
- const reset = sinceRevision > 0 &&
3346
- (sinceRevision > newestRevision ||
3347
- (sinceRevision < newestRevision &&
3348
- (events.length === 0 || sinceRevision < oldestAvailable - 1)));
3349
- if (reset) {
3350
- const snapshot = this.latestSnapshot(conversation.id);
3351
- this.input.send(createEnvelope({
3352
- type: "agent.v2.delta",
3353
- hostDeviceId: this.input.hostDeviceId,
3354
- payload: {
3355
- conversationId: conversation.id,
3356
- conversation: this.conversationSnapshot(conversation),
3357
- items: snapshot.items,
3358
- sinceRevision,
3359
- revision: newestRevision,
3360
- reset: true,
3361
- cursor: snapshot.cursor,
3362
- hasMore: snapshot.hasMore,
3363
- source: "device-history",
3364
- canonical: true,
3365
- },
3366
- }));
3367
- return;
3368
- }
3369
- const changed = events
3370
- .filter((event) => event.revision > sinceRevision)
3371
- .slice(-limit);
3372
- const itemsById = new Map();
3373
- for (const event of changed) {
3374
- if (event.item)
3375
- itemsById.set(event.item.id, event.item);
3376
- }
3377
- this.input.send(createEnvelope({
3378
- type: "agent.v2.delta",
3379
- hostDeviceId: this.input.hostDeviceId,
3380
- payload: {
3381
- conversationId: conversation.id,
3382
- conversation: this.conversationSnapshot(conversation),
3383
- items: [...itemsById.values()].map((item) => snapshotTimelineItem(item)),
3384
- sinceRevision,
3385
- revision: newestRevision,
3386
- reset: false,
3387
- hasMore: changed.length === limit && events.some((event) => event.revision > sinceRevision && event.revision < changed[0].revision),
3388
- source: "device-live",
3389
- canonical: true,
2629
+ items,
3390
2630
  },
3391
2631
  }));
3392
2632
  }
3393
- hydrateStoredTimeline(conversation) {
3394
- if (!conversation.agentSessionId)
3395
- return;
3396
- const existing = this.timelines.get(conversation.id) ?? [];
3397
- if (existing.length > 0) {
3398
- this.setRevisionFloor(conversation.id, Math.max(existing.length, ...existing.map((item) => item.revision ?? 0)));
3399
- return;
3400
- }
3401
- const stored = this.storedTimeline(conversation, MAX_TIMELINE_ITEMS);
3402
- conversation.historyComplete = stored.length <= MAX_SNAPSHOT_ITEMS;
3403
- if (stored.length === 0) {
3404
- this.setRevisionFloor(conversation.id, this.getRevision(conversation.id));
3405
- return;
3406
- }
3407
- const items = stored
3408
- .sort((a, b) => a.createdAt - b.createdAt)
3409
- .slice(-MAX_TIMELINE_ITEMS);
3410
- this.timelines.set(conversation.id, items);
3411
- for (const item of items)
3412
- this.rememberItemConversationId(conversation.id, item);
3413
- this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
3414
- const lastMessage = [...items].reverse().find((item) => item.text?.trim());
3415
- if (lastMessage?.text && !conversation.lastMessagePreview) {
3416
- conversation.lastMessagePreview = previewText(lastMessage.text);
3417
- }
3418
- const lastActivityAt = items.at(-1)?.createdAt;
3419
- if (lastActivityAt) {
3420
- conversation.lastActivityAt = Math.max(conversation.lastActivityAt, lastActivityAt);
3421
- }
3422
- }
3423
2633
  conversationIdFromParams(params) {
3424
2634
  const raw = asRecord(params);
3425
2635
  if (!raw)
@@ -3479,9 +2689,6 @@ export class AgentWorkspaceProxy {
3479
2689
  rememberTurnConversationId(conversationId, turnId) {
3480
2690
  this.currentTurnIds.set(conversationId, turnId);
3481
2691
  this.turnConversationIds.set(turnId, conversationId);
3482
- const conversation = this.conversations.get(conversationId);
3483
- if (conversation)
3484
- conversation.runningTurnId = turnId;
3485
2692
  }
3486
2693
  forgetCurrentTurn(conversationId, turnId) {
3487
2694
  const currentTurnId = this.currentTurnIds.get(conversationId);
@@ -3490,9 +2697,6 @@ export class AgentWorkspaceProxy {
3490
2697
  this.turnConversationIds.delete(turnId);
3491
2698
  if (currentTurnId && currentTurnId !== turnId)
3492
2699
  this.turnConversationIds.delete(currentTurnId);
3493
- const conversation = this.conversations.get(conversationId);
3494
- if (conversation)
3495
- conversation.runningTurnId = undefined;
3496
2700
  }
3497
2701
  rememberItemConversationId(conversationId, item) {
3498
2702
  const keys = [
@@ -3532,51 +2736,28 @@ export class AgentWorkspaceProxy {
3532
2736
  }
3533
2737
  cancelPendingPermissions(conversationId) {
3534
2738
  for (const [requestId, waiter] of this.permissionWaiters) {
3535
- const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
3536
- if (conversationId && ownerConversationId && ownerConversationId !== conversationId)
3537
- continue;
3538
2739
  clearTimeout(waiter.timer);
3539
2740
  waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
3540
- if (ownerConversationId) {
3541
- this.markPermission(ownerConversationId, requestId, {
3542
- permissionOutcome: "cancelled",
3543
- optionId: "cancelled",
3544
- permissionLive: false,
3545
- permissionPending: false,
3546
- permissionExpired: false,
3547
- permissionError: "已停止",
3548
- });
3549
- }
3550
- this.permissionWaiters.delete(requestId);
3551
2741
  this.pendingPermissions.delete(requestId);
3552
2742
  this.permissionSources.delete(requestId);
3553
2743
  }
2744
+ this.permissionWaiters.clear();
3554
2745
  for (const [requestId, waiter] of this.structuredInputWaiters) {
3555
- const pending = this.pendingStructuredInputs.get(requestId);
3556
- if (conversationId && pending?.conversationId && pending.conversationId !== conversationId)
3557
- continue;
3558
2746
  clearTimeout(waiter.timer);
3559
2747
  waiter.resolve(formatStructuredInputResponse({}));
2748
+ const pending = this.pendingStructuredInputs.get(requestId);
3560
2749
  if (pending) {
3561
2750
  this.markStructuredInput(pending.conversationId, requestId, {
3562
2751
  inputPending: false,
3563
2752
  inputError: "已停止",
3564
2753
  });
3565
2754
  }
3566
- this.structuredInputWaiters.delete(requestId);
3567
2755
  this.pendingStructuredInputs.delete(requestId);
3568
2756
  }
2757
+ this.structuredInputWaiters.clear();
3569
2758
  if (conversationId)
3570
2759
  this.updateConversationStatus(conversationId, "idle");
3571
2760
  }
3572
- conversationIdForPermissionRequest(requestId) {
3573
- for (const [conversationId, timeline] of this.timelines) {
3574
- if (timeline.some((item) => item.type === "permission" && item.permission?.requestId === requestId)) {
3575
- return conversationId;
3576
- }
3577
- }
3578
- return undefined;
3579
- }
3580
2761
  extractSessionId(value) {
3581
2762
  const raw = asRecord(value);
3582
2763
  if (!raw)