linkshell-cli 0.3.13 → 0.3.15
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.
- package/dist/cli/src/runtime/acp/acp-client.d.ts +4 -0
- package/dist/cli/src/runtime/acp/acp-client.js +11 -0
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +18 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +529 -25
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sessions.d.ts +3 -1
- package/dist/cli/src/runtime/acp/claude-sessions.js +16 -8
- package/dist/cli/src/runtime/acp/claude-sessions.js.map +1 -1
- package/dist/cli/src/runtime/acp/codex-sessions.d.ts +5 -1
- package/dist/cli/src/runtime/acp/codex-sessions.js +2 -2
- package/dist/cli/src/runtime/acp/codex-sessions.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.js +4 -3
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +6015 -1928
- package/dist/shared-protocol/src/index.js +73 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/runtime/acp/acp-client.ts +12 -0
- package/src/runtime/acp/agent-workspace.ts +579 -26
- package/src/runtime/acp/claude-sessions.ts +15 -6
- package/src/runtime/acp/codex-sessions.ts +4 -1
- package/src/runtime/bridge-session.ts +4 -5
|
@@ -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,15 +1153,33 @@ 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();
|
|
1129
1179
|
agentProtocols = new Map();
|
|
1130
1180
|
providerCapabilities = new Map();
|
|
1181
|
+
providerCapabilityErrors = new Map();
|
|
1182
|
+
capabilitiesRevision = 0;
|
|
1131
1183
|
initialized = false;
|
|
1132
1184
|
status = "unavailable";
|
|
1133
1185
|
error;
|
|
@@ -1137,6 +1189,8 @@ export class AgentWorkspaceProxy {
|
|
|
1137
1189
|
conversations = new Map();
|
|
1138
1190
|
conversationByAgentSessionId = new Map();
|
|
1139
1191
|
timelines = new Map();
|
|
1192
|
+
conversationRevisions = new Map();
|
|
1193
|
+
revisionEvents = new Map();
|
|
1140
1194
|
toolOutputBuffers = new Map();
|
|
1141
1195
|
pendingPermissions = new Map();
|
|
1142
1196
|
permissionWaiters = new Map();
|
|
@@ -1166,7 +1220,7 @@ export class AgentWorkspaceProxy {
|
|
|
1166
1220
|
this.input.send(createEnvelope({
|
|
1167
1221
|
type: "agent.v2.conversation.list.result",
|
|
1168
1222
|
hostDeviceId: this.input.hostDeviceId,
|
|
1169
|
-
payload: { conversations },
|
|
1223
|
+
payload: { conversations: conversations.map((conversation) => this.conversationSnapshot(conversation)) },
|
|
1170
1224
|
}));
|
|
1171
1225
|
break;
|
|
1172
1226
|
}
|
|
@@ -1175,6 +1229,16 @@ export class AgentWorkspaceProxy {
|
|
|
1175
1229
|
this.sendSnapshot(payload.conversationId);
|
|
1176
1230
|
break;
|
|
1177
1231
|
}
|
|
1232
|
+
case "agent.v2.history.request": {
|
|
1233
|
+
const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
|
|
1234
|
+
await this.sendHistoryPage(payload);
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
case "agent.v2.delta.request": {
|
|
1238
|
+
const payload = parseTypedPayload("agent.v2.delta.request", envelope.payload);
|
|
1239
|
+
this.sendDelta(payload);
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1178
1242
|
case "agent.v2.prompt": {
|
|
1179
1243
|
const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
|
|
1180
1244
|
await this.sendPrompt(payload);
|
|
@@ -1334,10 +1398,13 @@ export class AgentWorkspaceProxy {
|
|
|
1334
1398
|
try {
|
|
1335
1399
|
const result = await listModels.call(client);
|
|
1336
1400
|
const runtimeCapabilities = parseModelListCapabilities(result);
|
|
1337
|
-
if (runtimeCapabilities)
|
|
1401
|
+
if (runtimeCapabilities) {
|
|
1338
1402
|
this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
1403
|
+
this.providerCapabilityErrors.delete(provider);
|
|
1404
|
+
}
|
|
1339
1405
|
}
|
|
1340
1406
|
catch (error) {
|
|
1407
|
+
this.providerCapabilityErrors.set(provider, error instanceof Error ? error.message : String(error));
|
|
1341
1408
|
if (this.input.verbose) {
|
|
1342
1409
|
process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1343
1410
|
}
|
|
@@ -1377,14 +1444,21 @@ export class AgentWorkspaceProxy {
|
|
|
1377
1444
|
reasoningEffort: existing?.reasoningEffort,
|
|
1378
1445
|
permissionMode: existing?.permissionMode,
|
|
1379
1446
|
collaborationMode: existing?.collaborationMode,
|
|
1380
|
-
status: existing?.status ?? "idle",
|
|
1447
|
+
status: remote.status ?? existing?.status ?? "idle",
|
|
1381
1448
|
archived: remote.archived ?? existing?.archived ?? false,
|
|
1449
|
+
timelineRevision: existing?.timelineRevision ?? this.getRevision(conversationId),
|
|
1450
|
+
historyComplete: existing?.historyComplete ?? false,
|
|
1451
|
+
runningTurnId: remote.runningTurnId ?? this.currentTurnIds.get(conversationId),
|
|
1452
|
+
source: "device",
|
|
1453
|
+
canonical: true,
|
|
1382
1454
|
lastMessagePreview: existing?.lastMessagePreview,
|
|
1383
1455
|
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
1384
1456
|
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
1385
1457
|
};
|
|
1386
1458
|
this.conversations.set(conversation.id, conversation);
|
|
1387
1459
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1460
|
+
if (remote.runningTurnId)
|
|
1461
|
+
this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
|
|
1388
1462
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1389
1463
|
}
|
|
1390
1464
|
}
|
|
@@ -1394,6 +1468,7 @@ export class AgentWorkspaceProxy {
|
|
|
1394
1468
|
const protocol = this.agentProtocols.get(provider);
|
|
1395
1469
|
const runtimeCapabilities = this.providerCapabilities.get(provider);
|
|
1396
1470
|
const enabled = Boolean(client);
|
|
1471
|
+
const hasRuntimeModels = Boolean(runtimeCapabilities?.models?.length);
|
|
1397
1472
|
const supportsImages = enabled && protocolSupportsImages(protocol);
|
|
1398
1473
|
const isClaudeFallback = protocol === "claude-stream-json";
|
|
1399
1474
|
const supportsPermission = enabled && !isClaudeFallback;
|
|
@@ -1405,12 +1480,15 @@ export class AgentWorkspaceProxy {
|
|
|
1405
1480
|
label: providerLabel(provider),
|
|
1406
1481
|
enabled,
|
|
1407
1482
|
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
1483
|
+
providerProtocol: protocol,
|
|
1408
1484
|
supportsImages,
|
|
1409
1485
|
supportsPermission,
|
|
1410
1486
|
supportsPlan: enabled,
|
|
1411
1487
|
supportsCancel: enabled,
|
|
1412
1488
|
models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
|
|
1413
1489
|
defaultModel: runtimeCapabilities?.defaultModel,
|
|
1490
|
+
modelsSource: hasRuntimeModels ? "runtime" : enabled ? "fallback" : "unavailable",
|
|
1491
|
+
modelListError: this.providerCapabilityErrors.get(provider),
|
|
1414
1492
|
reasoningEfforts: supportsReasoningEffort
|
|
1415
1493
|
? runtimeCapabilities?.reasoningEfforts ?? (provider === "claude" ? [...CLAUDE_REASONING_EFFORTS] : [...ALL_REASONING_EFFORTS])
|
|
1416
1494
|
: [],
|
|
@@ -1439,6 +1517,7 @@ export class AgentWorkspaceProxy {
|
|
|
1439
1517
|
providers,
|
|
1440
1518
|
protocolVersion: 1,
|
|
1441
1519
|
workspaceProtocolVersion: 2,
|
|
1520
|
+
capabilitiesRevision: ++this.capabilitiesRevision,
|
|
1442
1521
|
error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
|
|
1443
1522
|
supportsSessionList: anyEnabled,
|
|
1444
1523
|
supportsSessionLoad: anyEnabled,
|
|
@@ -1462,12 +1541,18 @@ export class AgentWorkspaceProxy {
|
|
|
1462
1541
|
}
|
|
1463
1542
|
this.hydrateStoredTimeline(existingConversation);
|
|
1464
1543
|
this.activeConversationId = existingConversation.id;
|
|
1544
|
+
const snapshot = this.latestSnapshot(existingConversation.id);
|
|
1465
1545
|
this.input.send(createEnvelope({
|
|
1466
1546
|
type: "agent.v2.conversation.opened",
|
|
1467
1547
|
hostDeviceId: this.input.hostDeviceId,
|
|
1468
1548
|
payload: {
|
|
1469
|
-
conversation: existingConversation,
|
|
1470
|
-
snapshot:
|
|
1549
|
+
conversation: this.conversationSnapshot(existingConversation),
|
|
1550
|
+
snapshot: snapshot.items,
|
|
1551
|
+
revision: this.getRevision(existingConversation.id),
|
|
1552
|
+
cursor: snapshot.cursor,
|
|
1553
|
+
hasMore: snapshot.hasMore,
|
|
1554
|
+
source: "device-history",
|
|
1555
|
+
canonical: true,
|
|
1471
1556
|
},
|
|
1472
1557
|
}));
|
|
1473
1558
|
return existingConversation;
|
|
@@ -1502,6 +1587,11 @@ export class AgentWorkspaceProxy {
|
|
|
1502
1587
|
collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
|
|
1503
1588
|
status: "idle",
|
|
1504
1589
|
archived: existingConversation?.archived ?? false,
|
|
1590
|
+
timelineRevision: existingConversation?.timelineRevision ?? this.getRevision(conversationId),
|
|
1591
|
+
historyComplete: existingConversation?.historyComplete ?? false,
|
|
1592
|
+
runningTurnId: this.currentTurnIds.get(conversationId),
|
|
1593
|
+
source: "device",
|
|
1594
|
+
canonical: true,
|
|
1505
1595
|
lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
|
|
1506
1596
|
lastActivityAt: now,
|
|
1507
1597
|
createdAt: existingConversation?.createdAt ?? now,
|
|
@@ -1511,10 +1601,19 @@ export class AgentWorkspaceProxy {
|
|
|
1511
1601
|
this.activeConversationId = conversation.id;
|
|
1512
1602
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1513
1603
|
this.hydrateStoredTimeline(conversation);
|
|
1604
|
+
const snapshot = this.latestSnapshot(conversation.id);
|
|
1514
1605
|
this.input.send(createEnvelope({
|
|
1515
1606
|
type: "agent.v2.conversation.opened",
|
|
1516
1607
|
hostDeviceId: this.input.hostDeviceId,
|
|
1517
|
-
payload: {
|
|
1608
|
+
payload: {
|
|
1609
|
+
conversation: this.conversationSnapshot(conversation),
|
|
1610
|
+
snapshot: snapshot.items,
|
|
1611
|
+
revision: this.getRevision(conversation.id),
|
|
1612
|
+
cursor: snapshot.cursor,
|
|
1613
|
+
hasMore: snapshot.hasMore,
|
|
1614
|
+
source: "device-history",
|
|
1615
|
+
canonical: true,
|
|
1616
|
+
},
|
|
1518
1617
|
}));
|
|
1519
1618
|
return conversation;
|
|
1520
1619
|
}
|
|
@@ -1537,6 +1636,10 @@ export class AgentWorkspaceProxy {
|
|
|
1537
1636
|
collaborationMode: payload.collaborationMode,
|
|
1538
1637
|
status: "error",
|
|
1539
1638
|
archived: false,
|
|
1639
|
+
timelineRevision: this.getRevision(fallbackId),
|
|
1640
|
+
historyComplete: false,
|
|
1641
|
+
source: "device",
|
|
1642
|
+
canonical: true,
|
|
1540
1643
|
lastMessagePreview: message,
|
|
1541
1644
|
lastActivityAt: now,
|
|
1542
1645
|
createdAt: now,
|
|
@@ -1553,7 +1656,14 @@ export class AgentWorkspaceProxy {
|
|
|
1553
1656
|
this.input.send(createEnvelope({
|
|
1554
1657
|
type: "agent.v2.conversation.opened",
|
|
1555
1658
|
hostDeviceId: this.input.hostDeviceId,
|
|
1556
|
-
payload: {
|
|
1659
|
+
payload: {
|
|
1660
|
+
conversation: this.conversationSnapshot(conversation),
|
|
1661
|
+
snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []),
|
|
1662
|
+
revision: this.getRevision(conversation.id),
|
|
1663
|
+
hasMore: false,
|
|
1664
|
+
source: "device",
|
|
1665
|
+
canonical: true,
|
|
1666
|
+
},
|
|
1557
1667
|
}));
|
|
1558
1668
|
return conversation;
|
|
1559
1669
|
}
|
|
@@ -2510,6 +2620,14 @@ export class AgentWorkspaceProxy {
|
|
|
2510
2620
|
this.permissionWaiters.delete(requestId);
|
|
2511
2621
|
this.permissionSources.delete(requestId);
|
|
2512
2622
|
resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
|
|
2623
|
+
this.markPermission(conversationId, requestId, {
|
|
2624
|
+
permissionOutcome: "cancelled",
|
|
2625
|
+
optionId: "cancelled",
|
|
2626
|
+
permissionLive: false,
|
|
2627
|
+
permissionPending: false,
|
|
2628
|
+
permissionExpired: true,
|
|
2629
|
+
permissionError: "等待授权超时",
|
|
2630
|
+
});
|
|
2513
2631
|
this.updateConversationStatus(conversationId, "idle");
|
|
2514
2632
|
}, PERMISSION_TIMEOUT_MS);
|
|
2515
2633
|
this.permissionWaiters.set(requestId, { resolve, timer });
|
|
@@ -2539,6 +2657,8 @@ export class AgentWorkspaceProxy {
|
|
|
2539
2657
|
this.markPermission(payload.conversationId, payload.requestId, {
|
|
2540
2658
|
permissionOutcome: payload.outcome,
|
|
2541
2659
|
optionId: selectedOptionId,
|
|
2660
|
+
permissionLive: false,
|
|
2661
|
+
permissionExpired: false,
|
|
2542
2662
|
permissionError: undefined,
|
|
2543
2663
|
permissionPending: false,
|
|
2544
2664
|
});
|
|
@@ -2585,6 +2705,209 @@ export class AgentWorkspaceProxy {
|
|
|
2585
2705
|
updatedAt: Date.now(),
|
|
2586
2706
|
});
|
|
2587
2707
|
}
|
|
2708
|
+
getRevision(conversationId) {
|
|
2709
|
+
return this.conversationRevisions.get(conversationId) ??
|
|
2710
|
+
this.conversations.get(conversationId)?.timelineRevision ??
|
|
2711
|
+
0;
|
|
2712
|
+
}
|
|
2713
|
+
setRevisionFloor(conversationId, revision) {
|
|
2714
|
+
const nextRevision = Math.max(this.getRevision(conversationId), revision);
|
|
2715
|
+
this.conversationRevisions.set(conversationId, nextRevision);
|
|
2716
|
+
const conversation = this.conversations.get(conversationId);
|
|
2717
|
+
if (conversation) {
|
|
2718
|
+
conversation.timelineRevision = nextRevision;
|
|
2719
|
+
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
2720
|
+
}
|
|
2721
|
+
return nextRevision;
|
|
2722
|
+
}
|
|
2723
|
+
conversationSnapshot(conversation) {
|
|
2724
|
+
return {
|
|
2725
|
+
...conversation,
|
|
2726
|
+
timelineRevision: this.getRevision(conversation.id),
|
|
2727
|
+
historyComplete: conversation.historyComplete ?? false,
|
|
2728
|
+
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
2729
|
+
source: conversation.source ?? "device",
|
|
2730
|
+
canonical: conversation.canonical ?? true,
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
annotateTimelineItem(item, revision, source) {
|
|
2734
|
+
return {
|
|
2735
|
+
...item,
|
|
2736
|
+
revision: revision ?? item.revision,
|
|
2737
|
+
source: item.source ?? source,
|
|
2738
|
+
canonical: item.canonical ?? true,
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
recordRevisionEvent(conversationId, change) {
|
|
2742
|
+
const revision = this.getRevision(conversationId) + 1;
|
|
2743
|
+
this.conversationRevisions.set(conversationId, revision);
|
|
2744
|
+
const conversation = this.conversations.get(conversationId);
|
|
2745
|
+
if (conversation) {
|
|
2746
|
+
conversation.timelineRevision = revision;
|
|
2747
|
+
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
2748
|
+
}
|
|
2749
|
+
const event = {
|
|
2750
|
+
revision,
|
|
2751
|
+
item: change.item ? this.annotateTimelineItem(change.item, revision, "device-live") : undefined,
|
|
2752
|
+
conversation: change.conversation ? this.conversationSnapshot(change.conversation) : undefined,
|
|
2753
|
+
};
|
|
2754
|
+
const events = this.revisionEvents.get(conversationId) ?? [];
|
|
2755
|
+
events.push(event);
|
|
2756
|
+
if (events.length > MAX_DELTA_EVENTS) {
|
|
2757
|
+
events.splice(0, events.length - MAX_DELTA_EVENTS);
|
|
2758
|
+
}
|
|
2759
|
+
this.revisionEvents.set(conversationId, events);
|
|
2760
|
+
return event;
|
|
2761
|
+
}
|
|
2762
|
+
storedTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
|
|
2763
|
+
if (!conversation.agentSessionId)
|
|
2764
|
+
return [];
|
|
2765
|
+
const result = conversation.provider === "codex"
|
|
2766
|
+
? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd, { maxItems })
|
|
2767
|
+
: conversation.provider === "claude"
|
|
2768
|
+
? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id, { maxItems })
|
|
2769
|
+
: { items: [] };
|
|
2770
|
+
return result.items
|
|
2771
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2772
|
+
.map((item, index) => this.annotateTimelineItem(item, index + 1, "device-history"));
|
|
2773
|
+
}
|
|
2774
|
+
canonicalTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
|
|
2775
|
+
const merged = new Map();
|
|
2776
|
+
for (const item of this.storedTimeline(conversation, maxItems)) {
|
|
2777
|
+
merged.set(item.id, item);
|
|
2778
|
+
}
|
|
2779
|
+
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
2780
|
+
merged.set(item.id, this.annotateTimelineItem(item, item.revision, item.source ?? "device-live"));
|
|
2781
|
+
}
|
|
2782
|
+
const items = [...merged.values()]
|
|
2783
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2784
|
+
.slice(-maxItems);
|
|
2785
|
+
this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
2786
|
+
return items;
|
|
2787
|
+
}
|
|
2788
|
+
async appServerTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
|
|
2789
|
+
if (conversation.provider !== "codex" || !conversation.agentSessionId)
|
|
2790
|
+
return [];
|
|
2791
|
+
if (this.protocolForProvider(conversation.provider) !== "codex-app-server")
|
|
2792
|
+
return [];
|
|
2793
|
+
const client = this.clientForProvider(conversation.provider);
|
|
2794
|
+
const listTurns = client?.listTurns;
|
|
2795
|
+
if (typeof listTurns !== "function")
|
|
2796
|
+
return [];
|
|
2797
|
+
try {
|
|
2798
|
+
const result = await listTurns.call(client, { sessionId: conversation.agentSessionId, limit: maxItems });
|
|
2799
|
+
return this.timelineFromAppServerTurns(conversation.id, conversation.agentSessionId, result);
|
|
2800
|
+
}
|
|
2801
|
+
catch (error) {
|
|
2802
|
+
if (this.input.verbose) {
|
|
2803
|
+
process.stderr.write(`[agent:v2] thread/turns/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2804
|
+
}
|
|
2805
|
+
return [];
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
timelineFromAppServerTurns(conversationId, agentSessionId, value) {
|
|
2809
|
+
const raw = asRecord(value);
|
|
2810
|
+
const turns = Array.isArray(value) ? value :
|
|
2811
|
+
Array.isArray(raw?.turns) ? raw.turns :
|
|
2812
|
+
Array.isArray(raw?.items) ? raw.items :
|
|
2813
|
+
Array.isArray(raw?.entries) ? raw.entries :
|
|
2814
|
+
[];
|
|
2815
|
+
const items = [];
|
|
2816
|
+
turns.forEach((entry, index) => {
|
|
2817
|
+
const turn = asRecord(entry);
|
|
2818
|
+
if (!turn)
|
|
2819
|
+
return;
|
|
2820
|
+
const turnId = firstString(turn, ["id", "turnId", "turn_id"]) ?? `turn-${index + 1}`;
|
|
2821
|
+
const createdAt = parseTimestamp(turn.createdAt ?? turn.created_at ?? turn.startedAt ?? turn.started_at) ?? Date.now() + index;
|
|
2822
|
+
const updatedAt = parseTimestamp(turn.updatedAt ?? turn.updated_at ?? turn.completedAt ?? turn.completed_at);
|
|
2823
|
+
const userText = appServerText(turn.input ?? turn.prompt ?? turn.user ?? turn.userMessage ?? turn.request);
|
|
2824
|
+
if (userText) {
|
|
2825
|
+
items.push({
|
|
2826
|
+
id: `app-server:${agentSessionId}:${turnId}:user`,
|
|
2827
|
+
conversationId,
|
|
2828
|
+
type: "message",
|
|
2829
|
+
kind: "chat",
|
|
2830
|
+
turnId,
|
|
2831
|
+
role: "user",
|
|
2832
|
+
content: [{ type: "text", text: userText }],
|
|
2833
|
+
text: userText,
|
|
2834
|
+
createdAt,
|
|
2835
|
+
updatedAt,
|
|
2836
|
+
metadata: { source: "app-server", provider: "codex" },
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
const assistantText = appServerText(turn.output ?? turn.response ?? turn.assistant ?? turn.assistantMessage ?? turn.result);
|
|
2840
|
+
if (assistantText) {
|
|
2841
|
+
items.push({
|
|
2842
|
+
id: `app-server:${agentSessionId}:${turnId}:assistant`,
|
|
2843
|
+
conversationId,
|
|
2844
|
+
type: "message",
|
|
2845
|
+
kind: "chat",
|
|
2846
|
+
turnId,
|
|
2847
|
+
role: "assistant",
|
|
2848
|
+
content: [{ type: "text", text: assistantText }],
|
|
2849
|
+
text: assistantText,
|
|
2850
|
+
createdAt: updatedAt ?? createdAt + 1,
|
|
2851
|
+
updatedAt,
|
|
2852
|
+
metadata: { source: "app-server", provider: "codex" },
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
const nestedItems = Array.isArray(turn.items) ? turn.items : Array.isArray(turn.messages) ? turn.messages : [];
|
|
2856
|
+
for (const nested of nestedItems) {
|
|
2857
|
+
const nestedRecord = asRecord(nested);
|
|
2858
|
+
if (!nestedRecord)
|
|
2859
|
+
continue;
|
|
2860
|
+
const text = appServerText(nestedRecord.content ?? nestedRecord.text ?? nestedRecord.message);
|
|
2861
|
+
const role = nestedRecord.role === "user" || nestedRecord.role === "assistant" || nestedRecord.role === "system"
|
|
2862
|
+
? nestedRecord.role
|
|
2863
|
+
: undefined;
|
|
2864
|
+
if (!text || !role)
|
|
2865
|
+
continue;
|
|
2866
|
+
const itemId = firstString(nestedRecord, ["id", "itemId", "messageId"]) ?? `${role}-${items.length + 1}`;
|
|
2867
|
+
items.push({
|
|
2868
|
+
id: `app-server:${agentSessionId}:${turnId}:${itemId}`,
|
|
2869
|
+
conversationId,
|
|
2870
|
+
type: "message",
|
|
2871
|
+
kind: "chat",
|
|
2872
|
+
turnId,
|
|
2873
|
+
itemId,
|
|
2874
|
+
role,
|
|
2875
|
+
content: [{ type: "text", text }],
|
|
2876
|
+
text,
|
|
2877
|
+
createdAt: parseTimestamp(nestedRecord.createdAt ?? nestedRecord.created_at) ?? createdAt + items.length,
|
|
2878
|
+
updatedAt: parseTimestamp(nestedRecord.updatedAt ?? nestedRecord.updated_at),
|
|
2879
|
+
metadata: { source: "app-server", provider: "codex" },
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2883
|
+
return items
|
|
2884
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2885
|
+
.map((item, index) => this.annotateTimelineItem(item, index + 1, "app-server"));
|
|
2886
|
+
}
|
|
2887
|
+
mergeCanonicalTimelineItems(items, maxItems) {
|
|
2888
|
+
const byKey = new Map();
|
|
2889
|
+
for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
|
|
2890
|
+
const key = item.type === "message" && item.role && item.text
|
|
2891
|
+
? `${item.role}:${item.text.replace(/\s+/g, " ").trim().slice(0, 500)}`
|
|
2892
|
+
: item.id;
|
|
2893
|
+
const existing = byKey.get(key);
|
|
2894
|
+
if (!existing || item.source === "app-server" || (item.updatedAt ?? item.createdAt) >= (existing.updatedAt ?? existing.createdAt)) {
|
|
2895
|
+
byKey.set(key, item);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
return [...byKey.values()]
|
|
2899
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2900
|
+
.slice(-maxItems);
|
|
2901
|
+
}
|
|
2902
|
+
latestSnapshot(conversationId) {
|
|
2903
|
+
const timeline = this.timelines.get(conversationId) ?? [];
|
|
2904
|
+
const start = Math.max(0, timeline.length - MAX_SNAPSHOT_ITEMS);
|
|
2905
|
+
return {
|
|
2906
|
+
items: timeline.slice(start).map((item) => snapshotTimelineItem(item)),
|
|
2907
|
+
cursor: start > 0 ? String(start) : undefined,
|
|
2908
|
+
hasMore: start > 0,
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2588
2911
|
addItem(conversationId, item) {
|
|
2589
2912
|
this.rememberItemConversationId(conversationId, item);
|
|
2590
2913
|
const timeline = this.timelines.get(conversationId) ?? [];
|
|
@@ -2702,6 +3025,17 @@ export class AgentWorkspaceProxy {
|
|
|
2702
3025
|
this.timelines.set(newId, [...mergedTimeline.values()]
|
|
2703
3026
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2704
3027
|
.slice(-MAX_TIMELINE_ITEMS));
|
|
3028
|
+
const oldRevision = this.conversationRevisions.get(oldId) ?? 0;
|
|
3029
|
+
if (oldRevision > 0) {
|
|
3030
|
+
this.conversationRevisions.delete(oldId);
|
|
3031
|
+
this.setRevisionFloor(newId, oldRevision);
|
|
3032
|
+
}
|
|
3033
|
+
const oldEvents = this.revisionEvents.get(oldId);
|
|
3034
|
+
if (oldEvents) {
|
|
3035
|
+
this.revisionEvents.delete(oldId);
|
|
3036
|
+
const existingEvents = this.revisionEvents.get(newId) ?? [];
|
|
3037
|
+
this.revisionEvents.set(newId, [...existingEvents, ...oldEvents].slice(-MAX_DELTA_EVENTS));
|
|
3038
|
+
}
|
|
2705
3039
|
for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
|
|
2706
3040
|
if (conversationId === oldId) {
|
|
2707
3041
|
this.conversationByAgentSessionId.set(agentSessionId, newId);
|
|
@@ -2735,17 +3069,47 @@ export class AgentWorkspaceProxy {
|
|
|
2735
3069
|
}
|
|
2736
3070
|
emitItem(conversationId, item) {
|
|
2737
3071
|
const conversation = this.conversations.get(conversationId);
|
|
3072
|
+
const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
|
|
3073
|
+
const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
|
|
2738
3074
|
this.input.send(createEnvelope({
|
|
2739
3075
|
type: "agent.v2.event",
|
|
2740
3076
|
hostDeviceId: this.input.hostDeviceId,
|
|
2741
|
-
payload: {
|
|
3077
|
+
payload: {
|
|
3078
|
+
conversationId,
|
|
3079
|
+
conversation: event.conversation,
|
|
3080
|
+
item: event.item,
|
|
3081
|
+
revision: event.revision,
|
|
3082
|
+
source: "device-live",
|
|
3083
|
+
canonical: true,
|
|
3084
|
+
},
|
|
2742
3085
|
}));
|
|
2743
3086
|
}
|
|
2744
3087
|
emitConversation(conversation) {
|
|
3088
|
+
const event = this.recordRevisionEvent(conversation.id, { conversation });
|
|
2745
3089
|
this.input.send(createEnvelope({
|
|
2746
3090
|
type: "agent.v2.event",
|
|
2747
3091
|
hostDeviceId: this.input.hostDeviceId,
|
|
2748
|
-
payload: {
|
|
3092
|
+
payload: {
|
|
3093
|
+
conversationId: conversation.id,
|
|
3094
|
+
conversation: event.conversation,
|
|
3095
|
+
revision: event.revision,
|
|
3096
|
+
source: "device-live",
|
|
3097
|
+
canonical: true,
|
|
3098
|
+
},
|
|
3099
|
+
}));
|
|
3100
|
+
this.input.send(createEnvelope({
|
|
3101
|
+
type: "agent.v2.running_state",
|
|
3102
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3103
|
+
payload: {
|
|
3104
|
+
conversationId: conversation.id,
|
|
3105
|
+
status: conversation.status,
|
|
3106
|
+
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
3107
|
+
revision: event.revision,
|
|
3108
|
+
error: conversation.status === "error" ? conversation.lastMessagePreview : undefined,
|
|
3109
|
+
updatedAt: conversation.lastActivityAt,
|
|
3110
|
+
source: "device-live",
|
|
3111
|
+
canonical: true,
|
|
3112
|
+
},
|
|
2749
3113
|
}));
|
|
2750
3114
|
}
|
|
2751
3115
|
emitStatus(conversationId, status, text) {
|
|
@@ -2789,17 +3153,126 @@ export class AgentWorkspaceProxy {
|
|
|
2789
3153
|
if (conversation)
|
|
2790
3154
|
this.hydrateStoredTimeline(conversation);
|
|
2791
3155
|
}
|
|
2792
|
-
const conversations = [...this.conversations.values()];
|
|
2793
|
-
const
|
|
2794
|
-
? snapshotTimelineItems(this.timelines.get(conversationId) ?? [])
|
|
2795
|
-
: [];
|
|
3156
|
+
const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
|
|
3157
|
+
const snapshot = conversationId ? this.latestSnapshot(conversationId) : { items: [], cursor: undefined, hasMore: false };
|
|
2796
3158
|
this.input.send(createEnvelope({
|
|
2797
3159
|
type: "agent.v2.snapshot",
|
|
2798
3160
|
hostDeviceId: this.input.hostDeviceId,
|
|
2799
3161
|
payload: {
|
|
2800
3162
|
conversations,
|
|
2801
3163
|
activeConversationId: this.activeConversationId,
|
|
2802
|
-
items,
|
|
3164
|
+
items: snapshot.items,
|
|
3165
|
+
revision: conversationId ? this.getRevision(conversationId) : undefined,
|
|
3166
|
+
cursor: snapshot.cursor,
|
|
3167
|
+
hasMore: snapshot.hasMore,
|
|
3168
|
+
source: "device-history",
|
|
3169
|
+
canonical: true,
|
|
3170
|
+
},
|
|
3171
|
+
}));
|
|
3172
|
+
}
|
|
3173
|
+
async sendHistoryPage(payload) {
|
|
3174
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
3175
|
+
if (!conversation)
|
|
3176
|
+
return;
|
|
3177
|
+
const limit = Math.min(Math.max(payload.limit ?? MAX_SNAPSHOT_ITEMS, 1), 200);
|
|
3178
|
+
const items = this.mergeCanonicalTimelineItems([
|
|
3179
|
+
...this.canonicalTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3180
|
+
...await this.appServerTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3181
|
+
], HISTORY_PAGE_MAX_ITEMS);
|
|
3182
|
+
this.timelines.set(conversation.id, items.slice(-MAX_TIMELINE_ITEMS).map((item) => this.annotateTimelineItem(item, item.revision, item.source ?? "device-history")));
|
|
3183
|
+
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
3184
|
+
this.rememberItemConversationId(conversation.id, item);
|
|
3185
|
+
}
|
|
3186
|
+
const direction = payload.direction ?? "older";
|
|
3187
|
+
let page;
|
|
3188
|
+
let cursor;
|
|
3189
|
+
let hasMore = false;
|
|
3190
|
+
if (direction === "newer") {
|
|
3191
|
+
const start = clampHistoryCursor(payload.cursor, 0, items.length);
|
|
3192
|
+
const end = Math.min(items.length, start + limit);
|
|
3193
|
+
page = items.slice(start, end);
|
|
3194
|
+
hasMore = end < items.length;
|
|
3195
|
+
cursor = hasMore ? String(end) : undefined;
|
|
3196
|
+
}
|
|
3197
|
+
else {
|
|
3198
|
+
const end = clampHistoryCursor(payload.cursor, items.length, items.length);
|
|
3199
|
+
const start = Math.max(0, end - limit);
|
|
3200
|
+
page = items.slice(start, end);
|
|
3201
|
+
hasMore = start > 0;
|
|
3202
|
+
cursor = hasMore ? String(start) : undefined;
|
|
3203
|
+
}
|
|
3204
|
+
conversation.historyComplete = !hasMore;
|
|
3205
|
+
const revision = this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
3206
|
+
this.input.send(createEnvelope({
|
|
3207
|
+
type: "agent.v2.history.page",
|
|
3208
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3209
|
+
payload: {
|
|
3210
|
+
conversationId: conversation.id,
|
|
3211
|
+
conversation: this.conversationSnapshot(conversation),
|
|
3212
|
+
items: page.map((item) => snapshotTimelineItem(item)),
|
|
3213
|
+
revision,
|
|
3214
|
+
cursor,
|
|
3215
|
+
hasMore,
|
|
3216
|
+
source: "device-history",
|
|
3217
|
+
canonical: true,
|
|
3218
|
+
},
|
|
3219
|
+
}));
|
|
3220
|
+
}
|
|
3221
|
+
sendDelta(payload) {
|
|
3222
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
3223
|
+
if (!conversation)
|
|
3224
|
+
return;
|
|
3225
|
+
this.hydrateStoredTimeline(conversation);
|
|
3226
|
+
const sinceRevision = payload.sinceRevision ?? 0;
|
|
3227
|
+
const limit = Math.min(Math.max(payload.limit ?? 100, 1), 500);
|
|
3228
|
+
const events = this.revisionEvents.get(conversation.id) ?? [];
|
|
3229
|
+
const oldestAvailable = events[0]?.revision ?? this.getRevision(conversation.id);
|
|
3230
|
+
const newestRevision = this.getRevision(conversation.id);
|
|
3231
|
+
const reset = sinceRevision > 0 &&
|
|
3232
|
+
(sinceRevision > newestRevision ||
|
|
3233
|
+
(sinceRevision < newestRevision &&
|
|
3234
|
+
(events.length === 0 || sinceRevision < oldestAvailable - 1)));
|
|
3235
|
+
if (reset) {
|
|
3236
|
+
const snapshot = this.latestSnapshot(conversation.id);
|
|
3237
|
+
this.input.send(createEnvelope({
|
|
3238
|
+
type: "agent.v2.delta",
|
|
3239
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3240
|
+
payload: {
|
|
3241
|
+
conversationId: conversation.id,
|
|
3242
|
+
conversation: this.conversationSnapshot(conversation),
|
|
3243
|
+
items: snapshot.items,
|
|
3244
|
+
sinceRevision,
|
|
3245
|
+
revision: newestRevision,
|
|
3246
|
+
reset: true,
|
|
3247
|
+
cursor: snapshot.cursor,
|
|
3248
|
+
hasMore: snapshot.hasMore,
|
|
3249
|
+
source: "device-history",
|
|
3250
|
+
canonical: true,
|
|
3251
|
+
},
|
|
3252
|
+
}));
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
const changed = events
|
|
3256
|
+
.filter((event) => event.revision > sinceRevision)
|
|
3257
|
+
.slice(-limit);
|
|
3258
|
+
const itemsById = new Map();
|
|
3259
|
+
for (const event of changed) {
|
|
3260
|
+
if (event.item)
|
|
3261
|
+
itemsById.set(event.item.id, event.item);
|
|
3262
|
+
}
|
|
3263
|
+
this.input.send(createEnvelope({
|
|
3264
|
+
type: "agent.v2.delta",
|
|
3265
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3266
|
+
payload: {
|
|
3267
|
+
conversationId: conversation.id,
|
|
3268
|
+
conversation: this.conversationSnapshot(conversation),
|
|
3269
|
+
items: [...itemsById.values()].map((item) => snapshotTimelineItem(item)),
|
|
3270
|
+
sinceRevision,
|
|
3271
|
+
revision: newestRevision,
|
|
3272
|
+
reset: false,
|
|
3273
|
+
hasMore: changed.length === limit && events.some((event) => event.revision > sinceRevision && event.revision < changed[0].revision),
|
|
3274
|
+
source: "device-live",
|
|
3275
|
+
canonical: true,
|
|
2803
3276
|
},
|
|
2804
3277
|
}));
|
|
2805
3278
|
}
|
|
@@ -2807,21 +3280,23 @@ export class AgentWorkspaceProxy {
|
|
|
2807
3280
|
if (!conversation.agentSessionId)
|
|
2808
3281
|
return;
|
|
2809
3282
|
const existing = this.timelines.get(conversation.id) ?? [];
|
|
2810
|
-
if (existing.length > 0)
|
|
3283
|
+
if (existing.length > 0) {
|
|
3284
|
+
this.setRevisionFloor(conversation.id, Math.max(existing.length, ...existing.map((item) => item.revision ?? 0)));
|
|
2811
3285
|
return;
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
if (result.items.length === 0)
|
|
3286
|
+
}
|
|
3287
|
+
const stored = this.storedTimeline(conversation, MAX_TIMELINE_ITEMS);
|
|
3288
|
+
conversation.historyComplete = stored.length <= MAX_SNAPSHOT_ITEMS;
|
|
3289
|
+
if (stored.length === 0) {
|
|
3290
|
+
this.setRevisionFloor(conversation.id, this.getRevision(conversation.id));
|
|
2818
3291
|
return;
|
|
2819
|
-
|
|
3292
|
+
}
|
|
3293
|
+
const items = stored
|
|
2820
3294
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2821
3295
|
.slice(-MAX_TIMELINE_ITEMS);
|
|
2822
3296
|
this.timelines.set(conversation.id, items);
|
|
2823
3297
|
for (const item of items)
|
|
2824
3298
|
this.rememberItemConversationId(conversation.id, item);
|
|
3299
|
+
this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
|
|
2825
3300
|
const lastMessage = [...items].reverse().find((item) => item.text?.trim());
|
|
2826
3301
|
if (lastMessage?.text && !conversation.lastMessagePreview) {
|
|
2827
3302
|
conversation.lastMessagePreview = previewText(lastMessage.text);
|
|
@@ -2890,6 +3365,9 @@ export class AgentWorkspaceProxy {
|
|
|
2890
3365
|
rememberTurnConversationId(conversationId, turnId) {
|
|
2891
3366
|
this.currentTurnIds.set(conversationId, turnId);
|
|
2892
3367
|
this.turnConversationIds.set(turnId, conversationId);
|
|
3368
|
+
const conversation = this.conversations.get(conversationId);
|
|
3369
|
+
if (conversation)
|
|
3370
|
+
conversation.runningTurnId = turnId;
|
|
2893
3371
|
}
|
|
2894
3372
|
forgetCurrentTurn(conversationId, turnId) {
|
|
2895
3373
|
const currentTurnId = this.currentTurnIds.get(conversationId);
|
|
@@ -2898,6 +3376,9 @@ export class AgentWorkspaceProxy {
|
|
|
2898
3376
|
this.turnConversationIds.delete(turnId);
|
|
2899
3377
|
if (currentTurnId && currentTurnId !== turnId)
|
|
2900
3378
|
this.turnConversationIds.delete(currentTurnId);
|
|
3379
|
+
const conversation = this.conversations.get(conversationId);
|
|
3380
|
+
if (conversation)
|
|
3381
|
+
conversation.runningTurnId = undefined;
|
|
2901
3382
|
}
|
|
2902
3383
|
rememberItemConversationId(conversationId, item) {
|
|
2903
3384
|
const keys = [
|
|
@@ -2937,28 +3418,51 @@ export class AgentWorkspaceProxy {
|
|
|
2937
3418
|
}
|
|
2938
3419
|
cancelPendingPermissions(conversationId) {
|
|
2939
3420
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
3421
|
+
const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
|
|
3422
|
+
if (conversationId && ownerConversationId && ownerConversationId !== conversationId)
|
|
3423
|
+
continue;
|
|
2940
3424
|
clearTimeout(waiter.timer);
|
|
2941
3425
|
waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
|
|
3426
|
+
if (ownerConversationId) {
|
|
3427
|
+
this.markPermission(ownerConversationId, requestId, {
|
|
3428
|
+
permissionOutcome: "cancelled",
|
|
3429
|
+
optionId: "cancelled",
|
|
3430
|
+
permissionLive: false,
|
|
3431
|
+
permissionPending: false,
|
|
3432
|
+
permissionExpired: false,
|
|
3433
|
+
permissionError: "已停止",
|
|
3434
|
+
});
|
|
3435
|
+
}
|
|
3436
|
+
this.permissionWaiters.delete(requestId);
|
|
2942
3437
|
this.pendingPermissions.delete(requestId);
|
|
2943
3438
|
this.permissionSources.delete(requestId);
|
|
2944
3439
|
}
|
|
2945
|
-
this.permissionWaiters.clear();
|
|
2946
3440
|
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
3441
|
+
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3442
|
+
if (conversationId && pending?.conversationId && pending.conversationId !== conversationId)
|
|
3443
|
+
continue;
|
|
2947
3444
|
clearTimeout(waiter.timer);
|
|
2948
3445
|
waiter.resolve(formatStructuredInputResponse({}));
|
|
2949
|
-
const pending = this.pendingStructuredInputs.get(requestId);
|
|
2950
3446
|
if (pending) {
|
|
2951
3447
|
this.markStructuredInput(pending.conversationId, requestId, {
|
|
2952
3448
|
inputPending: false,
|
|
2953
3449
|
inputError: "已停止",
|
|
2954
3450
|
});
|
|
2955
3451
|
}
|
|
3452
|
+
this.structuredInputWaiters.delete(requestId);
|
|
2956
3453
|
this.pendingStructuredInputs.delete(requestId);
|
|
2957
3454
|
}
|
|
2958
|
-
this.structuredInputWaiters.clear();
|
|
2959
3455
|
if (conversationId)
|
|
2960
3456
|
this.updateConversationStatus(conversationId, "idle");
|
|
2961
3457
|
}
|
|
3458
|
+
conversationIdForPermissionRequest(requestId) {
|
|
3459
|
+
for (const [conversationId, timeline] of this.timelines) {
|
|
3460
|
+
if (timeline.some((item) => item.type === "permission" && item.permission?.requestId === requestId)) {
|
|
3461
|
+
return conversationId;
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
return undefined;
|
|
3465
|
+
}
|
|
2962
3466
|
extractSessionId(value) {
|
|
2963
3467
|
const raw = asRecord(value);
|
|
2964
3468
|
if (!raw)
|