linkshell-cli 0.3.12 → 0.3.14

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.
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
2
3
  import { homedir } from "node:os";
3
4
  import { basename, join, relative } from "node:path";
4
5
  import { createEnvelope, parseTypedPayload, } from "@linkshell/protocol";
@@ -12,9 +13,43 @@ const PERMISSION_TIMEOUT_MS = 5 * 60_000;
12
13
  const MAX_TIMELINE_ITEMS = 200;
13
14
  const MAX_SNAPSHOT_ITEMS = 80;
14
15
  const MAX_SNAPSHOT_TEXT_BYTES = 128 * 1024;
16
+ const MAX_DELTA_EVENTS = 500;
17
+ const HISTORY_PAGE_MAX_ITEMS = 500;
15
18
  function id(prefix) {
16
19
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
17
20
  }
21
+ function clampHistoryCursor(value, fallback, max) {
22
+ if (!value)
23
+ return fallback;
24
+ const parsed = Number.parseInt(value, 10);
25
+ if (!Number.isFinite(parsed))
26
+ return fallback;
27
+ return Math.max(0, Math.min(max, parsed));
28
+ }
29
+ function appServerText(value) {
30
+ if (typeof value === "string") {
31
+ const text = value.trim();
32
+ return text || undefined;
33
+ }
34
+ if (Array.isArray(value)) {
35
+ const text = value
36
+ .map((part) => appServerText(part))
37
+ .filter(Boolean)
38
+ .join("\n")
39
+ .trim();
40
+ return text || undefined;
41
+ }
42
+ const record = asRecord(value);
43
+ if (!record)
44
+ return undefined;
45
+ if (typeof record.text === "string")
46
+ return appServerText(record.text);
47
+ if (typeof record.content === "string")
48
+ return appServerText(record.content);
49
+ if (typeof record.message === "string")
50
+ return appServerText(record.message);
51
+ return appServerText(record.content ?? record.message ?? record.parts);
52
+ }
18
53
  function stringify(value) {
19
54
  if (typeof value === "string")
20
55
  return value;
@@ -616,6 +651,23 @@ function providerLabel(provider) {
616
651
  const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"];
617
652
  const CLAUDE_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
618
653
  const AGENT_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"];
654
+ const COMMAND_OUTPUT_MAX_BYTES = 96 * 1024;
655
+ const LINKSHELL_NATIVE_COMMANDS = [
656
+ { name: "status", description: "Show current Agent and workspace status", category: "LinkShell", argsMode: "none" },
657
+ { name: "plan", description: "Enter Plan mode for the next turn", category: "Agent", argsMode: "none" },
658
+ { name: "exit-plan", description: "Exit Plan mode", category: "Agent", argsMode: "none" },
659
+ { name: "review", description: "Ask the Agent to review current local changes", category: "Agent", argsMode: "optional" },
660
+ { name: "subagents", description: "Ask the Agent to split work across subagents when useful", category: "Agent", argsMode: "optional" },
661
+ { name: "compact", description: "Compact the active Codex context", category: "Codex", argsMode: "none", providers: ["codex"] },
662
+ { name: "clear", description: "Start a fresh Agent context for this conversation", category: "Agent", argsMode: "none", destructive: true },
663
+ { name: "git-status", description: "Show branch and working tree status", category: "Git", argsMode: "none" },
664
+ { name: "git-diff", description: "Show a compact diffstat for current changes", category: "Git", argsMode: "none" },
665
+ { name: "git-commit", description: "Commit staged changes with the given message", category: "Git", argsMode: "required" },
666
+ { name: "git-pull", description: "Pull with fast-forward only", category: "Git", argsMode: "none" },
667
+ { name: "git-push", description: "Push the current branch", category: "Git", argsMode: "none" },
668
+ { name: "git-stash", description: "Stash current working tree changes", category: "Git", argsMode: "optional" },
669
+ { name: "git-stash-pop", description: "Pop the latest stash", category: "Git", argsMode: "none" },
670
+ ];
619
671
  const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
620
672
  "add-dir",
621
673
  "agents",
@@ -877,17 +929,30 @@ function customClaudeCommands(cwd) {
877
929
  }
878
930
  function defaultProviderCommands(provider, cwd, enabled) {
879
931
  const disabledReason = enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`;
932
+ const linkshellCommands = LINKSHELL_NATIVE_COMMANDS
933
+ .filter((command) => !command.providers || command.providers.includes(provider))
934
+ .map((command) => makeCommand({
935
+ provider,
936
+ name: command.name,
937
+ description: command.description,
938
+ source: "linkshell",
939
+ category: command.category,
940
+ argsMode: command.argsMode ?? "optional",
941
+ destructive: command.destructive,
942
+ disabledReason,
943
+ executionKind: "native",
944
+ }));
880
945
  if (provider === "codex") {
881
- return [];
946
+ return linkshellCommands;
882
947
  }
883
948
  if (provider === "claude") {
884
949
  const custom = customClaudeCommands(cwd).map((command) => ({
885
950
  ...command,
886
951
  disabledReason: command.disabledReason ?? disabledReason,
887
952
  }));
888
- return custom;
953
+ return [...linkshellCommands, ...custom];
889
954
  }
890
- return [];
955
+ return linkshellCommands;
891
956
  }
892
957
  function mergeCommands(...groups) {
893
958
  const map = new Map();
@@ -904,6 +969,76 @@ function mergeCommands(...groups) {
904
969
  }
905
970
  return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
906
971
  }
972
+ function isGitNativeCommand(name) {
973
+ return name === "git-status" ||
974
+ name === "git-diff" ||
975
+ name === "git-commit" ||
976
+ name === "git-pull" ||
977
+ name === "git-push" ||
978
+ name === "git-stash" ||
979
+ name === "git-stash-pop";
980
+ }
981
+ function gitCommandArgs(name, args) {
982
+ const message = args?.trim();
983
+ switch (name) {
984
+ case "git-status":
985
+ return { display: "git status --short --branch", argv: ["status", "--short", "--branch"] };
986
+ case "git-diff":
987
+ return { display: "git diff --stat", argv: ["diff", "--stat"] };
988
+ case "git-commit":
989
+ if (!message)
990
+ throw new Error("请先输入提交信息,例如 /git-commit fix mobile agent timeline");
991
+ return { display: `git commit -m ${JSON.stringify(message)}`, argv: ["commit", "-m", message] };
992
+ case "git-pull":
993
+ return { display: "git pull --ff-only", argv: ["pull", "--ff-only"] };
994
+ case "git-push":
995
+ return { display: "git push", argv: ["push"] };
996
+ case "git-stash":
997
+ return {
998
+ display: "git stash push -u",
999
+ argv: ["stash", "push", "-u", "-m", message || "LinkShell mobile stash"],
1000
+ };
1001
+ case "git-stash-pop":
1002
+ return { display: "git stash pop", argv: ["stash", "pop"] };
1003
+ default:
1004
+ throw new Error(`未知 Git 命令:/${name}`);
1005
+ }
1006
+ }
1007
+ function runProcess(command, args, options) {
1008
+ return new Promise((resolve, reject) => {
1009
+ const child = spawn(command, args, {
1010
+ cwd: options.cwd,
1011
+ stdio: ["ignore", "pipe", "pipe"],
1012
+ windowsHide: true,
1013
+ });
1014
+ const maxBytes = options.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES;
1015
+ let output = "";
1016
+ let bytes = 0;
1017
+ let truncated = false;
1018
+ const append = (chunk) => {
1019
+ if (bytes >= maxBytes) {
1020
+ truncated = true;
1021
+ return;
1022
+ }
1023
+ const remaining = maxBytes - bytes;
1024
+ const slice = chunk.byteLength > remaining ? chunk.subarray(0, remaining) : chunk;
1025
+ output += slice.toString("utf8");
1026
+ bytes += slice.byteLength;
1027
+ if (slice.byteLength < chunk.byteLength)
1028
+ truncated = true;
1029
+ };
1030
+ child.stdout.on("data", append);
1031
+ child.stderr.on("data", append);
1032
+ child.once("error", reject);
1033
+ child.once("close", (exitCode, signal) => {
1034
+ resolve({
1035
+ output: `${output.trimEnd()}${truncated ? "\n\n[truncated by LinkShell]" : ""}`,
1036
+ exitCode,
1037
+ signal,
1038
+ });
1039
+ });
1040
+ });
1041
+ }
907
1042
  function runtimeCommands(provider, value) {
908
1043
  const raw = asRecord(value);
909
1044
  const commandsValue = Array.isArray(value) ? value :
@@ -1018,10 +1153,26 @@ function parseRemoteSessions(value) {
1018
1153
  createdAt: parseTimestamp(source.createdAt ?? source.created_at),
1019
1154
  lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
1020
1155
  archived: typeof source.archived === "boolean" ? source.archived : undefined,
1156
+ status: normalizeAgentStatus(firstString(source, ["status", "state", "phase"])),
1157
+ runningTurnId: firstString(source, ["runningTurnId", "running_turn_id", "turnId", "activeTurnId"]),
1021
1158
  });
1022
1159
  }
1023
1160
  return result;
1024
1161
  }
1162
+ function normalizeAgentStatus(value) {
1163
+ if (!value)
1164
+ return undefined;
1165
+ const normalized = value.toLowerCase();
1166
+ if (normalized === "running" || normalized === "in_progress" || normalized === "busy")
1167
+ return "running";
1168
+ if (normalized === "waiting_permission" || normalized === "waiting" || normalized === "blocked")
1169
+ return "waiting_permission";
1170
+ if (normalized === "error" || normalized === "failed")
1171
+ return "error";
1172
+ if (normalized === "idle" || normalized === "completed" || normalized === "done")
1173
+ return "idle";
1174
+ return undefined;
1175
+ }
1025
1176
  export class AgentWorkspaceProxy {
1026
1177
  input;
1027
1178
  clients = new Map();
@@ -1036,6 +1187,8 @@ export class AgentWorkspaceProxy {
1036
1187
  conversations = new Map();
1037
1188
  conversationByAgentSessionId = new Map();
1038
1189
  timelines = new Map();
1190
+ conversationRevisions = new Map();
1191
+ revisionEvents = new Map();
1039
1192
  toolOutputBuffers = new Map();
1040
1193
  pendingPermissions = new Map();
1041
1194
  permissionWaiters = new Map();
@@ -1065,7 +1218,7 @@ export class AgentWorkspaceProxy {
1065
1218
  this.input.send(createEnvelope({
1066
1219
  type: "agent.v2.conversation.list.result",
1067
1220
  hostDeviceId: this.input.hostDeviceId,
1068
- payload: { conversations },
1221
+ payload: { conversations: conversations.map((conversation) => this.conversationSnapshot(conversation)) },
1069
1222
  }));
1070
1223
  break;
1071
1224
  }
@@ -1074,6 +1227,16 @@ export class AgentWorkspaceProxy {
1074
1227
  this.sendSnapshot(payload.conversationId);
1075
1228
  break;
1076
1229
  }
1230
+ case "agent.v2.history.request": {
1231
+ const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
1232
+ await this.sendHistoryPage(payload);
1233
+ break;
1234
+ }
1235
+ case "agent.v2.delta.request": {
1236
+ const payload = parseTypedPayload("agent.v2.delta.request", envelope.payload);
1237
+ this.sendDelta(payload);
1238
+ break;
1239
+ }
1077
1240
  case "agent.v2.prompt": {
1078
1241
  const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
1079
1242
  await this.sendPrompt(payload);
@@ -1276,14 +1439,21 @@ export class AgentWorkspaceProxy {
1276
1439
  reasoningEffort: existing?.reasoningEffort,
1277
1440
  permissionMode: existing?.permissionMode,
1278
1441
  collaborationMode: existing?.collaborationMode,
1279
- status: existing?.status ?? "idle",
1442
+ status: remote.status ?? existing?.status ?? "idle",
1280
1443
  archived: remote.archived ?? existing?.archived ?? false,
1444
+ timelineRevision: existing?.timelineRevision ?? this.getRevision(conversationId),
1445
+ historyComplete: existing?.historyComplete ?? false,
1446
+ runningTurnId: remote.runningTurnId ?? this.currentTurnIds.get(conversationId),
1447
+ source: "device",
1448
+ canonical: true,
1281
1449
  lastMessagePreview: existing?.lastMessagePreview,
1282
1450
  lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
1283
1451
  createdAt: remote.createdAt ?? existing?.createdAt ?? now,
1284
1452
  };
1285
1453
  this.conversations.set(conversation.id, conversation);
1286
1454
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1455
+ if (remote.runningTurnId)
1456
+ this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
1287
1457
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1288
1458
  }
1289
1459
  }
@@ -1361,12 +1531,18 @@ export class AgentWorkspaceProxy {
1361
1531
  }
1362
1532
  this.hydrateStoredTimeline(existingConversation);
1363
1533
  this.activeConversationId = existingConversation.id;
1534
+ const snapshot = this.latestSnapshot(existingConversation.id);
1364
1535
  this.input.send(createEnvelope({
1365
1536
  type: "agent.v2.conversation.opened",
1366
1537
  hostDeviceId: this.input.hostDeviceId,
1367
1538
  payload: {
1368
- conversation: existingConversation,
1369
- snapshot: snapshotTimelineItems(this.timelines.get(existingConversation.id) ?? []),
1539
+ conversation: this.conversationSnapshot(existingConversation),
1540
+ snapshot: snapshot.items,
1541
+ revision: this.getRevision(existingConversation.id),
1542
+ cursor: snapshot.cursor,
1543
+ hasMore: snapshot.hasMore,
1544
+ source: "device-history",
1545
+ canonical: true,
1370
1546
  },
1371
1547
  }));
1372
1548
  return existingConversation;
@@ -1401,6 +1577,11 @@ export class AgentWorkspaceProxy {
1401
1577
  collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
1402
1578
  status: "idle",
1403
1579
  archived: existingConversation?.archived ?? false,
1580
+ timelineRevision: existingConversation?.timelineRevision ?? this.getRevision(conversationId),
1581
+ historyComplete: existingConversation?.historyComplete ?? false,
1582
+ runningTurnId: this.currentTurnIds.get(conversationId),
1583
+ source: "device",
1584
+ canonical: true,
1404
1585
  lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
1405
1586
  lastActivityAt: now,
1406
1587
  createdAt: existingConversation?.createdAt ?? now,
@@ -1410,10 +1591,19 @@ export class AgentWorkspaceProxy {
1410
1591
  this.activeConversationId = conversation.id;
1411
1592
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1412
1593
  this.hydrateStoredTimeline(conversation);
1594
+ const snapshot = this.latestSnapshot(conversation.id);
1413
1595
  this.input.send(createEnvelope({
1414
1596
  type: "agent.v2.conversation.opened",
1415
1597
  hostDeviceId: this.input.hostDeviceId,
1416
- payload: { conversation, snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []) },
1598
+ payload: {
1599
+ conversation: this.conversationSnapshot(conversation),
1600
+ snapshot: snapshot.items,
1601
+ revision: this.getRevision(conversation.id),
1602
+ cursor: snapshot.cursor,
1603
+ hasMore: snapshot.hasMore,
1604
+ source: "device-history",
1605
+ canonical: true,
1606
+ },
1417
1607
  }));
1418
1608
  return conversation;
1419
1609
  }
@@ -1436,6 +1626,10 @@ export class AgentWorkspaceProxy {
1436
1626
  collaborationMode: payload.collaborationMode,
1437
1627
  status: "error",
1438
1628
  archived: false,
1629
+ timelineRevision: this.getRevision(fallbackId),
1630
+ historyComplete: false,
1631
+ source: "device",
1632
+ canonical: true,
1439
1633
  lastMessagePreview: message,
1440
1634
  lastActivityAt: now,
1441
1635
  createdAt: now,
@@ -1452,7 +1646,14 @@ export class AgentWorkspaceProxy {
1452
1646
  this.input.send(createEnvelope({
1453
1647
  type: "agent.v2.conversation.opened",
1454
1648
  hostDeviceId: this.input.hostDeviceId,
1455
- payload: { conversation, snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []) },
1649
+ payload: {
1650
+ conversation: this.conversationSnapshot(conversation),
1651
+ snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []),
1652
+ revision: this.getRevision(conversation.id),
1653
+ hasMore: false,
1654
+ source: "device",
1655
+ canonical: true,
1656
+ },
1456
1657
  }));
1457
1658
  return conversation;
1458
1659
  }
@@ -1640,14 +1841,8 @@ export class AgentWorkspaceProxy {
1640
1841
  this.emitStatus(conversation.id, conversation.status, `${providerLabel(conversation.provider)} · ${conversation.collaborationMode === "plan" ? "Plan mode" : "Default mode"} · ${conversation.cwd}`);
1641
1842
  return;
1642
1843
  }
1643
- if (conversation.provider !== "codex") {
1644
- this.addItem(conversation.id, {
1645
- id: id("error"),
1646
- conversationId: conversation.id,
1647
- type: "error",
1648
- error: `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`,
1649
- createdAt: now,
1650
- });
1844
+ if (isGitNativeCommand(command.name)) {
1845
+ await this.executeGitNativeCommand(conversation, command.name, args);
1651
1846
  return;
1652
1847
  }
1653
1848
  if (command.name === "plan" || command.name === "exit-plan") {
@@ -1662,6 +1857,21 @@ export class AgentWorkspaceProxy {
1662
1857
  : "已退出 Plan mode。");
1663
1858
  return;
1664
1859
  }
1860
+ if (command.name === "review" || command.name === "subagents") {
1861
+ const prompt = command.name === "review"
1862
+ ? args || "Review the current local changes."
1863
+ : args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
1864
+ await this.sendPrompt({
1865
+ conversationId: conversation.id,
1866
+ clientMessageId: id(command.name),
1867
+ contentBlocks: [{ type: "text", text: prompt }],
1868
+ model: conversation.model,
1869
+ reasoningEffort: conversation.reasoningEffort,
1870
+ permissionMode: conversation.permissionMode,
1871
+ collaborationMode: conversation.collaborationMode,
1872
+ });
1873
+ return;
1874
+ }
1665
1875
  if (command.name === "compact") {
1666
1876
  if (!(client instanceof AcpClient))
1667
1877
  throw new Error("当前 Codex runtime 不支持原生 compact。");
@@ -1700,21 +1910,6 @@ export class AgentWorkspaceProxy {
1700
1910
  this.emitStatus(conversation.id, "idle", "上下文已重置,已创建新的 Codex thread。");
1701
1911
  return;
1702
1912
  }
1703
- if (command.name === "review" || command.name === "subagents") {
1704
- const prompt = command.name === "review"
1705
- ? args || "Review the current local changes."
1706
- : args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
1707
- await this.sendPrompt({
1708
- conversationId: conversation.id,
1709
- clientMessageId: id(command.name),
1710
- contentBlocks: [{ type: "text", text: prompt }],
1711
- model: conversation.model,
1712
- reasoningEffort: conversation.reasoningEffort,
1713
- permissionMode: conversation.permissionMode,
1714
- collaborationMode: conversation.collaborationMode,
1715
- });
1716
- return;
1717
- }
1718
1913
  throw new Error(`命令暂未实现:/${command.name}`);
1719
1914
  }
1720
1915
  catch (error) {
@@ -1729,6 +1924,41 @@ export class AgentWorkspaceProxy {
1729
1924
  });
1730
1925
  }
1731
1926
  }
1927
+ async executeGitNativeCommand(conversation, commandName, args) {
1928
+ const git = gitCommandArgs(commandName, args);
1929
+ const toolId = id(commandName);
1930
+ const now = Date.now();
1931
+ conversation.status = "running";
1932
+ conversation.lastMessagePreview = git.display;
1933
+ conversation.lastActivityAt = now;
1934
+ this.emitConversation(conversation);
1935
+ this.upsertTool(conversation.id, {
1936
+ id: toolId,
1937
+ name: "命令",
1938
+ input: `${git.display}\n\ncwd: ${conversation.cwd}`,
1939
+ createdAt: now,
1940
+ status: "running",
1941
+ });
1942
+ const result = await runProcess("git", git.argv, {
1943
+ cwd: conversation.cwd,
1944
+ maxBytes: COMMAND_OUTPUT_MAX_BYTES,
1945
+ });
1946
+ const ok = result.exitCode === 0;
1947
+ this.upsertTool(conversation.id, {
1948
+ id: toolId,
1949
+ name: "命令",
1950
+ input: `${git.display}\n\ncwd: ${conversation.cwd}`,
1951
+ output: result.output || (ok ? "完成" : `退出码 ${result.exitCode ?? "unknown"}`),
1952
+ createdAt: now,
1953
+ status: ok ? "completed" : "failed",
1954
+ });
1955
+ conversation.status = ok ? "idle" : "error";
1956
+ conversation.lastMessagePreview = ok
1957
+ ? `${git.display} 完成`
1958
+ : `${git.display} 失败`;
1959
+ conversation.lastActivityAt = Date.now();
1960
+ this.emitConversation(conversation);
1961
+ }
1732
1962
  handleRequest(method, params) {
1733
1963
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
1734
1964
  return this.handleStructuredInput(params, true);
@@ -2380,6 +2610,14 @@ export class AgentWorkspaceProxy {
2380
2610
  this.permissionWaiters.delete(requestId);
2381
2611
  this.permissionSources.delete(requestId);
2382
2612
  resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
2613
+ this.markPermission(conversationId, requestId, {
2614
+ permissionOutcome: "cancelled",
2615
+ optionId: "cancelled",
2616
+ permissionLive: false,
2617
+ permissionPending: false,
2618
+ permissionExpired: true,
2619
+ permissionError: "等待授权超时",
2620
+ });
2383
2621
  this.updateConversationStatus(conversationId, "idle");
2384
2622
  }, PERMISSION_TIMEOUT_MS);
2385
2623
  this.permissionWaiters.set(requestId, { resolve, timer });
@@ -2409,6 +2647,8 @@ export class AgentWorkspaceProxy {
2409
2647
  this.markPermission(payload.conversationId, payload.requestId, {
2410
2648
  permissionOutcome: payload.outcome,
2411
2649
  optionId: selectedOptionId,
2650
+ permissionLive: false,
2651
+ permissionExpired: false,
2412
2652
  permissionError: undefined,
2413
2653
  permissionPending: false,
2414
2654
  });
@@ -2455,6 +2695,209 @@ export class AgentWorkspaceProxy {
2455
2695
  updatedAt: Date.now(),
2456
2696
  });
2457
2697
  }
2698
+ getRevision(conversationId) {
2699
+ return this.conversationRevisions.get(conversationId) ??
2700
+ this.conversations.get(conversationId)?.timelineRevision ??
2701
+ 0;
2702
+ }
2703
+ setRevisionFloor(conversationId, revision) {
2704
+ const nextRevision = Math.max(this.getRevision(conversationId), revision);
2705
+ this.conversationRevisions.set(conversationId, nextRevision);
2706
+ const conversation = this.conversations.get(conversationId);
2707
+ if (conversation) {
2708
+ conversation.timelineRevision = nextRevision;
2709
+ conversation.runningTurnId = this.currentTurnIds.get(conversationId);
2710
+ }
2711
+ return nextRevision;
2712
+ }
2713
+ conversationSnapshot(conversation) {
2714
+ return {
2715
+ ...conversation,
2716
+ timelineRevision: this.getRevision(conversation.id),
2717
+ historyComplete: conversation.historyComplete ?? false,
2718
+ runningTurnId: this.currentTurnIds.get(conversation.id),
2719
+ source: conversation.source ?? "device",
2720
+ canonical: conversation.canonical ?? true,
2721
+ };
2722
+ }
2723
+ annotateTimelineItem(item, revision, source) {
2724
+ return {
2725
+ ...item,
2726
+ revision: revision ?? item.revision,
2727
+ source: item.source ?? source,
2728
+ canonical: item.canonical ?? true,
2729
+ };
2730
+ }
2731
+ recordRevisionEvent(conversationId, change) {
2732
+ const revision = this.getRevision(conversationId) + 1;
2733
+ this.conversationRevisions.set(conversationId, revision);
2734
+ const conversation = this.conversations.get(conversationId);
2735
+ if (conversation) {
2736
+ conversation.timelineRevision = revision;
2737
+ conversation.runningTurnId = this.currentTurnIds.get(conversationId);
2738
+ }
2739
+ const event = {
2740
+ revision,
2741
+ item: change.item ? this.annotateTimelineItem(change.item, revision, "device-live") : undefined,
2742
+ conversation: change.conversation ? this.conversationSnapshot(change.conversation) : undefined,
2743
+ };
2744
+ const events = this.revisionEvents.get(conversationId) ?? [];
2745
+ events.push(event);
2746
+ if (events.length > MAX_DELTA_EVENTS) {
2747
+ events.splice(0, events.length - MAX_DELTA_EVENTS);
2748
+ }
2749
+ this.revisionEvents.set(conversationId, events);
2750
+ return event;
2751
+ }
2752
+ storedTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
2753
+ if (!conversation.agentSessionId)
2754
+ return [];
2755
+ const result = conversation.provider === "codex"
2756
+ ? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd, { maxItems })
2757
+ : conversation.provider === "claude"
2758
+ ? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id, { maxItems })
2759
+ : { items: [] };
2760
+ return result.items
2761
+ .sort((a, b) => a.createdAt - b.createdAt)
2762
+ .map((item, index) => this.annotateTimelineItem(item, index + 1, "device-history"));
2763
+ }
2764
+ canonicalTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
2765
+ const merged = new Map();
2766
+ for (const item of this.storedTimeline(conversation, maxItems)) {
2767
+ merged.set(item.id, item);
2768
+ }
2769
+ for (const item of this.timelines.get(conversation.id) ?? []) {
2770
+ merged.set(item.id, this.annotateTimelineItem(item, item.revision, item.source ?? "device-live"));
2771
+ }
2772
+ const items = [...merged.values()]
2773
+ .sort((a, b) => a.createdAt - b.createdAt)
2774
+ .slice(-maxItems);
2775
+ this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
2776
+ return items;
2777
+ }
2778
+ async appServerTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
2779
+ if (conversation.provider !== "codex" || !conversation.agentSessionId)
2780
+ return [];
2781
+ if (this.protocolForProvider(conversation.provider) !== "codex-app-server")
2782
+ return [];
2783
+ const client = this.clientForProvider(conversation.provider);
2784
+ const listTurns = client?.listTurns;
2785
+ if (typeof listTurns !== "function")
2786
+ return [];
2787
+ try {
2788
+ const result = await listTurns.call(client, { sessionId: conversation.agentSessionId, limit: maxItems });
2789
+ return this.timelineFromAppServerTurns(conversation.id, conversation.agentSessionId, result);
2790
+ }
2791
+ catch (error) {
2792
+ if (this.input.verbose) {
2793
+ process.stderr.write(`[agent:v2] thread/turns/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
2794
+ }
2795
+ return [];
2796
+ }
2797
+ }
2798
+ timelineFromAppServerTurns(conversationId, agentSessionId, value) {
2799
+ const raw = asRecord(value);
2800
+ const turns = Array.isArray(value) ? value :
2801
+ Array.isArray(raw?.turns) ? raw.turns :
2802
+ Array.isArray(raw?.items) ? raw.items :
2803
+ Array.isArray(raw?.entries) ? raw.entries :
2804
+ [];
2805
+ const items = [];
2806
+ turns.forEach((entry, index) => {
2807
+ const turn = asRecord(entry);
2808
+ if (!turn)
2809
+ return;
2810
+ const turnId = firstString(turn, ["id", "turnId", "turn_id"]) ?? `turn-${index + 1}`;
2811
+ const createdAt = parseTimestamp(turn.createdAt ?? turn.created_at ?? turn.startedAt ?? turn.started_at) ?? Date.now() + index;
2812
+ const updatedAt = parseTimestamp(turn.updatedAt ?? turn.updated_at ?? turn.completedAt ?? turn.completed_at);
2813
+ const userText = appServerText(turn.input ?? turn.prompt ?? turn.user ?? turn.userMessage ?? turn.request);
2814
+ if (userText) {
2815
+ items.push({
2816
+ id: `app-server:${agentSessionId}:${turnId}:user`,
2817
+ conversationId,
2818
+ type: "message",
2819
+ kind: "chat",
2820
+ turnId,
2821
+ role: "user",
2822
+ content: [{ type: "text", text: userText }],
2823
+ text: userText,
2824
+ createdAt,
2825
+ updatedAt,
2826
+ metadata: { source: "app-server", provider: "codex" },
2827
+ });
2828
+ }
2829
+ const assistantText = appServerText(turn.output ?? turn.response ?? turn.assistant ?? turn.assistantMessage ?? turn.result);
2830
+ if (assistantText) {
2831
+ items.push({
2832
+ id: `app-server:${agentSessionId}:${turnId}:assistant`,
2833
+ conversationId,
2834
+ type: "message",
2835
+ kind: "chat",
2836
+ turnId,
2837
+ role: "assistant",
2838
+ content: [{ type: "text", text: assistantText }],
2839
+ text: assistantText,
2840
+ createdAt: updatedAt ?? createdAt + 1,
2841
+ updatedAt,
2842
+ metadata: { source: "app-server", provider: "codex" },
2843
+ });
2844
+ }
2845
+ const nestedItems = Array.isArray(turn.items) ? turn.items : Array.isArray(turn.messages) ? turn.messages : [];
2846
+ for (const nested of nestedItems) {
2847
+ const nestedRecord = asRecord(nested);
2848
+ if (!nestedRecord)
2849
+ continue;
2850
+ const text = appServerText(nestedRecord.content ?? nestedRecord.text ?? nestedRecord.message);
2851
+ const role = nestedRecord.role === "user" || nestedRecord.role === "assistant" || nestedRecord.role === "system"
2852
+ ? nestedRecord.role
2853
+ : undefined;
2854
+ if (!text || !role)
2855
+ continue;
2856
+ const itemId = firstString(nestedRecord, ["id", "itemId", "messageId"]) ?? `${role}-${items.length + 1}`;
2857
+ items.push({
2858
+ id: `app-server:${agentSessionId}:${turnId}:${itemId}`,
2859
+ conversationId,
2860
+ type: "message",
2861
+ kind: "chat",
2862
+ turnId,
2863
+ itemId,
2864
+ role,
2865
+ content: [{ type: "text", text }],
2866
+ text,
2867
+ createdAt: parseTimestamp(nestedRecord.createdAt ?? nestedRecord.created_at) ?? createdAt + items.length,
2868
+ updatedAt: parseTimestamp(nestedRecord.updatedAt ?? nestedRecord.updated_at),
2869
+ metadata: { source: "app-server", provider: "codex" },
2870
+ });
2871
+ }
2872
+ });
2873
+ return items
2874
+ .sort((a, b) => a.createdAt - b.createdAt)
2875
+ .map((item, index) => this.annotateTimelineItem(item, index + 1, "app-server"));
2876
+ }
2877
+ mergeCanonicalTimelineItems(items, maxItems) {
2878
+ const byKey = new Map();
2879
+ for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
2880
+ const key = item.type === "message" && item.role && item.text
2881
+ ? `${item.role}:${item.text.replace(/\s+/g, " ").trim().slice(0, 500)}`
2882
+ : item.id;
2883
+ const existing = byKey.get(key);
2884
+ if (!existing || item.source === "app-server" || (item.updatedAt ?? item.createdAt) >= (existing.updatedAt ?? existing.createdAt)) {
2885
+ byKey.set(key, item);
2886
+ }
2887
+ }
2888
+ return [...byKey.values()]
2889
+ .sort((a, b) => a.createdAt - b.createdAt)
2890
+ .slice(-maxItems);
2891
+ }
2892
+ latestSnapshot(conversationId) {
2893
+ const timeline = this.timelines.get(conversationId) ?? [];
2894
+ const start = Math.max(0, timeline.length - MAX_SNAPSHOT_ITEMS);
2895
+ return {
2896
+ items: timeline.slice(start).map((item) => snapshotTimelineItem(item)),
2897
+ cursor: start > 0 ? String(start) : undefined,
2898
+ hasMore: start > 0,
2899
+ };
2900
+ }
2458
2901
  addItem(conversationId, item) {
2459
2902
  this.rememberItemConversationId(conversationId, item);
2460
2903
  const timeline = this.timelines.get(conversationId) ?? [];
@@ -2572,6 +3015,17 @@ export class AgentWorkspaceProxy {
2572
3015
  this.timelines.set(newId, [...mergedTimeline.values()]
2573
3016
  .sort((a, b) => a.createdAt - b.createdAt)
2574
3017
  .slice(-MAX_TIMELINE_ITEMS));
3018
+ const oldRevision = this.conversationRevisions.get(oldId) ?? 0;
3019
+ if (oldRevision > 0) {
3020
+ this.conversationRevisions.delete(oldId);
3021
+ this.setRevisionFloor(newId, oldRevision);
3022
+ }
3023
+ const oldEvents = this.revisionEvents.get(oldId);
3024
+ if (oldEvents) {
3025
+ this.revisionEvents.delete(oldId);
3026
+ const existingEvents = this.revisionEvents.get(newId) ?? [];
3027
+ this.revisionEvents.set(newId, [...existingEvents, ...oldEvents].slice(-MAX_DELTA_EVENTS));
3028
+ }
2575
3029
  for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
2576
3030
  if (conversationId === oldId) {
2577
3031
  this.conversationByAgentSessionId.set(agentSessionId, newId);
@@ -2605,17 +3059,47 @@ export class AgentWorkspaceProxy {
2605
3059
  }
2606
3060
  emitItem(conversationId, item) {
2607
3061
  const conversation = this.conversations.get(conversationId);
3062
+ const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
3063
+ const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
2608
3064
  this.input.send(createEnvelope({
2609
3065
  type: "agent.v2.event",
2610
3066
  hostDeviceId: this.input.hostDeviceId,
2611
- payload: { conversationId, conversation, item: snapshotTimelineItem(item, { stripImages: false }) },
3067
+ payload: {
3068
+ conversationId,
3069
+ conversation: event.conversation,
3070
+ item: event.item,
3071
+ revision: event.revision,
3072
+ source: "device-live",
3073
+ canonical: true,
3074
+ },
2612
3075
  }));
2613
3076
  }
2614
3077
  emitConversation(conversation) {
3078
+ const event = this.recordRevisionEvent(conversation.id, { conversation });
2615
3079
  this.input.send(createEnvelope({
2616
3080
  type: "agent.v2.event",
2617
3081
  hostDeviceId: this.input.hostDeviceId,
2618
- payload: { conversationId: conversation.id, conversation },
3082
+ payload: {
3083
+ conversationId: conversation.id,
3084
+ conversation: event.conversation,
3085
+ revision: event.revision,
3086
+ source: "device-live",
3087
+ canonical: true,
3088
+ },
3089
+ }));
3090
+ this.input.send(createEnvelope({
3091
+ type: "agent.v2.running_state",
3092
+ hostDeviceId: this.input.hostDeviceId,
3093
+ payload: {
3094
+ conversationId: conversation.id,
3095
+ status: conversation.status,
3096
+ runningTurnId: this.currentTurnIds.get(conversation.id),
3097
+ revision: event.revision,
3098
+ error: conversation.status === "error" ? conversation.lastMessagePreview : undefined,
3099
+ updatedAt: conversation.lastActivityAt,
3100
+ source: "device-live",
3101
+ canonical: true,
3102
+ },
2619
3103
  }));
2620
3104
  }
2621
3105
  emitStatus(conversationId, status, text) {
@@ -2659,17 +3143,126 @@ export class AgentWorkspaceProxy {
2659
3143
  if (conversation)
2660
3144
  this.hydrateStoredTimeline(conversation);
2661
3145
  }
2662
- const conversations = [...this.conversations.values()];
2663
- const items = conversationId
2664
- ? snapshotTimelineItems(this.timelines.get(conversationId) ?? [])
2665
- : [];
3146
+ const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
3147
+ const snapshot = conversationId ? this.latestSnapshot(conversationId) : { items: [], cursor: undefined, hasMore: false };
2666
3148
  this.input.send(createEnvelope({
2667
3149
  type: "agent.v2.snapshot",
2668
3150
  hostDeviceId: this.input.hostDeviceId,
2669
3151
  payload: {
2670
3152
  conversations,
2671
3153
  activeConversationId: this.activeConversationId,
2672
- items,
3154
+ items: snapshot.items,
3155
+ revision: conversationId ? this.getRevision(conversationId) : undefined,
3156
+ cursor: snapshot.cursor,
3157
+ hasMore: snapshot.hasMore,
3158
+ source: "device-history",
3159
+ canonical: true,
3160
+ },
3161
+ }));
3162
+ }
3163
+ async sendHistoryPage(payload) {
3164
+ const conversation = this.conversations.get(payload.conversationId);
3165
+ if (!conversation)
3166
+ return;
3167
+ const limit = Math.min(Math.max(payload.limit ?? MAX_SNAPSHOT_ITEMS, 1), 200);
3168
+ const items = this.mergeCanonicalTimelineItems([
3169
+ ...this.canonicalTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
3170
+ ...await this.appServerTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
3171
+ ], HISTORY_PAGE_MAX_ITEMS);
3172
+ this.timelines.set(conversation.id, items.slice(-MAX_TIMELINE_ITEMS).map((item) => this.annotateTimelineItem(item, item.revision, item.source ?? "device-history")));
3173
+ for (const item of this.timelines.get(conversation.id) ?? []) {
3174
+ this.rememberItemConversationId(conversation.id, item);
3175
+ }
3176
+ const direction = payload.direction ?? "older";
3177
+ let page;
3178
+ let cursor;
3179
+ let hasMore = false;
3180
+ if (direction === "newer") {
3181
+ const start = clampHistoryCursor(payload.cursor, 0, items.length);
3182
+ const end = Math.min(items.length, start + limit);
3183
+ page = items.slice(start, end);
3184
+ hasMore = end < items.length;
3185
+ cursor = hasMore ? String(end) : undefined;
3186
+ }
3187
+ else {
3188
+ const end = clampHistoryCursor(payload.cursor, items.length, items.length);
3189
+ const start = Math.max(0, end - limit);
3190
+ page = items.slice(start, end);
3191
+ hasMore = start > 0;
3192
+ cursor = hasMore ? String(start) : undefined;
3193
+ }
3194
+ conversation.historyComplete = !hasMore;
3195
+ const revision = this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
3196
+ this.input.send(createEnvelope({
3197
+ type: "agent.v2.history.page",
3198
+ hostDeviceId: this.input.hostDeviceId,
3199
+ payload: {
3200
+ conversationId: conversation.id,
3201
+ conversation: this.conversationSnapshot(conversation),
3202
+ items: page.map((item) => snapshotTimelineItem(item)),
3203
+ revision,
3204
+ cursor,
3205
+ hasMore,
3206
+ source: "device-history",
3207
+ canonical: true,
3208
+ },
3209
+ }));
3210
+ }
3211
+ sendDelta(payload) {
3212
+ const conversation = this.conversations.get(payload.conversationId);
3213
+ if (!conversation)
3214
+ return;
3215
+ this.hydrateStoredTimeline(conversation);
3216
+ const sinceRevision = payload.sinceRevision ?? 0;
3217
+ const limit = Math.min(Math.max(payload.limit ?? 100, 1), 500);
3218
+ const events = this.revisionEvents.get(conversation.id) ?? [];
3219
+ const oldestAvailable = events[0]?.revision ?? this.getRevision(conversation.id);
3220
+ const newestRevision = this.getRevision(conversation.id);
3221
+ const reset = sinceRevision > 0 &&
3222
+ (sinceRevision > newestRevision ||
3223
+ (sinceRevision < newestRevision &&
3224
+ (events.length === 0 || sinceRevision < oldestAvailable - 1)));
3225
+ if (reset) {
3226
+ const snapshot = this.latestSnapshot(conversation.id);
3227
+ this.input.send(createEnvelope({
3228
+ type: "agent.v2.delta",
3229
+ hostDeviceId: this.input.hostDeviceId,
3230
+ payload: {
3231
+ conversationId: conversation.id,
3232
+ conversation: this.conversationSnapshot(conversation),
3233
+ items: snapshot.items,
3234
+ sinceRevision,
3235
+ revision: newestRevision,
3236
+ reset: true,
3237
+ cursor: snapshot.cursor,
3238
+ hasMore: snapshot.hasMore,
3239
+ source: "device-history",
3240
+ canonical: true,
3241
+ },
3242
+ }));
3243
+ return;
3244
+ }
3245
+ const changed = events
3246
+ .filter((event) => event.revision > sinceRevision)
3247
+ .slice(-limit);
3248
+ const itemsById = new Map();
3249
+ for (const event of changed) {
3250
+ if (event.item)
3251
+ itemsById.set(event.item.id, event.item);
3252
+ }
3253
+ this.input.send(createEnvelope({
3254
+ type: "agent.v2.delta",
3255
+ hostDeviceId: this.input.hostDeviceId,
3256
+ payload: {
3257
+ conversationId: conversation.id,
3258
+ conversation: this.conversationSnapshot(conversation),
3259
+ items: [...itemsById.values()].map((item) => snapshotTimelineItem(item)),
3260
+ sinceRevision,
3261
+ revision: newestRevision,
3262
+ reset: false,
3263
+ hasMore: changed.length === limit && events.some((event) => event.revision > sinceRevision && event.revision < changed[0].revision),
3264
+ source: "device-live",
3265
+ canonical: true,
2673
3266
  },
2674
3267
  }));
2675
3268
  }
@@ -2677,21 +3270,23 @@ export class AgentWorkspaceProxy {
2677
3270
  if (!conversation.agentSessionId)
2678
3271
  return;
2679
3272
  const existing = this.timelines.get(conversation.id) ?? [];
2680
- if (existing.length > 0)
3273
+ if (existing.length > 0) {
3274
+ this.setRevisionFloor(conversation.id, Math.max(existing.length, ...existing.map((item) => item.revision ?? 0)));
2681
3275
  return;
2682
- const result = conversation.provider === "codex"
2683
- ? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd)
2684
- : conversation.provider === "claude"
2685
- ? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id)
2686
- : { items: [] };
2687
- if (result.items.length === 0)
3276
+ }
3277
+ const stored = this.storedTimeline(conversation, MAX_TIMELINE_ITEMS);
3278
+ conversation.historyComplete = stored.length <= MAX_SNAPSHOT_ITEMS;
3279
+ if (stored.length === 0) {
3280
+ this.setRevisionFloor(conversation.id, this.getRevision(conversation.id));
2688
3281
  return;
2689
- const items = result.items
3282
+ }
3283
+ const items = stored
2690
3284
  .sort((a, b) => a.createdAt - b.createdAt)
2691
3285
  .slice(-MAX_TIMELINE_ITEMS);
2692
3286
  this.timelines.set(conversation.id, items);
2693
3287
  for (const item of items)
2694
3288
  this.rememberItemConversationId(conversation.id, item);
3289
+ this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
2695
3290
  const lastMessage = [...items].reverse().find((item) => item.text?.trim());
2696
3291
  if (lastMessage?.text && !conversation.lastMessagePreview) {
2697
3292
  conversation.lastMessagePreview = previewText(lastMessage.text);
@@ -2760,6 +3355,9 @@ export class AgentWorkspaceProxy {
2760
3355
  rememberTurnConversationId(conversationId, turnId) {
2761
3356
  this.currentTurnIds.set(conversationId, turnId);
2762
3357
  this.turnConversationIds.set(turnId, conversationId);
3358
+ const conversation = this.conversations.get(conversationId);
3359
+ if (conversation)
3360
+ conversation.runningTurnId = turnId;
2763
3361
  }
2764
3362
  forgetCurrentTurn(conversationId, turnId) {
2765
3363
  const currentTurnId = this.currentTurnIds.get(conversationId);
@@ -2768,6 +3366,9 @@ export class AgentWorkspaceProxy {
2768
3366
  this.turnConversationIds.delete(turnId);
2769
3367
  if (currentTurnId && currentTurnId !== turnId)
2770
3368
  this.turnConversationIds.delete(currentTurnId);
3369
+ const conversation = this.conversations.get(conversationId);
3370
+ if (conversation)
3371
+ conversation.runningTurnId = undefined;
2771
3372
  }
2772
3373
  rememberItemConversationId(conversationId, item) {
2773
3374
  const keys = [
@@ -2807,28 +3408,51 @@ export class AgentWorkspaceProxy {
2807
3408
  }
2808
3409
  cancelPendingPermissions(conversationId) {
2809
3410
  for (const [requestId, waiter] of this.permissionWaiters) {
3411
+ const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
3412
+ if (conversationId && ownerConversationId && ownerConversationId !== conversationId)
3413
+ continue;
2810
3414
  clearTimeout(waiter.timer);
2811
3415
  waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
3416
+ if (ownerConversationId) {
3417
+ this.markPermission(ownerConversationId, requestId, {
3418
+ permissionOutcome: "cancelled",
3419
+ optionId: "cancelled",
3420
+ permissionLive: false,
3421
+ permissionPending: false,
3422
+ permissionExpired: false,
3423
+ permissionError: "已停止",
3424
+ });
3425
+ }
3426
+ this.permissionWaiters.delete(requestId);
2812
3427
  this.pendingPermissions.delete(requestId);
2813
3428
  this.permissionSources.delete(requestId);
2814
3429
  }
2815
- this.permissionWaiters.clear();
2816
3430
  for (const [requestId, waiter] of this.structuredInputWaiters) {
3431
+ const pending = this.pendingStructuredInputs.get(requestId);
3432
+ if (conversationId && pending?.conversationId && pending.conversationId !== conversationId)
3433
+ continue;
2817
3434
  clearTimeout(waiter.timer);
2818
3435
  waiter.resolve(formatStructuredInputResponse({}));
2819
- const pending = this.pendingStructuredInputs.get(requestId);
2820
3436
  if (pending) {
2821
3437
  this.markStructuredInput(pending.conversationId, requestId, {
2822
3438
  inputPending: false,
2823
3439
  inputError: "已停止",
2824
3440
  });
2825
3441
  }
3442
+ this.structuredInputWaiters.delete(requestId);
2826
3443
  this.pendingStructuredInputs.delete(requestId);
2827
3444
  }
2828
- this.structuredInputWaiters.clear();
2829
3445
  if (conversationId)
2830
3446
  this.updateConversationStatus(conversationId, "idle");
2831
3447
  }
3448
+ conversationIdForPermissionRequest(requestId) {
3449
+ for (const [conversationId, timeline] of this.timelines) {
3450
+ if (timeline.some((item) => item.type === "permission" && item.permission?.requestId === requestId)) {
3451
+ return conversationId;
3452
+ }
3453
+ }
3454
+ return undefined;
3455
+ }
2832
3456
  extractSessionId(value) {
2833
3457
  const raw = asRecord(value);
2834
3458
  if (!raw)