linkshell-cli 0.3.13 → 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.
@@ -13,9 +13,43 @@ const PERMISSION_TIMEOUT_MS = 5 * 60_000;
13
13
  const MAX_TIMELINE_ITEMS = 200;
14
14
  const MAX_SNAPSHOT_ITEMS = 80;
15
15
  const MAX_SNAPSHOT_TEXT_BYTES = 128 * 1024;
16
+ const MAX_DELTA_EVENTS = 500;
17
+ const HISTORY_PAGE_MAX_ITEMS = 500;
16
18
  function id(prefix) {
17
19
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
18
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
+ }
19
53
  function stringify(value) {
20
54
  if (typeof value === "string")
21
55
  return value;
@@ -1119,10 +1153,26 @@ function parseRemoteSessions(value) {
1119
1153
  createdAt: parseTimestamp(source.createdAt ?? source.created_at),
1120
1154
  lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
1121
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"]),
1122
1158
  });
1123
1159
  }
1124
1160
  return result;
1125
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
+ }
1126
1176
  export class AgentWorkspaceProxy {
1127
1177
  input;
1128
1178
  clients = new Map();
@@ -1137,6 +1187,8 @@ export class AgentWorkspaceProxy {
1137
1187
  conversations = new Map();
1138
1188
  conversationByAgentSessionId = new Map();
1139
1189
  timelines = new Map();
1190
+ conversationRevisions = new Map();
1191
+ revisionEvents = new Map();
1140
1192
  toolOutputBuffers = new Map();
1141
1193
  pendingPermissions = new Map();
1142
1194
  permissionWaiters = new Map();
@@ -1166,7 +1218,7 @@ export class AgentWorkspaceProxy {
1166
1218
  this.input.send(createEnvelope({
1167
1219
  type: "agent.v2.conversation.list.result",
1168
1220
  hostDeviceId: this.input.hostDeviceId,
1169
- payload: { conversations },
1221
+ payload: { conversations: conversations.map((conversation) => this.conversationSnapshot(conversation)) },
1170
1222
  }));
1171
1223
  break;
1172
1224
  }
@@ -1175,6 +1227,16 @@ export class AgentWorkspaceProxy {
1175
1227
  this.sendSnapshot(payload.conversationId);
1176
1228
  break;
1177
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
+ }
1178
1240
  case "agent.v2.prompt": {
1179
1241
  const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
1180
1242
  await this.sendPrompt(payload);
@@ -1377,14 +1439,21 @@ export class AgentWorkspaceProxy {
1377
1439
  reasoningEffort: existing?.reasoningEffort,
1378
1440
  permissionMode: existing?.permissionMode,
1379
1441
  collaborationMode: existing?.collaborationMode,
1380
- status: existing?.status ?? "idle",
1442
+ status: remote.status ?? existing?.status ?? "idle",
1381
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,
1382
1449
  lastMessagePreview: existing?.lastMessagePreview,
1383
1450
  lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
1384
1451
  createdAt: remote.createdAt ?? existing?.createdAt ?? now,
1385
1452
  };
1386
1453
  this.conversations.set(conversation.id, conversation);
1387
1454
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1455
+ if (remote.runningTurnId)
1456
+ this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
1388
1457
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1389
1458
  }
1390
1459
  }
@@ -1462,12 +1531,18 @@ export class AgentWorkspaceProxy {
1462
1531
  }
1463
1532
  this.hydrateStoredTimeline(existingConversation);
1464
1533
  this.activeConversationId = existingConversation.id;
1534
+ const snapshot = this.latestSnapshot(existingConversation.id);
1465
1535
  this.input.send(createEnvelope({
1466
1536
  type: "agent.v2.conversation.opened",
1467
1537
  hostDeviceId: this.input.hostDeviceId,
1468
1538
  payload: {
1469
- conversation: existingConversation,
1470
- 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,
1471
1546
  },
1472
1547
  }));
1473
1548
  return existingConversation;
@@ -1502,6 +1577,11 @@ export class AgentWorkspaceProxy {
1502
1577
  collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
1503
1578
  status: "idle",
1504
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,
1505
1585
  lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
1506
1586
  lastActivityAt: now,
1507
1587
  createdAt: existingConversation?.createdAt ?? now,
@@ -1511,10 +1591,19 @@ export class AgentWorkspaceProxy {
1511
1591
  this.activeConversationId = conversation.id;
1512
1592
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1513
1593
  this.hydrateStoredTimeline(conversation);
1594
+ const snapshot = this.latestSnapshot(conversation.id);
1514
1595
  this.input.send(createEnvelope({
1515
1596
  type: "agent.v2.conversation.opened",
1516
1597
  hostDeviceId: this.input.hostDeviceId,
1517
- 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
+ },
1518
1607
  }));
1519
1608
  return conversation;
1520
1609
  }
@@ -1537,6 +1626,10 @@ export class AgentWorkspaceProxy {
1537
1626
  collaborationMode: payload.collaborationMode,
1538
1627
  status: "error",
1539
1628
  archived: false,
1629
+ timelineRevision: this.getRevision(fallbackId),
1630
+ historyComplete: false,
1631
+ source: "device",
1632
+ canonical: true,
1540
1633
  lastMessagePreview: message,
1541
1634
  lastActivityAt: now,
1542
1635
  createdAt: now,
@@ -1553,7 +1646,14 @@ export class AgentWorkspaceProxy {
1553
1646
  this.input.send(createEnvelope({
1554
1647
  type: "agent.v2.conversation.opened",
1555
1648
  hostDeviceId: this.input.hostDeviceId,
1556
- 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
+ },
1557
1657
  }));
1558
1658
  return conversation;
1559
1659
  }
@@ -2510,6 +2610,14 @@ export class AgentWorkspaceProxy {
2510
2610
  this.permissionWaiters.delete(requestId);
2511
2611
  this.permissionSources.delete(requestId);
2512
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
+ });
2513
2621
  this.updateConversationStatus(conversationId, "idle");
