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
|
@@ -20,6 +20,7 @@ type AgentPermissionMode = "read_only" | "workspace_write" | "full_access";
|
|
|
20
20
|
type AgentCollaborationMode = "default" | "plan";
|
|
21
21
|
type AgentCommandExecutionKind = "prompt" | "native" | "local_ui";
|
|
22
22
|
type AgentCommandSource = "built_in" | "custom" | "project" | "user" | "linkshell";
|
|
23
|
+
type AgentSyncSource = "device" | "device-history" | "device-live" | "app-server" | "cache";
|
|
23
24
|
|
|
24
25
|
interface AgentContentBlock {
|
|
25
26
|
type: "text" | "image";
|
|
@@ -165,6 +166,11 @@ interface AgentConversation {
|
|
|
165
166
|
collaborationMode?: AgentCollaborationMode;
|
|
166
167
|
status: AgentStatus;
|
|
167
168
|
archived: boolean;
|
|
169
|
+
timelineRevision?: number;
|
|
170
|
+
historyComplete?: boolean;
|
|
171
|
+
runningTurnId?: string;
|
|
172
|
+
source?: AgentSyncSource;
|
|
173
|
+
canonical?: boolean;
|
|
168
174
|
lastMessagePreview?: string;
|
|
169
175
|
lastActivityAt: number;
|
|
170
176
|
createdAt: number;
|
|
@@ -175,6 +181,9 @@ interface AgentTimelineItem {
|
|
|
175
181
|
conversationId: string;
|
|
176
182
|
type: "message" | "tool_call" | "plan" | "permission" | "status" | "error";
|
|
177
183
|
kind?: AgentTimelineKind;
|
|
184
|
+
revision?: number;
|
|
185
|
+
source?: AgentSyncSource;
|
|
186
|
+
canonical?: boolean;
|
|
178
187
|
turnId?: string;
|
|
179
188
|
itemId?: string;
|
|
180
189
|
role?: "user" | "assistant" | "system";
|
|
@@ -206,15 +215,51 @@ interface PendingStructuredInputWaiter {
|
|
|
206
215
|
source?: string;
|
|
207
216
|
}
|
|
208
217
|
|
|
218
|
+
interface AgentRevisionEvent {
|
|
219
|
+
revision: number;
|
|
220
|
+
item?: AgentTimelineItem;
|
|
221
|
+
conversation?: AgentConversation;
|
|
222
|
+
}
|
|
223
|
+
|
|
209
224
|
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
210
225
|
const MAX_TIMELINE_ITEMS = 200;
|
|
211
226
|
const MAX_SNAPSHOT_ITEMS = 80;
|
|
212
227
|
const MAX_SNAPSHOT_TEXT_BYTES = 128 * 1024;
|
|
228
|
+
const MAX_DELTA_EVENTS = 500;
|
|
229
|
+
const HISTORY_PAGE_MAX_ITEMS = 500;
|
|
213
230
|
|
|
214
231
|
function id(prefix: string): string {
|
|
215
232
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
216
233
|
}
|
|
217
234
|
|
|
235
|
+
function clampHistoryCursor(value: string | undefined, fallback: number, max: number): number {
|
|
236
|
+
if (!value) return fallback;
|
|
237
|
+
const parsed = Number.parseInt(value, 10);
|
|
238
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
239
|
+
return Math.max(0, Math.min(max, parsed));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function appServerText(value: unknown): string | undefined {
|
|
243
|
+
if (typeof value === "string") {
|
|
244
|
+
const text = value.trim();
|
|
245
|
+
return text || undefined;
|
|
246
|
+
}
|
|
247
|
+
if (Array.isArray(value)) {
|
|
248
|
+
const text = value
|
|
249
|
+
.map((part) => appServerText(part))
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
.join("\n")
|
|
252
|
+
.trim();
|
|
253
|
+
return text || undefined;
|
|
254
|
+
}
|
|
255
|
+
const record = asRecord(value);
|
|
256
|
+
if (!record) return undefined;
|
|
257
|
+
if (typeof record.text === "string") return appServerText(record.text);
|
|
258
|
+
if (typeof record.content === "string") return appServerText(record.content);
|
|
259
|
+
if (typeof record.message === "string") return appServerText(record.message);
|
|
260
|
+
return appServerText(record.content ?? record.message ?? record.parts);
|
|
261
|
+
}
|
|
262
|
+
|
|
218
263
|
function stringify(value: unknown): string {
|
|
219
264
|
if (typeof value === "string") return value;
|
|
220
265
|
try {
|
|
@@ -1346,6 +1391,8 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1346
1391
|
createdAt?: number;
|
|
1347
1392
|
lastActivityAt?: number;
|
|
1348
1393
|
archived?: boolean;
|
|
1394
|
+
status?: AgentStatus;
|
|
1395
|
+
runningTurnId?: string;
|
|
1349
1396
|
}> {
|
|
1350
1397
|
const raw = asRecord(value);
|
|
1351
1398
|
const sessionsValue =
|
|
@@ -1362,6 +1409,8 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1362
1409
|
createdAt?: number;
|
|
1363
1410
|
lastActivityAt?: number;
|
|
1364
1411
|
archived?: boolean;
|
|
1412
|
+
status?: AgentStatus;
|
|
1413
|
+
runningTurnId?: string;
|
|
1365
1414
|
}> = [];
|
|
1366
1415
|
for (const entry of sessionsValue) {
|
|
1367
1416
|
const session = asRecord(entry);
|
|
@@ -1381,11 +1430,23 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1381
1430
|
createdAt: parseTimestamp(source.createdAt ?? source.created_at),
|
|
1382
1431
|
lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
|
|
1383
1432
|
archived: typeof source.archived === "boolean" ? source.archived : undefined,
|
|
1433
|
+
status: normalizeAgentStatus(firstString(source, ["status", "state", "phase"])),
|
|
1434
|
+
runningTurnId: firstString(source, ["runningTurnId", "running_turn_id", "turnId", "activeTurnId"]),
|
|
1384
1435
|
});
|
|
1385
1436
|
}
|
|
1386
1437
|
return result;
|
|
1387
1438
|
}
|
|
1388
1439
|
|
|
1440
|
+
function normalizeAgentStatus(value: string | undefined): AgentStatus | undefined {
|
|
1441
|
+
if (!value) return undefined;
|
|
1442
|
+
const normalized = value.toLowerCase();
|
|
1443
|
+
if (normalized === "running" || normalized === "in_progress" || normalized === "busy") return "running";
|
|
1444
|
+
if (normalized === "waiting_permission" || normalized === "waiting" || normalized === "blocked") return "waiting_permission";
|
|
1445
|
+
if (normalized === "error" || normalized === "failed") return "error";
|
|
1446
|
+
if (normalized === "idle" || normalized === "completed" || normalized === "done") return "idle";
|
|
1447
|
+
return undefined;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1389
1450
|
export class AgentWorkspaceProxy {
|
|
1390
1451
|
private clients = new Map<AgentProvider, AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient>();
|
|
1391
1452
|
private agentProtocols = new Map<AgentProvider, AgentProtocol>();
|
|
@@ -1399,6 +1460,8 @@ export class AgentWorkspaceProxy {
|
|
|
1399
1460
|
private conversations = new Map<string, AgentConversation>();
|
|
1400
1461
|
private conversationByAgentSessionId = new Map<string, string>();
|
|
1401
1462
|
private timelines = new Map<string, AgentTimelineItem[]>();
|
|
1463
|
+
private conversationRevisions = new Map<string, number>();
|
|
1464
|
+
private revisionEvents = new Map<string, AgentRevisionEvent[]>();
|
|
1402
1465
|
private toolOutputBuffers = new Map<string, string>();
|
|
1403
1466
|
private pendingPermissions = new Map<string, AgentPermission>();
|
|
1404
1467
|
private permissionWaiters = new Map<string, PendingPermissionWaiter>();
|
|
@@ -1439,7 +1502,7 @@ export class AgentWorkspaceProxy {
|
|
|
1439
1502
|
this.input.send(createEnvelope({
|
|
1440
1503
|
type: "agent.v2.conversation.list.result",
|
|
1441
1504
|
hostDeviceId: this.input.hostDeviceId,
|
|
1442
|
-
payload: { conversations },
|
|
1505
|
+
payload: { conversations: conversations.map((conversation) => this.conversationSnapshot(conversation)) },
|
|
1443
1506
|
}));
|
|
1444
1507
|
break;
|
|
1445
1508
|
}
|
|
@@ -1448,6 +1511,16 @@ export class AgentWorkspaceProxy {
|
|
|
1448
1511
|
this.sendSnapshot(payload.conversationId);
|
|
1449
1512
|
break;
|
|
1450
1513
|
}
|
|
1514
|
+
case "agent.v2.history.request": {
|
|
1515
|
+
const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
|
|
1516
|
+
await this.sendHistoryPage(payload);
|
|
1517
|
+
break;
|
|
1518
|
+
}
|
|
1519
|
+
case "agent.v2.delta.request": {
|
|
1520
|
+
const payload = parseTypedPayload("agent.v2.delta.request", envelope.payload);
|
|
1521
|
+
this.sendDelta(payload);
|
|
1522
|
+
break;
|
|
1523
|
+
}
|
|
1451
1524
|
case "agent.v2.prompt": {
|
|
1452
1525
|
const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
|
|
1453
1526
|
await this.sendPrompt(payload);
|
|
@@ -1656,14 +1729,20 @@ export class AgentWorkspaceProxy {
|
|
|
1656
1729
|
reasoningEffort: existing?.reasoningEffort,
|
|
1657
1730
|
permissionMode: existing?.permissionMode,
|
|
1658
1731
|
collaborationMode: existing?.collaborationMode,
|
|
1659
|
-
status: existing?.status ?? "idle",
|
|
1732
|
+
status: remote.status ?? existing?.status ?? "idle",
|
|
1660
1733
|
archived: remote.archived ?? existing?.archived ?? false,
|
|
1734
|
+
timelineRevision: existing?.timelineRevision ?? this.getRevision(conversationId),
|
|
1735
|
+
historyComplete: existing?.historyComplete ?? false,
|
|
1736
|
+
runningTurnId: remote.runningTurnId ?? this.currentTurnIds.get(conversationId),
|
|
1737
|
+
source: "device",
|
|
1738
|
+
canonical: true,
|
|
1661
1739
|
lastMessagePreview: existing?.lastMessagePreview,
|
|
1662
1740
|
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
1663
1741
|
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
1664
1742
|
};
|
|
1665
1743
|
this.conversations.set(conversation.id, conversation);
|
|
1666
1744
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1745
|
+
if (remote.runningTurnId) this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
|
|
1667
1746
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1668
1747
|
}
|
|
1669
1748
|
}
|
|
@@ -1758,12 +1837,18 @@ export class AgentWorkspaceProxy {
|
|
|
1758
1837
|
}
|
|
1759
1838
|
this.hydrateStoredTimeline(existingConversation);
|
|
1760
1839
|
this.activeConversationId = existingConversation.id;
|
|
1840
|
+
const snapshot = this.latestSnapshot(existingConversation.id);
|
|
1761
1841
|
this.input.send(createEnvelope({
|
|
1762
1842
|
type: "agent.v2.conversation.opened",
|
|
1763
1843
|
hostDeviceId: this.input.hostDeviceId,
|
|
1764
1844
|
payload: {
|
|
1765
|
-
conversation: existingConversation,
|
|
1766
|
-
snapshot:
|
|
1845
|
+
conversation: this.conversationSnapshot(existingConversation),
|
|
1846
|
+
snapshot: snapshot.items,
|
|
1847
|
+
revision: this.getRevision(existingConversation.id),
|
|
1848
|
+
cursor: snapshot.cursor,
|
|
1849
|
+
hasMore: snapshot.hasMore,
|
|
1850
|
+
source: "device-history",
|
|
1851
|
+
canonical: true,
|
|
1767
1852
|
},
|
|
1768
1853
|
}));
|
|
1769
1854
|
return existingConversation;
|
|
@@ -1807,6 +1892,11 @@ export class AgentWorkspaceProxy {
|
|
|
1807
1892
|
collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
|
|
1808
1893
|
status: "idle",
|
|
1809
1894
|
archived: existingConversation?.archived ?? false,
|
|
1895
|
+
timelineRevision: existingConversation?.timelineRevision ?? this.getRevision(conversationId),
|
|
1896
|
+
historyComplete: existingConversation?.historyComplete ?? false,
|
|
1897
|
+
runningTurnId: this.currentTurnIds.get(conversationId),
|
|
1898
|
+
source: "device",
|
|
1899
|
+
canonical: true,
|
|
1810
1900
|
lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
|
|
1811
1901
|
lastActivityAt: now,
|
|
1812
1902
|
createdAt: existingConversation?.createdAt ?? now,
|
|
@@ -1816,10 +1906,19 @@ export class AgentWorkspaceProxy {
|
|
|
1816
1906
|
this.activeConversationId = conversation.id;
|
|
1817
1907
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1818
1908
|
this.hydrateStoredTimeline(conversation);
|
|
1909
|
+
const snapshot = this.latestSnapshot(conversation.id);
|
|
1819
1910
|
this.input.send(createEnvelope({
|
|
1820
1911
|
type: "agent.v2.conversation.opened",
|
|
1821
1912
|
hostDeviceId: this.input.hostDeviceId,
|
|
1822
|
-
payload: {
|
|
1913
|
+
payload: {
|
|
1914
|
+
conversation: this.conversationSnapshot(conversation),
|
|
1915
|
+
snapshot: snapshot.items,
|
|
1916
|
+
revision: this.getRevision(conversation.id),
|
|
1917
|
+
cursor: snapshot.cursor,
|
|
1918
|
+
hasMore: snapshot.hasMore,
|
|
1919
|
+
source: "device-history",
|
|
1920
|
+
canonical: true,
|
|
1921
|
+
},
|
|
1823
1922
|
}));
|
|
1824
1923
|
return conversation;
|
|
1825
1924
|
} catch (error) {
|
|
@@ -1855,6 +1954,10 @@ export class AgentWorkspaceProxy {
|
|
|
1855
1954
|
collaborationMode: payload.collaborationMode,
|
|
1856
1955
|
status: "error",
|
|
1857
1956
|
archived: false,
|
|
1957
|
+
timelineRevision: this.getRevision(fallbackId),
|
|
1958
|
+
historyComplete: false,
|
|
1959
|
+
source: "device",
|
|
1960
|
+
canonical: true,
|
|
1858
1961
|
lastMessagePreview: message,
|
|
1859
1962
|
lastActivityAt: now,
|
|
1860
1963
|
createdAt: now,
|
|
@@ -1871,7 +1974,14 @@ export class AgentWorkspaceProxy {
|
|
|
1871
1974
|
this.input.send(createEnvelope({
|
|
1872
1975
|
type: "agent.v2.conversation.opened",
|
|
1873
1976
|
hostDeviceId: this.input.hostDeviceId,
|
|
1874
|
-
payload: {
|
|
1977
|
+
payload: {
|
|
1978
|
+
conversation: this.conversationSnapshot(conversation),
|
|
1979
|
+
snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []),
|
|
1980
|
+
revision: this.getRevision(conversation.id),
|
|
1981
|
+
hasMore: false,
|
|
1982
|
+
source: "device",
|
|
1983
|
+
canonical: true,
|
|
1984
|
+
},
|
|
1875
1985
|
}));
|
|
1876
1986
|
return conversation;
|
|
1877
1987
|
}
|
|
@@ -2881,6 +2991,14 @@ export class AgentWorkspaceProxy {
|
|
|
2881
2991
|
this.permissionWaiters.delete(requestId);
|
|
2882
2992
|
this.permissionSources.delete(requestId);
|
|
2883
2993
|
resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
|
|
2994
|
+
this.markPermission(conversationId, requestId, {
|
|
2995
|
+
permissionOutcome: "cancelled",
|
|
2996
|
+
optionId: "cancelled",
|
|
2997
|
+
permissionLive: false,
|
|
2998
|
+
permissionPending: false,
|
|
2999
|
+
permissionExpired: true,
|
|
3000
|
+
permissionError: "等待授权超时",
|
|
3001
|
+
});
|
|
2884
3002
|
this.updateConversationStatus(conversationId, "idle");
|
|
2885
3003
|
}, PERMISSION_TIMEOUT_MS);
|
|
2886
3004
|
this.permissionWaiters.set(requestId, { resolve, timer });
|
|
@@ -2920,6 +3038,8 @@ export class AgentWorkspaceProxy {
|
|
|
2920
3038
|
this.markPermission(payload.conversationId, payload.requestId, {
|
|
2921
3039
|
permissionOutcome: payload.outcome,
|
|
2922
3040
|
optionId: selectedOptionId,
|
|
3041
|
+
permissionLive: false,
|
|
3042
|
+
permissionExpired: false,
|
|
2923
3043
|
permissionError: undefined,
|
|
2924
3044
|
permissionPending: false,
|
|
2925
3045
|
});
|
|
@@ -2980,6 +3100,228 @@ export class AgentWorkspaceProxy {
|
|
|
2980
3100
|
});
|
|
2981
3101
|
}
|
|
2982
3102
|
|
|
3103
|
+
private getRevision(conversationId: string): number {
|
|
3104
|
+
return this.conversationRevisions.get(conversationId) ??
|
|
3105
|
+
this.conversations.get(conversationId)?.timelineRevision ??
|
|
3106
|
+
0;
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
private setRevisionFloor(conversationId: string, revision: number): number {
|
|
3110
|
+
const nextRevision = Math.max(this.getRevision(conversationId), revision);
|
|
3111
|
+
this.conversationRevisions.set(conversationId, nextRevision);
|
|
3112
|
+
const conversation = this.conversations.get(conversationId);
|
|
3113
|
+
if (conversation) {
|
|
3114
|
+
conversation.timelineRevision = nextRevision;
|
|
3115
|
+
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
3116
|
+
}
|
|
3117
|
+
return nextRevision;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
private conversationSnapshot(conversation: AgentConversation): AgentConversation {
|
|
3121
|
+
return {
|
|
3122
|
+
...conversation,
|
|
3123
|
+
timelineRevision: this.getRevision(conversation.id),
|
|
3124
|
+
historyComplete: conversation.historyComplete ?? false,
|
|
3125
|
+
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
3126
|
+
source: conversation.source ?? "device",
|
|
3127
|
+
canonical: conversation.canonical ?? true,
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
private annotateTimelineItem(
|
|
3132
|
+
item: AgentTimelineItem,
|
|
3133
|
+
revision: number | undefined,
|
|
3134
|
+
source: AgentSyncSource,
|
|
3135
|
+
): AgentTimelineItem {
|
|
3136
|
+
return {
|
|
3137
|
+
...item,
|
|
3138
|
+
revision: revision ?? item.revision,
|
|
3139
|
+
source: item.source ?? source,
|
|
3140
|
+
canonical: item.canonical ?? true,
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
private recordRevisionEvent(
|
|
3145
|
+
conversationId: string,
|
|
3146
|
+
change: { item?: AgentTimelineItem; conversation?: AgentConversation },
|
|
3147
|
+
): AgentRevisionEvent {
|
|
3148
|
+
const revision = this.getRevision(conversationId) + 1;
|
|
3149
|
+
this.conversationRevisions.set(conversationId, revision);
|
|
3150
|
+
const conversation = this.conversations.get(conversationId);
|
|
3151
|
+
if (conversation) {
|
|
3152
|
+
conversation.timelineRevision = revision;
|
|
3153
|
+
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
3154
|
+
}
|
|
3155
|
+
const event: AgentRevisionEvent = {
|
|
3156
|
+
revision,
|
|
3157
|
+
item: change.item ? this.annotateTimelineItem(change.item, revision, "device-live") : undefined,
|
|
3158
|
+
conversation: change.conversation ? this.conversationSnapshot(change.conversation) : undefined,
|
|
3159
|
+
};
|
|
3160
|
+
const events = this.revisionEvents.get(conversationId) ?? [];
|
|
3161
|
+
events.push(event);
|
|
3162
|
+
if (events.length > MAX_DELTA_EVENTS) {
|
|
3163
|
+
events.splice(0, events.length - MAX_DELTA_EVENTS);
|
|
3164
|
+
}
|
|
3165
|
+
this.revisionEvents.set(conversationId, events);
|
|
3166
|
+
return event;
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
private storedTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): AgentTimelineItem[] {
|
|
3170
|
+
if (!conversation.agentSessionId) return [];
|
|
3171
|
+
const result = conversation.provider === "codex"
|
|
3172
|
+
? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd, { maxItems })
|
|
3173
|
+
: conversation.provider === "claude"
|
|
3174
|
+
? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id, { maxItems })
|
|
3175
|
+
: { items: [] };
|
|
3176
|
+
return result.items
|
|
3177
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3178
|
+
.map((item, index) => this.annotateTimelineItem(item as AgentTimelineItem, index + 1, "device-history"));
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
private canonicalTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): AgentTimelineItem[] {
|
|
3182
|
+
const merged = new Map<string, AgentTimelineItem>();
|
|
3183
|
+
for (const item of this.storedTimeline(conversation, maxItems)) {
|
|
3184
|
+
merged.set(item.id, item);
|
|
3185
|
+
}
|
|
3186
|
+
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
3187
|
+
merged.set(item.id, this.annotateTimelineItem(item, item.revision, item.source ?? "device-live"));
|
|
3188
|
+
}
|
|
3189
|
+
const items = [...merged.values()]
|
|
3190
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3191
|
+
.slice(-maxItems);
|
|
3192
|
+
this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
3193
|
+
return items;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
private async appServerTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): Promise<AgentTimelineItem[]> {
|
|
3197
|
+
if (conversation.provider !== "codex" || !conversation.agentSessionId) return [];
|
|
3198
|
+
if (this.protocolForProvider(conversation.provider) !== "codex-app-server") return [];
|
|
3199
|
+
const client = this.clientForProvider(conversation.provider);
|
|
3200
|
+
const listTurns = (client as { listTurns?: (input: { sessionId: string; limit?: number }) => Promise<unknown> } | undefined)?.listTurns;
|
|
3201
|
+
if (typeof listTurns !== "function") return [];
|
|
3202
|
+
try {
|
|
3203
|
+
const result = await listTurns.call(client, { sessionId: conversation.agentSessionId, limit: maxItems });
|
|
3204
|
+
return this.timelineFromAppServerTurns(conversation.id, conversation.agentSessionId, result);
|
|
3205
|
+
} catch (error) {
|
|
3206
|
+
if (this.input.verbose) {
|
|
3207
|
+
process.stderr.write(`[agent:v2] thread/turns/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
3208
|
+
}
|
|
3209
|
+
return [];
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
private timelineFromAppServerTurns(
|
|
3214
|
+
conversationId: string,
|
|
3215
|
+
agentSessionId: string,
|
|
3216
|
+
value: unknown,
|
|
3217
|
+
): AgentTimelineItem[] {
|
|
3218
|
+
const raw = asRecord(value);
|
|
3219
|
+
const turns =
|
|
3220
|
+
Array.isArray(value) ? value :
|
|
3221
|
+
Array.isArray(raw?.turns) ? raw.turns :
|
|
3222
|
+
Array.isArray(raw?.items) ? raw.items :
|
|
3223
|
+
Array.isArray(raw?.entries) ? raw.entries :
|
|
3224
|
+
[];
|
|
3225
|
+
const items: AgentTimelineItem[] = [];
|
|
3226
|
+
turns.forEach((entry, index) => {
|
|
3227
|
+
const turn = asRecord(entry);
|
|
3228
|
+
if (!turn) return;
|
|
3229
|
+
const turnId = firstString(turn, ["id", "turnId", "turn_id"]) ?? `turn-${index + 1}`;
|
|
3230
|
+
const createdAt = parseTimestamp(turn.createdAt ?? turn.created_at ?? turn.startedAt ?? turn.started_at) ?? Date.now() + index;
|
|
3231
|
+
const updatedAt = parseTimestamp(turn.updatedAt ?? turn.updated_at ?? turn.completedAt ?? turn.completed_at);
|
|
3232
|
+
const userText = appServerText(turn.input ?? turn.prompt ?? turn.user ?? turn.userMessage ?? turn.request);
|
|
3233
|
+
if (userText) {
|
|
3234
|
+
items.push({
|
|
3235
|
+
id: `app-server:${agentSessionId}:${turnId}:user`,
|
|
3236
|
+
conversationId,
|
|
3237
|
+
type: "message",
|
|
3238
|
+
kind: "chat",
|
|
3239
|
+
turnId,
|
|
3240
|
+
role: "user",
|
|
3241
|
+
content: [{ type: "text", text: userText }],
|
|
3242
|
+
text: userText,
|
|
3243
|
+
createdAt,
|
|
3244
|
+
updatedAt,
|
|
3245
|
+
metadata: { source: "app-server", provider: "codex" },
|
|
3246
|
+
});
|
|
3247
|
+
}
|
|
3248
|
+
const assistantText = appServerText(turn.output ?? turn.response ?? turn.assistant ?? turn.assistantMessage ?? turn.result);
|
|
3249
|
+
if (assistantText) {
|
|
3250
|
+
items.push({
|
|
3251
|
+
id: `app-server:${agentSessionId}:${turnId}:assistant`,
|
|
3252
|
+
conversationId,
|
|
3253
|
+
type: "message",
|
|
3254
|
+
kind: "chat",
|
|
3255
|
+
turnId,
|
|
3256
|
+
role: "assistant",
|
|
3257
|
+
content: [{ type: "text", text: assistantText }],
|
|
3258
|
+
text: assistantText,
|
|
3259
|
+
createdAt: updatedAt ?? createdAt + 1,
|
|
3260
|
+
updatedAt,
|
|
3261
|
+
metadata: { source: "app-server", provider: "codex" },
|
|
3262
|
+
});
|
|
3263
|
+
}
|
|
3264
|
+
const nestedItems = Array.isArray(turn.items) ? turn.items : Array.isArray(turn.messages) ? turn.messages : [];
|
|
3265
|
+
for (const nested of nestedItems) {
|
|
3266
|
+
const nestedRecord = asRecord(nested);
|
|
3267
|
+
if (!nestedRecord) continue;
|
|
3268
|
+
const text = appServerText(nestedRecord.content ?? nestedRecord.text ?? nestedRecord.message);
|
|
3269
|
+
const role = nestedRecord.role === "user" || nestedRecord.role === "assistant" || nestedRecord.role === "system"
|
|
3270
|
+
? nestedRecord.role
|
|
3271
|
+
: undefined;
|
|
3272
|
+
if (!text || !role) continue;
|
|
3273
|
+
const itemId = firstString(nestedRecord, ["id", "itemId", "messageId"]) ?? `${role}-${items.length + 1}`;
|
|
3274
|
+
items.push({
|
|
3275
|
+
id: `app-server:${agentSessionId}:${turnId}:${itemId}`,
|
|
3276
|
+
conversationId,
|
|
3277
|
+
type: "message",
|
|
3278
|
+
kind: "chat",
|
|
3279
|
+
turnId,
|
|
3280
|
+
itemId,
|
|
3281
|
+
role,
|
|
3282
|
+
content: [{ type: "text", text }],
|
|
3283
|
+
text,
|
|
3284
|
+
createdAt: parseTimestamp(nestedRecord.createdAt ?? nestedRecord.created_at) ?? createdAt + items.length,
|
|
3285
|
+
updatedAt: parseTimestamp(nestedRecord.updatedAt ?? nestedRecord.updated_at),
|
|
3286
|
+
metadata: { source: "app-server", provider: "codex" },
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
});
|
|
3290
|
+
return items
|
|
3291
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3292
|
+
.map((item, index) => this.annotateTimelineItem(item, index + 1, "app-server"));
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
private mergeCanonicalTimelineItems(items: AgentTimelineItem[], maxItems: number): AgentTimelineItem[] {
|
|
3296
|
+
const byKey = new Map<string, AgentTimelineItem>();
|
|
3297
|
+
for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
|
|
3298
|
+
const key = item.type === "message" && item.role && item.text
|
|
3299
|
+
? `${item.role}:${item.text.replace(/\s+/g, " ").trim().slice(0, 500)}`
|
|
3300
|
+
: item.id;
|
|
3301
|
+
const existing = byKey.get(key);
|
|
3302
|
+
if (!existing || item.source === "app-server" || (item.updatedAt ?? item.createdAt) >= (existing.updatedAt ?? existing.createdAt)) {
|
|
3303
|
+
byKey.set(key, item);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
return [...byKey.values()]
|
|
3307
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3308
|
+
.slice(-maxItems);
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
private latestSnapshot(conversationId: string): {
|
|
3312
|
+
items: AgentTimelineItem[];
|
|
3313
|
+
cursor?: string;
|
|
3314
|
+
hasMore: boolean;
|
|
3315
|
+
} {
|
|
3316
|
+
const timeline = this.timelines.get(conversationId) ?? [];
|
|
3317
|
+
const start = Math.max(0, timeline.length - MAX_SNAPSHOT_ITEMS);
|
|
3318
|
+
return {
|
|
3319
|
+
items: timeline.slice(start).map((item) => snapshotTimelineItem(item)),
|
|
3320
|
+
cursor: start > 0 ? String(start) : undefined,
|
|
3321
|
+
hasMore: start > 0,
|
|
3322
|
+
};
|
|
3323
|
+
}
|
|
3324
|
+
|
|
2983
3325
|
private addItem(conversationId: string, item: AgentTimelineItem): void {
|
|
2984
3326
|
this.rememberItemConversationId(conversationId, item);
|
|
2985
3327
|
const timeline = this.timelines.get(conversationId) ?? [];
|
|
@@ -3111,6 +3453,17 @@ export class AgentWorkspaceProxy {
|
|
|
3111
3453
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3112
3454
|
.slice(-MAX_TIMELINE_ITEMS),
|
|
3113
3455
|
);
|
|
3456
|
+
const oldRevision = this.conversationRevisions.get(oldId) ?? 0;
|
|
3457
|
+
if (oldRevision > 0) {
|
|
3458
|
+
this.conversationRevisions.delete(oldId);
|
|
3459
|
+
this.setRevisionFloor(newId, oldRevision);
|
|
3460
|
+
}
|
|
3461
|
+
const oldEvents = this.revisionEvents.get(oldId);
|
|
3462
|
+
if (oldEvents) {
|
|
3463
|
+
this.revisionEvents.delete(oldId);
|
|
3464
|
+
const existingEvents = this.revisionEvents.get(newId) ?? [];
|
|
3465
|
+
this.revisionEvents.set(newId, [...existingEvents, ...oldEvents].slice(-MAX_DELTA_EVENTS));
|
|
3466
|
+
}
|
|
3114
3467
|
|
|
3115
3468
|
for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
|
|
3116
3469
|
if (conversationId === oldId) {
|
|
@@ -3146,18 +3499,48 @@ export class AgentWorkspaceProxy {
|
|
|
3146
3499
|
|
|
3147
3500
|
private emitItem(conversationId: string, item: AgentTimelineItem): void {
|
|
3148
3501
|
const conversation = this.conversations.get(conversationId);
|
|
3502
|
+
const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
|
|
3503
|
+
const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
|
|
3149
3504
|
this.input.send(createEnvelope({
|
|
3150
3505
|
type: "agent.v2.event",
|
|
3151
3506
|
hostDeviceId: this.input.hostDeviceId,
|
|
3152
|
-
payload: {
|
|
3507
|
+
payload: {
|
|
3508
|
+
conversationId,
|
|
3509
|
+
conversation: event.conversation,
|
|
3510
|
+
item: event.item,
|
|
3511
|
+
revision: event.revision,
|
|
3512
|
+
source: "device-live",
|
|
3513
|
+
canonical: true,
|
|
3514
|
+
},
|
|
3153
3515
|
}));
|
|
3154
3516
|
}
|
|
3155
3517
|
|
|
3156
3518
|
private emitConversation(conversation: AgentConversation): void {
|
|
3519
|
+
const event = this.recordRevisionEvent(conversation.id, { conversation });
|
|
3157
3520
|
this.input.send(createEnvelope({
|
|
3158
3521
|
type: "agent.v2.event",
|
|
3159
3522
|
hostDeviceId: this.input.hostDeviceId,
|
|
3160
|
-
payload: {
|
|
3523
|
+
payload: {
|
|
3524
|
+
conversationId: conversation.id,
|
|
3525
|
+
conversation: event.conversation,
|
|
3526
|
+
revision: event.revision,
|
|
3527
|
+
source: "device-live",
|
|
3528
|
+
canonical: true,
|
|
3529
|
+
},
|
|
3530
|
+
}));
|
|
3531
|
+
this.input.send(createEnvelope({
|
|
3532
|
+
type: "agent.v2.running_state",
|
|
3533
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3534
|
+
payload: {
|
|
3535
|
+
conversationId: conversation.id,
|
|
3536
|
+
status: conversation.status,
|
|
3537
|
+
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
3538
|
+
revision: event.revision,
|
|
3539
|
+
error: conversation.status === "error" ? conversation.lastMessagePreview : undefined,
|
|
3540
|
+
updatedAt: conversation.lastActivityAt,
|
|
3541
|
+
source: "device-live",
|
|
3542
|
+
canonical: true,
|
|
3543
|
+
},
|
|
3161
3544
|
}));
|
|
3162
3545
|
}
|
|
3163
3546
|
|
|
@@ -3206,17 +3589,143 @@ export class AgentWorkspaceProxy {
|
|
|
3206
3589
|
const conversation = this.conversations.get(this.activeConversationId);
|
|
3207
3590
|
if (conversation) this.hydrateStoredTimeline(conversation);
|
|
3208
3591
|
}
|
|
3209
|
-
const conversations = [...this.conversations.values()];
|
|
3210
|
-
const
|
|
3211
|
-
? snapshotTimelineItems(this.timelines.get(conversationId) ?? [])
|
|
3212
|
-
: [];
|
|
3592
|
+
const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
|
|
3593
|
+
const snapshot = conversationId ? this.latestSnapshot(conversationId) : { items: [], cursor: undefined, hasMore: false };
|
|
3213
3594
|
this.input.send(createEnvelope({
|
|
3214
3595
|
type: "agent.v2.snapshot",
|
|
3215
3596
|
hostDeviceId: this.input.hostDeviceId,
|
|
3216
3597
|
payload: {
|
|
3217
3598
|
conversations,
|
|
3218
3599
|
activeConversationId: this.activeConversationId,
|
|
3219
|
-
items,
|
|
3600
|
+
items: snapshot.items,
|
|
3601
|
+
revision: conversationId ? this.getRevision(conversationId) : undefined,
|
|
3602
|
+
cursor: snapshot.cursor,
|
|
3603
|
+
hasMore: snapshot.hasMore,
|
|
3604
|
+
source: "device-history",
|
|
3605
|
+
canonical: true,
|
|
3606
|
+
},
|
|
3607
|
+
}));
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
private async sendHistoryPage(payload: {
|
|
3611
|
+
conversationId: string;
|
|
3612
|
+
cursor?: string;
|
|
3613
|
+
limit?: number;
|
|
3614
|
+
direction?: "older" | "newer";
|
|
3615
|
+
}): Promise<void> {
|
|
3616
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
3617
|
+
if (!conversation) return;
|
|
3618
|
+
const limit = Math.min(Math.max(payload.limit ?? MAX_SNAPSHOT_ITEMS, 1), 200);
|
|
3619
|
+
const items = this.mergeCanonicalTimelineItems([
|
|
3620
|
+
...this.canonicalTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3621
|
+
...await this.appServerTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3622
|
+
], HISTORY_PAGE_MAX_ITEMS);
|
|
3623
|
+
this.timelines.set(
|
|
3624
|
+
conversation.id,
|
|
3625
|
+
items.slice(-MAX_TIMELINE_ITEMS).map((item) => this.annotateTimelineItem(item, item.revision, item.source ?? "device-history")),
|
|
3626
|
+
);
|
|
3627
|
+
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
3628
|
+
this.rememberItemConversationId(conversation.id, item);
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
const direction = payload.direction ?? "older";
|
|
3632
|
+
let page: AgentTimelineItem[];
|
|
3633
|
+
let cursor: string | undefined;
|
|
3634
|
+
let hasMore = false;
|
|
3635
|
+
if (direction === "newer") {
|
|
3636
|
+
const start = clampHistoryCursor(payload.cursor, 0, items.length);
|
|
3637
|
+
const end = Math.min(items.length, start + limit);
|
|
3638
|
+
page = items.slice(start, end);
|
|
3639
|
+
hasMore = end < items.length;
|
|
3640
|
+
cursor = hasMore ? String(end) : undefined;
|
|
3641
|
+
} else {
|
|
3642
|
+
const end = clampHistoryCursor(payload.cursor, items.length, items.length);
|
|
3643
|
+
const start = Math.max(0, end - limit);
|
|
3644
|
+
page = items.slice(start, end);
|
|
3645
|
+
hasMore = start > 0;
|
|
3646
|
+
cursor = hasMore ? String(start) : undefined;
|
|
3647
|
+
}
|
|
3648
|
+
conversation.historyComplete = !hasMore;
|
|
3649
|
+
const revision = this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
3650
|
+
this.input.send(createEnvelope({
|
|
3651
|
+
type: "agent.v2.history.page",
|
|
3652
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3653
|
+
payload: {
|
|
3654
|
+
conversationId: conversation.id,
|
|
3655
|
+
conversation: this.conversationSnapshot(conversation),
|
|
3656
|
+
items: page.map((item) => snapshotTimelineItem(item)),
|
|
3657
|
+
revision,
|
|
3658
|
+
cursor,
|
|
3659
|
+
hasMore,
|
|
3660
|
+
source: "device-history",
|
|
3661
|
+
canonical: true,
|
|
3662
|
+
},
|
|
3663
|
+
}));
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
private sendDelta(payload: {
|
|
3667
|
+
conversationId: string;
|
|
3668
|
+
sinceRevision?: number;
|
|
3669
|
+
limit?: number;
|
|
3670
|
+
}): void {
|
|
3671
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
3672
|
+
if (!conversation) return;
|
|
3673
|
+
this.hydrateStoredTimeline(conversation);
|
|
3674
|
+
const sinceRevision = payload.sinceRevision ?? 0;
|
|
3675
|
+
const limit = Math.min(Math.max(payload.limit ?? 100, 1), 500);
|
|
3676
|
+
const events = this.revisionEvents.get(conversation.id) ?? [];
|
|
3677
|
+
const oldestAvailable = events[0]?.revision ?? this.getRevision(conversation.id);
|
|
3678
|
+
const newestRevision = this.getRevision(conversation.id);
|
|
3679
|
+
const reset = sinceRevision > 0 &&
|
|
3680
|
+
(
|
|
3681
|
+
sinceRevision > newestRevision ||
|
|
3682
|
+
(
|
|
3683
|
+
sinceRevision < newestRevision &&
|
|
3684
|
+
(events.length === 0 || sinceRevision < oldestAvailable - 1)
|
|
3685
|
+
)
|
|
3686
|
+
);
|
|
3687
|
+
|
|
3688
|
+
if (reset) {
|
|
3689
|
+
const snapshot = this.latestSnapshot(conversation.id);
|
|
3690
|
+
this.input.send(createEnvelope({
|
|
3691
|
+
type: "agent.v2.delta",
|
|
3692
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3693
|
+
payload: {
|
|
3694
|
+
conversationId: conversation.id,
|
|
3695
|
+
conversation: this.conversationSnapshot(conversation),
|
|
3696
|
+
items: snapshot.items,
|
|
3697
|
+
sinceRevision,
|
|
3698
|
+
revision: newestRevision,
|
|
3699
|
+
reset: true,
|
|
3700
|
+
cursor: snapshot.cursor,
|
|
3701
|
+
hasMore: snapshot.hasMore,
|
|
3702
|
+
source: "device-history",
|
|
3703
|
+
canonical: true,
|
|
3704
|
+
},
|
|
3705
|
+
}));
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
const changed = events
|
|
3710
|
+
.filter((event) => event.revision > sinceRevision)
|
|
3711
|
+
.slice(-limit);
|
|
3712
|
+
const itemsById = new Map<string, AgentTimelineItem>();
|
|
3713
|
+
for (const event of changed) {
|
|
3714
|
+
if (event.item) itemsById.set(event.item.id, event.item);
|
|
3715
|
+
}
|
|
3716
|
+
this.input.send(createEnvelope({
|
|
3717
|
+
type: "agent.v2.delta",
|
|
3718
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3719
|
+
payload: {
|
|
3720
|
+
conversationId: conversation.id,
|
|
3721
|
+
conversation: this.conversationSnapshot(conversation),
|
|
3722
|
+
items: [...itemsById.values()].map((item) => snapshotTimelineItem(item)),
|
|
3723
|
+
sinceRevision,
|
|
3724
|
+
revision: newestRevision,
|
|
3725
|
+
reset: false,
|
|
3726
|
+
hasMore: changed.length === limit && events.some((event) => event.revision > sinceRevision && event.revision < changed[0]!.revision),
|
|
3727
|
+
source: "device-live",
|
|
3728
|
+
canonical: true,
|
|
3220
3729
|
},
|
|
3221
3730
|
}));
|
|
3222
3731
|
}
|
|
@@ -3224,18 +3733,25 @@ export class AgentWorkspaceProxy {
|
|
|
3224
3733
|
private hydrateStoredTimeline(conversation: AgentConversation): void {
|
|
3225
3734
|
if (!conversation.agentSessionId) return;
|
|
3226
3735
|
const existing = this.timelines.get(conversation.id) ?? [];
|
|
3227
|
-
if (existing.length > 0)
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
const
|
|
3736
|
+
if (existing.length > 0) {
|
|
3737
|
+
this.setRevisionFloor(conversation.id, Math.max(
|
|
3738
|
+
existing.length,
|
|
3739
|
+
...existing.map((item) => item.revision ?? 0),
|
|
3740
|
+
));
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
const stored = this.storedTimeline(conversation, MAX_TIMELINE_ITEMS);
|
|
3744
|
+
conversation.historyComplete = stored.length <= MAX_SNAPSHOT_ITEMS;
|
|
3745
|
+
if (stored.length === 0) {
|
|
3746
|
+
this.setRevisionFloor(conversation.id, this.getRevision(conversation.id));
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
const items = stored
|
|
3235
3750
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3236
|
-
.slice(-MAX_TIMELINE_ITEMS)
|
|
3751
|
+
.slice(-MAX_TIMELINE_ITEMS);
|
|
3237
3752
|
this.timelines.set(conversation.id, items);
|
|
3238
3753
|
for (const item of items) this.rememberItemConversationId(conversation.id, item);
|
|
3754
|
+
this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
|
|
3239
3755
|
const lastMessage = [...items].reverse().find((item) => item.text?.trim());
|
|
3240
3756
|
if (lastMessage?.text && !conversation.lastMessagePreview) {
|
|
3241
3757
|
conversation.lastMessagePreview = previewText(lastMessage.text);
|
|
@@ -3303,6 +3819,8 @@ export class AgentWorkspaceProxy {
|
|
|
3303
3819
|
private rememberTurnConversationId(conversationId: string, turnId: string): void {
|
|
3304
3820
|
this.currentTurnIds.set(conversationId, turnId);
|
|
3305
3821
|
this.turnConversationIds.set(turnId, conversationId);
|
|
3822
|
+
const conversation = this.conversations.get(conversationId);
|
|
3823
|
+
if (conversation) conversation.runningTurnId = turnId;
|
|
3306
3824
|
}
|
|
3307
3825
|
|
|
3308
3826
|
private forgetCurrentTurn(conversationId: string, turnId?: string): void {
|
|
@@ -3310,6 +3828,8 @@ export class AgentWorkspaceProxy {
|
|
|
3310
3828
|
this.currentTurnIds.delete(conversationId);
|
|
3311
3829
|
if (turnId) this.turnConversationIds.delete(turnId);
|
|
3312
3830
|
if (currentTurnId && currentTurnId !== turnId) this.turnConversationIds.delete(currentTurnId);
|
|
3831
|
+
const conversation = this.conversations.get(conversationId);
|
|
3832
|
+
if (conversation) conversation.runningTurnId = undefined;
|
|
3313
3833
|
}
|
|
3314
3834
|
|
|
3315
3835
|
private rememberItemConversationId(conversationId: string, item: AgentTimelineItem): void {
|
|
@@ -3351,32 +3871,54 @@ export class AgentWorkspaceProxy {
|
|
|
3351
3871
|
|
|
3352
3872
|
private cancelPendingPermissions(conversationId?: string): void {
|
|
3353
3873
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
3874
|
+
const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
|
|
3875
|
+
if (conversationId && ownerConversationId && ownerConversationId !== conversationId) continue;
|
|
3354
3876
|
clearTimeout(waiter.timer);
|
|
3355
3877
|
waiter.resolve(formatPermissionResponse(
|
|
3356
3878
|
this.permissionSources.get(requestId),
|
|
3357
3879
|
"cancelled",
|
|
3358
3880
|
"cancelled",
|
|
3359
3881
|
));
|
|
3882
|
+
if (ownerConversationId) {
|
|
3883
|
+
this.markPermission(ownerConversationId, requestId, {
|
|
3884
|
+
permissionOutcome: "cancelled",
|
|
3885
|
+
optionId: "cancelled",
|
|
3886
|
+
permissionLive: false,
|
|
3887
|
+
permissionPending: false,
|
|
3888
|
+
permissionExpired: false,
|
|
3889
|
+
permissionError: "已停止",
|
|
3890
|
+
});
|
|
3891
|
+
}
|
|
3892
|
+
this.permissionWaiters.delete(requestId);
|
|
3360
3893
|
this.pendingPermissions.delete(requestId);
|
|
3361
3894
|
this.permissionSources.delete(requestId);
|
|
3362
3895
|
}
|
|
3363
|
-
this.permissionWaiters.clear();
|
|
3364
3896
|
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
3897
|
+
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3898
|
+
if (conversationId && pending?.conversationId && pending.conversationId !== conversationId) continue;
|
|
3365
3899
|
clearTimeout(waiter.timer);
|
|
3366
3900
|
waiter.resolve(formatStructuredInputResponse({}));
|
|
3367
|
-
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3368
3901
|
if (pending) {
|
|
3369
3902
|
this.markStructuredInput(pending.conversationId, requestId, {
|
|
3370
3903
|
inputPending: false,
|
|
3371
3904
|
inputError: "已停止",
|
|
3372
3905
|
});
|
|
3373
3906
|
}
|
|
3907
|
+
this.structuredInputWaiters.delete(requestId);
|
|
3374
3908
|
this.pendingStructuredInputs.delete(requestId);
|
|
3375
3909
|
}
|
|
3376
|
-
this.structuredInputWaiters.clear();
|
|
3377
3910
|
if (conversationId) this.updateConversationStatus(conversationId, "idle");
|
|
3378
3911
|
}
|
|
3379
3912
|
|
|
3913
|
+
private conversationIdForPermissionRequest(requestId: string): string | undefined {
|
|
3914
|
+
for (const [conversationId, timeline] of this.timelines) {
|
|
3915
|
+
if (timeline.some((item) => item.type === "permission" && item.permission?.requestId === requestId)) {
|
|
3916
|
+
return conversationId;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
return undefined;
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3380
3922
|
private extractSessionId(value: unknown): string | undefined {
|
|
3381
3923
|
const raw = asRecord(value);
|
|
3382
3924
|
if (!raw) return undefined;
|