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.
- 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 +16 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +518 -24
- 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 +6361 -2337
- package/dist/shared-protocol/src/index.js +69 -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 +567 -25
- 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,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:
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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
|
|
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
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
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
|
-
|
|
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)
|