2514
2622
  }, PERMISSION_TIMEOUT_MS);
2515
2623
  this.permissionWaiters.set(requestId, { resolve, timer });
@@ -2539,6 +2647,8 @@ export class AgentWorkspaceProxy {
2539
2647
  this.markPermission(payload.conversationId, payload.requestId, {
2540
2648
  permissionOutcome: payload.outcome,
2541
2649
  optionId: selectedOptionId,
2650
+ permissionLive: false,
2651
+ permissionExpired: false,
2542
2652
  permissionError: undefined,
2543
2653
  permissionPending: false,
2544
2654
  });
@@ -2585,6 +2695,209 @@ export class AgentWorkspaceProxy {
2585
2695
  updatedAt: Date.now(),
2586
2696
  });
2587
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
+ }
2588
2901
  addItem(conversationId, item) {
2589
2902
  this.rememberItemConversationId(conversationId, item);
2590
2903
  const timeline = this.timelines.get(conversationId) ?? [];
@@ -2702,6 +3015,17 @@ export class AgentWorkspaceProxy {
2702
3015
  this.timelines.set(newId, [...mergedTimeline.values()]
2703
3016
  .sort((a, b) => a.createdAt - b.createdAt)
2704
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
+ }
2705
3029
  for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
2706
3030
  if (conversationId === oldId) {
2707
3031
  this.conversationByAgentSessionId.set(agentSessionId, newId);
@@ -2735,17 +3059,47 @@ export class AgentWorkspaceProxy {
2735
3059
  }
2736
3060
  emitItem(conversationId, item) {
2737
3061
  const conversation = this.conversations.get(conversationId);
3062
+ const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
3063
+ const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
2738
3064
  this.input.send(createEnvelope({
2739
3065
  type: "agent.v2.event",
2740
3066
  hostDeviceId: this.input.hostDeviceId,
2741
- 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
+ },
2742
3075
  }));
2743
3076
  }
2744
3077
  emitConversation(conversation) {
3078
+ const event = this.recordRevisionEvent(conversation.id, { conversation });
2745
3079
  this.input.send(createEnvelope({
2746
3080
  type: "agent.v2.event",
2747
3081
  hostDeviceId: this.input.hostDeviceId,
2748
- 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
+ },
2749
3103
  }));
2750
3104
  }
2751
3105
  emitStatus(conversationId, status, text) {
@@ -2789,17 +3143,126 @@ export class AgentWorkspaceProxy {
2789
3143
  if (conversation)
2790
3144
  this.hydrateStoredTimeline(conversation);
2791
3145
  }
2792
- const conversations = [...this.conversations.values()];
2793
- const items = conversationId
2794
- ? snapshotTimelineItems(this.timelines.get(conversationId) ?? [])
2795
- : [];
3146
+ const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
3147
+ const snapshot = conversationId ? this.latestSnapshot(conversationId) : { items: [], cursor: undefined, hasMore: false };
2796
3148
  this.input.send(createEnvelope({
2797
3149
  type: "agent.v2.snapshot",
2798
3150
  hostDeviceId: this.input.hostDeviceId,
2799
3151
  payload: {
2800
3152
  conversations,
2801
3153
  activeConversationId: this.activeConversationId,
2802
- 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,
2803
3266
  },
2804
3267
  }));
2805
3268
  }
@@ -2807,21 +3270,23 @@ export class AgentWorkspaceProxy {
2807
3270
  if (!conversation.agentSessionId)
2808
3271
  return;
2809
3272
  const existing = this.timelines.get(conversation.id) ?? [];
2810
- if (existing.length > 0)
3273
+ if (existing.length > 0) {
3274
+ this.setRevisionFloor(conversation.id, Math.max(existing.length, ...existing.map((item) => item.revision ?? 0)));
2811
3275
  return;
2812
- const result = conversation.provider === "codex"
2813
- ? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd)
2814
- : conversation.provider === "claude"
2815
- ? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id)
2816
- : { items: [] };
2817
- 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));
2818
3281
  return;
2819
- const items = result.items
3282
+ }
3283
+ const items = stored
2820
3284
  .sort((a, b) => a.createdAt - b.createdAt)
2821
3285
  .slice(-MAX_TIMELINE_ITEMS);
2822
3286
  this.timelines.set(conversation.id, items);
2823
3287
  for (const item of items)
2824
3288
  this.rememberItemConversationId(conversation.id, item);
3289
+ this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
2825
3290
  const lastMessage = [...items].reverse().find((item) => item.text?.trim());
2826
3291
  if (lastMessage?.text && !conversation.lastMessagePreview) {
2827
3292
  conversation.lastMessagePreview = previewText(lastMessage.text);
@@ -2890,6 +3355,9 @@ export class AgentWorkspaceProxy {
2890
3355
  rememberTurnConversationId(conversationId, turnId) {
2891
3356
  this.currentTurnIds.set(conversationId, turnId);
2892
3357
  this.turnConversationIds.set(turnId, conversationId);
3358
+ const conversation = this.conversations.get(conversationId);
3359
+ if (conversation)
3360
+ conversation.runningTurnId = turnId;
2893
3361
  }
2894
3362
  forgetCurrentTurn(conversationId, turnId) {
2895
3363
  const currentTurnId = this.currentTurnIds.get(conversationId);
@@ -2898,6 +3366,9 @@ export class AgentWorkspaceProxy {
2898
3366
  this.turnConversationIds.delete(turnId);
2899
3367
  if (currentTurnId && currentTurnId !== turnId)
2900
3368
  this.turnConversationIds.delete(currentTurnId);
3369
+ const conversation = this.conversations.get(conversationId);
3370
+ if (conversation)
3371
+ conversation.runningTurnId = undefined;
2901
3372
  }
2902
3373
  rememberItemConversationId(conversationId, item) {
2903
3374
  const keys = [
@@ -2937,28 +3408,51 @@ export class AgentWorkspaceProxy {
2937
3408
  }
2938
3409
  cancelPendingPermissions(conversationId) {
2939
3410
  for (const [requestId, waiter] of this.permissionWaiters) {
3411
+ const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
3412
+ if (conversationId && ownerConversationId && ownerConversationId !== conversationId)
3413
+ continue;
2940
3414
  clearTimeout(waiter.timer);
2941
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);
2942
3427
  this.pendingPermissions.delete(requestId);
2943
3428
  this.permissionSources.delete(requestId);
2944
3429
  }
2945
- this.permissionWaiters.clear();
2946
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;
2947
3434
  clearTimeout(waiter.timer);
2948
3435
  waiter.resolve(formatStructuredInputResponse({}));
2949
- const pending = this.pendingStructuredInputs.get(requestId);
2950
3436
  if (pending) {
2951
3437
  this.markStructuredInput(pending.conversationId, requestId, {
2952
3438
  inputPending: false,
2953
3439
  inputError: "已停止",
2954
3440
  });
2955
3441
  }
3442
+ this.structuredInputWaiters.delete(requestId);
2956
3443
  this.pendingStructuredInputs.delete(requestId);
2957
3444
  }
2958
- this.structuredInputWaiters.clear();
2959
3445
  if (conversationId)
2960
3446
  this.updateConversationStatus(conversationId, "idle");
2961
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
+ }
2962
3456
  extractSessionId(value) {
2963
3457
  const raw = asRecord(value);
2964
3458
  if (!raw)