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
|
@@ -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,15 +1430,29 @@ 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>();
|
|
1392
1453
|
private providerCapabilities = new Map<AgentProvider, ProviderRuntimeCapabilities>();
|
|
1454
|
+
private providerCapabilityErrors = new Map<AgentProvider, string>();
|
|
1455
|
+
private capabilitiesRevision = 0;
|
|
1393
1456
|
private initialized = false;
|
|
1394
1457
|
private status: AgentStatus = "unavailable";
|
|
1395
1458
|
private error: string | undefined;
|
|
@@ -1399,6 +1462,8 @@ export class AgentWorkspaceProxy {
|
|
|
1399
1462
|
private conversations = new Map<string, AgentConversation>();
|
|
1400
1463
|
private conversationByAgentSessionId = new Map<string, string>();
|
|
1401
1464
|
private timelines = new Map<string, AgentTimelineItem[]>();
|
|
1465
|
+
private conversationRevisions = new Map<string, number>();
|
|
1466
|
+
private revisionEvents = new Map<string, AgentRevisionEvent[]>();
|
|
1402
1467
|
private toolOutputBuffers = new Map<string, string>();
|
|
1403
1468
|
private pendingPermissions = new Map<string, AgentPermission>();
|
|
1404
1469
|
private permissionWaiters = new Map<string, PendingPermissionWaiter>();
|
|
@@ -1439,7 +1504,7 @@ export class AgentWorkspaceProxy {
|
|
|
1439
1504
|
this.input.send(createEnvelope({
|
|
1440
1505
|
type: "agent.v2.conversation.list.result",
|
|
1441
1506
|
hostDeviceId: this.input.hostDeviceId,
|
|
1442
|
-
payload: { conversations },
|
|
1507
|
+
payload: { conversations: conversations.map((conversation) => this.conversationSnapshot(conversation)) },
|
|
1443
1508
|
}));
|
|
1444
1509
|
break;
|
|
1445
1510
|
}
|
|
@@ -1448,6 +1513,16 @@ export class AgentWorkspaceProxy {
|
|
|
1448
1513
|
this.sendSnapshot(payload.conversationId);
|
|
1449
1514
|
break;
|
|
1450
1515
|
}
|
|
1516
|
+
case "agent.v2.history.request": {
|
|
1517
|
+
const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
|
|
1518
|
+
await this.sendHistoryPage(payload);
|
|
1519
|
+
break;
|
|
1520
|
+
}
|
|
1521
|
+
case "agent.v2.delta.request": {
|
|
1522
|
+
const payload = parseTypedPayload("agent.v2.delta.request", envelope.payload);
|
|
1523
|
+
this.sendDelta(payload);
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1451
1526
|
case "agent.v2.prompt": {
|
|
1452
1527
|
const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
|
|
1453
1528
|
await this.sendPrompt(payload);
|
|
@@ -1614,8 +1689,12 @@ export class AgentWorkspaceProxy {
|
|
|
1614
1689
|
try {
|
|
1615
1690
|
const result = await listModels.call(client);
|
|
1616
1691
|
const runtimeCapabilities = parseModelListCapabilities(result);
|
|
1617
|
-
if (runtimeCapabilities)
|
|
1692
|
+
if (runtimeCapabilities) {
|
|
1693
|
+
this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
1694
|
+
this.providerCapabilityErrors.delete(provider);
|
|
1695
|
+
}
|
|
1618
1696
|
} catch (error) {
|
|
1697
|
+
this.providerCapabilityErrors.set(provider, error instanceof Error ? error.message : String(error));
|
|
1619
1698
|
if (this.input.verbose) {
|
|
1620
1699
|
process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1621
1700
|
}
|
|
@@ -1656,14 +1735,20 @@ export class AgentWorkspaceProxy {
|
|
|
1656
1735
|
reasoningEffort: existing?.reasoningEffort,
|
|
1657
1736
|
permissionMode: existing?.permissionMode,
|
|
1658
1737
|
collaborationMode: existing?.collaborationMode,
|
|
1659
|
-
status: existing?.status ?? "idle",
|
|
1738
|
+
status: remote.status ?? existing?.status ?? "idle",
|
|
1660
1739
|
archived: remote.archived ?? existing?.archived ?? false,
|
|
1740
|
+
timelineRevision: existing?.timelineRevision ?? this.getRevision(conversationId),
|
|
1741
|
+
historyComplete: existing?.historyComplete ?? false,
|
|
1742
|
+
runningTurnId: remote.runningTurnId ?? this.currentTurnIds.get(conversationId),
|
|
1743
|
+
source: "device",
|
|
1744
|
+
canonical: true,
|
|
1661
1745
|
lastMessagePreview: existing?.lastMessagePreview,
|
|
1662
1746
|
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
1663
1747
|
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
1664
1748
|
};
|
|
1665
1749
|
this.conversations.set(conversation.id, conversation);
|
|
1666
1750
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1751
|
+
if (remote.runningTurnId) this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
|
|
1667
1752
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1668
1753
|
}
|
|
1669
1754
|
}
|
|
@@ -1674,6 +1759,7 @@ export class AgentWorkspaceProxy {
|
|
|
1674
1759
|
const protocol = this.agentProtocols.get(provider);
|
|
1675
1760
|
const runtimeCapabilities = this.providerCapabilities.get(provider);
|
|
1676
1761
|
const enabled = Boolean(client);
|
|
1762
|
+
const hasRuntimeModels = Boolean(runtimeCapabilities?.models?.length);
|
|
1677
1763
|
const supportsImages = enabled && protocolSupportsImages(protocol);
|
|
1678
1764
|
const isClaudeFallback = protocol === "claude-stream-json";
|
|
1679
1765
|
const supportsPermission = enabled && !isClaudeFallback;
|
|
@@ -1688,12 +1774,15 @@ export class AgentWorkspaceProxy {
|
|
|
1688
1774
|
label: providerLabel(provider),
|
|
1689
1775
|
enabled,
|
|
1690
1776
|
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
1777
|
+
providerProtocol: protocol,
|
|
1691
1778
|
supportsImages,
|
|
1692
1779
|
supportsPermission,
|
|
1693
1780
|
supportsPlan: enabled,
|
|
1694
1781
|
supportsCancel: enabled,
|
|
1695
1782
|
models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
|
|
1696
1783
|
defaultModel: runtimeCapabilities?.defaultModel,
|
|
1784
|
+
modelsSource: hasRuntimeModels ? "runtime" : enabled ? "fallback" : "unavailable",
|
|
1785
|
+
modelListError: this.providerCapabilityErrors.get(provider),
|
|
1697
1786
|
reasoningEfforts: supportsReasoningEffort
|
|
1698
1787
|
? runtimeCapabilities?.reasoningEfforts ?? (provider === "claude" ? [...CLAUDE_REASONING_EFFORTS] : [...ALL_REASONING_EFFORTS])
|
|
1699
1788
|
: [],
|
|
@@ -1722,6 +1811,7 @@ export class AgentWorkspaceProxy {
|
|
|
1722
1811
|
providers,
|
|
1723
1812
|
protocolVersion: 1,
|
|
1724
1813
|
workspaceProtocolVersion: 2,
|
|
1814
|
+
capabilitiesRevision: ++this.capabilitiesRevision,
|
|
1725
1815
|
error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
|
|
1726
1816
|
supportsSessionList: anyEnabled,
|
|
1727
1817
|
supportsSessionLoad: anyEnabled,
|
|
@@ -1758,12 +1848,18 @@ export class AgentWorkspaceProxy {
|
|
|
1758
1848
|
}
|
|
1759
1849
|
this.hydrateStoredTimeline(existingConversation);
|
|
1760
1850
|
this.activeConversationId = existingConversation.id;
|
|
1851
|
+
const snapshot = this.latestSnapshot(existingConversation.id);
|
|
1761
1852
|
this.input.send(createEnvelope({
|
|
1762
1853
|
type: "agent.v2.conversation.opened",
|
|
1763
1854
|
hostDeviceId: this.input.hostDeviceId,
|
|
1764
1855
|
payload: {
|
|
1765
|
-
conversation: existingConversation,
|
|
1766
|
-
snapshot:
|
|
1856
|
+
conversation: this.conversationSnapshot(existingConversation),
|
|
1857
|
+
snapshot: snapshot.items,
|
|
1858
|
+
revision: this.getRevision(existingConversation.id),
|
|
1859
|
+
cursor: snapshot.cursor,
|
|
1860
|
+
hasMore: snapshot.hasMore,
|
|
1861
|
+
source: "device-history",
|
|
1862
|
+
canonical: true,
|
|
1767
1863
|
},
|
|
1768
1864
|
}));
|
|
1769
1865
|
return existingConversation;
|
|
@@ -1807,6 +1903,11 @@ export class AgentWorkspaceProxy {
|
|
|
1807
1903
|
collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
|
|
1808
1904
|
status: "idle",
|
|
1809
1905
|
archived: existingConversation?.archived ?? false,
|
|
1906
|
+
timelineRevision: existingConversation?.timelineRevision ?? this.getRevision(conversationId),
|
|
1907
|
+
historyComplete: existingConversation?.historyComplete ?? false,
|
|
1908
|
+
runningTurnId: this.currentTurnIds.get(conversationId),
|
|
1909
|
+
source: "device",
|
|
1910
|
+
canonical: true,
|
|
1810
1911
|
lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
|
|
1811
1912
|
lastActivityAt: now,
|
|
1812
1913
|
createdAt: existingConversation?.createdAt ?? now,
|
|
@@ -1816,10 +1917,19 @@ export class AgentWorkspaceProxy {
|
|
|
1816
1917
|
this.activeConversationId = conversation.id;
|
|
1817
1918
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1818
1919
|
this.hydrateStoredTimeline(conversation);
|
|
1920
|
+
const snapshot = this.latestSnapshot(conversation.id);
|
|
1819
1921
|
this.input.send(createEnvelope({
|
|
1820
1922
|
type: "agent.v2.conversation.opened",
|
|
1821
1923
|
hostDeviceId: this.input.hostDeviceId,
|
|
1822
|
-
payload: {
|
|
1924
|
+
payload: {
|
|
1925
|
+
conversation: this.conversationSnapshot(conversation),
|
|
1926
|
+
snapshot: snapshot.items,
|
|
1927
|
+
revision: this.getRevision(conversation.id),
|
|
1928
|
+
cursor: snapshot.cursor,
|
|
1929
|
+
hasMore: snapshot.hasMore,
|
|
1930
|
+
source: "device-history",
|
|
1931
|
+
canonical: true,
|
|
1932
|
+
},
|
|
1823
1933
|
}));
|
|
1824
1934
|
return conversation;
|
|
1825
1935
|
} catch (error) {
|
|
@@ -1855,6 +1965,10 @@ export class AgentWorkspaceProxy {
|
|
|
1855
1965
|
collaborationMode: payload.collaborationMode,
|
|
1856
1966
|
status: "error",
|
|
1857
1967
|
archived: false,
|
|
1968
|
+
timelineRevision: this.getRevision(fallbackId),
|
|
1969
|
+
historyComplete: false,
|
|
1970
|
+
source: "device",
|
|
1971
|
+
canonical: true,
|
|
1858
1972
|
lastMessagePreview: message,
|
|
1859
1973
|
lastActivityAt: now,
|
|
1860
1974
|
createdAt: now,
|
|
@@ -1871,7 +1985,14 @@ export class AgentWorkspaceProxy {
|
|
|
1871
1985
|
this.input.send(createEnvelope({
|
|
1872
1986
|
type: "agent.v2.conversation.opened",
|
|
1873
1987
|
hostDeviceId: this.input.hostDeviceId,
|
|
1874
|
-
payload: {
|
|
1988
|
+
payload: {
|
|
1989
|
+
conversation: this.conversationSnapshot(conversation),
|
|
1990
|
+
snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []),
|
|
1991
|
+
revision: this.getRevision(conversation.id),
|
|
1992
|
+
hasMore: false,
|
|
1993
|
+
source: "device",
|
|
1994
|
+
canonical: true,
|
|
1995
|
+
},
|
|
1875
1996
|
}));
|
|
1876
1997
|
return conversation;
|
|
1877
1998
|
}
|
|
@@ -2881,6 +3002,14 @@ export class AgentWorkspaceProxy {
|
|
|
2881
3002
|
this.permissionWaiters.delete(requestId);
|
|
2882
3003
|
this.permissionSources.delete(requestId);
|
|
2883
3004
|
resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
|
|
3005
|
+
this.markPermission(conversationId, requestId, {
|
|
3006
|
+
permissionOutcome: "cancelled",
|
|
3007
|
+
optionId: "cancelled",
|
|
3008
|
+
permissionLive: false,
|
|
3009
|
+
permissionPending: false,
|
|
3010
|
+
permissionExpired: true,
|
|
3011
|
+
permissionError: "等待授权超时",
|
|
3012
|
+
});
|
|
2884
3013
|
this.updateConversationStatus(conversationId, "idle");
|
|
2885
3014
|
}, PERMISSION_TIMEOUT_MS);
|
|
2886
3015
|
this.permissionWaiters.set(requestId, { resolve, timer });
|
|
@@ -2920,6 +3049,8 @@ export class AgentWorkspaceProxy {
|
|
|
2920
3049
|
this.markPermission(payload.conversationId, payload.requestId, {
|
|
2921
3050
|
permissionOutcome: payload.outcome,
|
|
2922
3051
|
optionId: selectedOptionId,
|
|
3052
|
+
permissionLive: false,
|
|
3053
|
+
permissionExpired: false,
|
|
2923
3054
|
permissionError: undefined,
|
|
2924
3055
|
permissionPending: false,
|
|
2925
3056
|
});
|
|
@@ -2980,6 +3111,228 @@ export class AgentWorkspaceProxy {
|
|
|
2980
3111
|
});
|
|
2981
3112
|
}
|
|
2982
3113
|
|
|
3114
|
+
private getRevision(conversationId: string): number {
|
|
3115
|
+
return this.conversationRevisions.get(conversationId) ??
|
|
3116
|
+
this.conversations.get(conversationId)?.timelineRevision ??
|
|
3117
|
+
0;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
private setRevisionFloor(conversationId: string, revision: number): number {
|
|
3121
|
+
const nextRevision = Math.max(this.getRevision(conversationId), revision);
|
|
3122
|
+
this.conversationRevisions.set(conversationId, nextRevision);
|
|
3123
|
+
const conversation = this.conversations.get(conversationId);
|
|
3124
|
+
if (conversation) {
|
|
3125
|
+
conversation.timelineRevision = nextRevision;
|
|
3126
|
+
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
3127
|
+
}
|
|
3128
|
+
return nextRevision;
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
private conversationSnapshot(conversation: AgentConversation): AgentConversation {
|
|
3132
|
+
return {
|
|
3133
|
+
...conversation,
|
|
3134
|
+
timelineRevision: this.getRevision(conversation.id),
|
|
3135
|
+
historyComplete: conversation.historyComplete ?? false,
|
|
3136
|
+
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
3137
|
+
source: conversation.source ?? "device",
|
|
3138
|
+
canonical: conversation.canonical ?? true,
|
|
3139
|
+
};
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
private annotateTimelineItem(
|
|
3143
|
+
item: AgentTimelineItem,
|
|
3144
|
+
revision: number | undefined,
|
|
3145
|
+
source: AgentSyncSource,
|
|
3146
|
+
): AgentTimelineItem {
|
|
3147
|
+
return {
|
|
3148
|
+
...item,
|
|
3149
|
+
revision: revision ?? item.revision,
|
|
3150
|
+
source: item.source ?? source,
|
|
3151
|
+
canonical: item.canonical ?? true,
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
private recordRevisionEvent(
|
|
3156
|
+
conversationId: string,
|
|
3157
|
+
change: { item?: AgentTimelineItem; conversation?: AgentConversation },
|
|
3158
|
+
): AgentRevisionEvent {
|
|
3159
|
+
const revision = this.getRevision(conversationId) + 1;
|
|
3160
|
+
this.conversationRevisions.set(conversationId, revision);
|
|
3161
|
+
const conversation = this.conversations.get(conversationId);
|
|
3162
|
+
if (conversation) {
|
|
3163
|
+
conversation.timelineRevision = revision;
|
|
3164
|
+
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
3165
|
+
}
|
|
3166
|
+
const event: AgentRevisionEvent = {
|
|
3167
|
+
revision,
|
|
3168
|
+
item: change.item ? this.annotateTimelineItem(change.item, revision, "device-live") : undefined,
|
|
3169
|
+
conversation: change.conversation ? this.conversationSnapshot(change.conversation) : undefined,
|
|
3170
|
+
};
|
|
3171
|
+
const events = this.revisionEvents.get(conversationId) ?? [];
|
|
3172
|
+
events.push(event);
|
|
3173
|
+
if (events.length > MAX_DELTA_EVENTS) {
|
|
3174
|
+
events.splice(0, events.length - MAX_DELTA_EVENTS);
|
|
3175
|
+
}
|
|
3176
|
+
this.revisionEvents.set(conversationId, events);
|
|
3177
|
+
return event;
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
private storedTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): AgentTimelineItem[] {
|
|
3181
|
+
if (!conversation.agentSessionId) return [];
|
|
3182
|
+
const result = conversation.provider === "codex"
|
|
3183
|
+
? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd, { maxItems })
|
|
3184
|
+
: conversation.provider === "claude"
|
|
3185
|
+
? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id, { maxItems })
|
|
3186
|
+
: { items: [] };
|
|
3187
|
+
return result.items
|
|
3188
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3189
|
+
.map((item, index) => this.annotateTimelineItem(item as AgentTimelineItem, index + 1, "device-history"));
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
private canonicalTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): AgentTimelineItem[] {
|
|
3193
|
+
const merged = new Map<string, AgentTimelineItem>();
|
|
3194
|
+
for (const item of this.storedTimeline(conversation, maxItems)) {
|
|
3195
|
+
merged.set(item.id, item);
|
|
3196
|
+
}
|
|
3197
|
+
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
3198
|
+
merged.set(item.id, this.annotateTimelineItem(item, item.revision, item.source ?? "device-live"));
|
|
3199
|
+
}
|
|
3200
|
+
const items = [...merged.values()]
|
|
3201
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3202
|
+
.slice(-maxItems);
|
|
3203
|
+
this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
3204
|
+
return items;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
private async appServerTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): Promise<AgentTimelineItem[]> {
|
|
3208
|
+
if (conversation.provider !== "codex" || !conversation.agentSessionId) return [];
|
|
3209
|
+
if (this.protocolForProvider(conversation.provider) !== "codex-app-server") return [];
|
|
3210
|
+
const client = this.clientForProvider(conversation.provider);
|
|
3211
|
+
const listTurns = (client as { listTurns?: (input: { sessionId: string; limit?: number }) => Promise<unknown> } | undefined)?.listTurns;
|
|
3212
|
+
if (typeof listTurns !== "function") return [];
|
|
3213
|
+
try {
|
|
3214
|
+
const result = await listTurns.call(client, { sessionId: conversation.agentSessionId, limit: maxItems });
|
|
3215
|
+
return this.timelineFromAppServerTurns(conversation.id, conversation.agentSessionId, result);
|
|
3216
|
+
} catch (error) {
|
|
3217
|
+
if (this.input.verbose) {
|
|
3218
|
+
process.stderr.write(`[agent:v2] thread/turns/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
3219
|
+
}
|
|
3220
|
+
return [];
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
private timelineFromAppServerTurns(
|
|
3225
|
+
conversationId: string,
|
|
3226
|
+
agentSessionId: string,
|
|
3227
|
+
value: unknown,
|
|
3228
|
+
): AgentTimelineItem[] {
|
|
3229
|
+
const raw = asRecord(value);
|
|
3230
|
+
const turns =
|
|
3231
|
+
Array.isArray(value) ? value :
|
|
3232
|
+
Array.isArray(raw?.turns) ? raw.turns :
|
|
3233
|
+
Array.isArray(raw?.items) ? raw.items :
|
|
3234
|
+
Array.isArray(raw?.entries) ? raw.entries :
|
|
3235
|
+
[];
|
|
3236
|
+
const items: AgentTimelineItem[] = [];
|
|
3237
|
+
turns.forEach((entry, index) => {
|
|
3238
|
+
const turn = asRecord(entry);
|
|
3239
|
+
if (!turn) return;
|
|
3240
|
+
const turnId = firstString(turn, ["id", "turnId", "turn_id"]) ?? `turn-${index + 1}`;
|
|
3241
|
+
const createdAt = parseTimestamp(turn.createdAt ?? turn.created_at ?? turn.startedAt ?? turn.started_at) ?? Date.now() + index;
|
|
3242
|
+
const updatedAt = parseTimestamp(turn.updatedAt ?? turn.updated_at ?? turn.completedAt ?? turn.completed_at);
|
|
3243
|
+
const userText = appServerText(turn.input ?? turn.prompt ?? turn.user ?? turn.userMessage ?? turn.request);
|
|
3244
|
+
if (userText) {
|
|
3245
|
+
items.push({
|
|
3246
|
+
id: `app-server:${agentSessionId}:${turnId}:user`,
|
|
3247
|
+
conversationId,
|
|
3248
|
+
type: "message",
|
|
3249
|
+
kind: "chat",
|
|
3250
|
+
turnId,
|
|
3251
|
+
role: "user",
|
|
3252
|
+
content: [{ type: "text", text: userText }],
|
|
3253
|
+
text: userText,
|
|
3254
|
+
createdAt,
|
|
3255
|
+
updatedAt,
|
|
3256
|
+
metadata: { source: "app-server", provider: "codex" },
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
const assistantText = appServerText(turn.output ?? turn.response ?? turn.assistant ?? turn.assistantMessage ?? turn.result);
|
|
3260
|
+
if (assistantText) {
|
|
3261
|
+
items.push({
|
|
3262
|
+
id: `app-server:${agentSessionId}:${turnId}:assistant`,
|
|
3263
|
+
conversationId,
|
|
3264
|
+
type: "message",
|
|
3265
|
+
kind: "chat",
|
|
3266
|
+
turnId,
|
|
3267
|
+
role: "assistant",
|
|
3268
|
+
content: [{ type: "text", text: assistantText }],
|
|
3269
|
+
text: assistantText,
|
|
3270
|
+
createdAt: updatedAt ?? createdAt + 1,
|
|
3271
|
+
updatedAt,
|
|
3272
|
+
metadata: { source: "app-server", provider: "codex" },
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
const nestedItems = Array.isArray(turn.items) ? turn.items : Array.isArray(turn.messages) ? turn.messages : [];
|
|
3276
|
+
for (const nested of nestedItems) {
|
|
3277
|
+
const nestedRecord = asRecord(nested);
|
|
3278
|
+
if (!nestedRecord) continue;
|
|
3279
|
+
const text = appServerText(nestedRecord.content ?? nestedRecord.text ?? nestedRecord.message);
|
|
3280
|
+
const role = nestedRecord.role === "user" || nestedRecord.role === "assistant" || nestedRecord.role === "system"
|
|
3281
|
+
? nestedRecord.role
|
|
3282
|
+
: undefined;
|
|
3283
|
+
if (!text || !role) continue;
|
|
3284
|
+
const itemId = firstString(nestedRecord, ["id", "itemId", "messageId"]) ?? `${role}-${items.length + 1}`;
|
|
3285
|
+
items.push({
|
|
3286
|
+
id: `app-server:${agentSessionId}:${turnId}:${itemId}`,
|
|
3287
|
+
conversationId,
|
|
3288
|
+
type: "message",
|
|
3289
|
+
kind: "chat",
|
|
3290
|
+
turnId,
|
|
3291
|
+
itemId,
|
|
3292
|
+
role,
|
|
3293
|
+
content: [{ type: "text", text }],
|
|
3294
|
+
text,
|
|
3295
|
+
createdAt: parseTimestamp(nestedRecord.createdAt ?? nestedRecord.created_at) ?? createdAt + items.length,
|
|
3296
|
+
updatedAt: parseTimestamp(nestedRecord.updatedAt ?? nestedRecord.updated_at),
|
|
3297
|
+
metadata: { source: "app-server", provider: "codex" },
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
});
|
|
3301
|
+
return items
|
|
3302
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3303
|
+
.map((item, index) => this.annotateTimelineItem(item, index + 1, "app-server"));
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
private mergeCanonicalTimelineItems(items: AgentTimelineItem[], maxItems: number): AgentTimelineItem[] {
|
|
3307
|
+
const byKey = new Map<string, AgentTimelineItem>();
|
|
3308
|
+
for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
|
|
3309
|
+
const key = item.type === "message" && item.role && item.text
|
|
3310
|
+
? `${item.role}:${item.text.replace(/\s+/g, " ").trim().slice(0, 500)}`
|
|
3311
|
+
: item.id;
|
|
3312
|
+
const existing = byKey.get(key);
|
|
3313
|
+
if (!existing || item.source === "app-server" || (item.updatedAt ?? item.createdAt) >= (existing.updatedAt ?? existing.createdAt)) {
|
|
3314
|
+
byKey.set(key, item);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
return [...byKey.values()]
|
|
3318
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3319
|
+
.slice(-maxItems);
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
private latestSnapshot(conversationId: string): {
|
|
3323
|
+
items: AgentTimelineItem[];
|
|
3324
|
+
cursor?: string;
|
|
3325
|
+
hasMore: boolean;
|
|
3326
|
+
} {
|
|
3327
|
+
const timeline = this.timelines.get(conversationId) ?? [];
|
|
3328
|
+
const start = Math.max(0, timeline.length - MAX_SNAPSHOT_ITEMS);
|
|
3329
|
+
return {
|
|
3330
|
+
items: timeline.slice(start).map((item) => snapshotTimelineItem(item)),
|
|
3331
|
+
cursor: start > 0 ? String(start) : undefined,
|
|
3332
|
+
hasMore: start > 0,
|
|
3333
|
+
};
|
|
3334
|
+
}
|
|
3335
|
+
|
|
2983
3336
|
private addItem(conversationId: string, item: AgentTimelineItem): void {
|
|
2984
3337
|
this.rememberItemConversationId(conversationId, item);
|
|
2985
3338
|
const timeline = this.timelines.get(conversationId) ?? [];
|
|
@@ -3111,6 +3464,17 @@ export class AgentWorkspaceProxy {
|
|
|
3111
3464
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3112
3465
|
.slice(-MAX_TIMELINE_ITEMS),
|
|
3113
3466
|
);
|
|
3467
|
+
const oldRevision = this.conversationRevisions.get(oldId) ?? 0;
|
|
3468
|
+
if (oldRevision > 0) {
|
|
3469
|
+
this.conversationRevisions.delete(oldId);
|
|
3470
|
+
this.setRevisionFloor(newId, oldRevision);
|
|
3471
|
+
}
|
|
3472
|
+
const oldEvents = this.revisionEvents.get(oldId);
|
|
3473
|
+
if (oldEvents) {
|
|
3474
|
+
this.revisionEvents.delete(oldId);
|
|
3475
|
+
const existingEvents = this.revisionEvents.get(newId) ?? [];
|
|
3476
|
+
this.revisionEvents.set(newId, [...existingEvents, ...oldEvents].slice(-MAX_DELTA_EVENTS));
|
|
3477
|
+
}
|
|
3114
3478
|
|
|
3115
3479
|
for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
|
|
3116
3480
|
if (conversationId === oldId) {
|
|
@@ -3146,18 +3510,48 @@ export class AgentWorkspaceProxy {
|
|
|
3146
3510
|
|
|
3147
3511
|
private emitItem(conversationId: string, item: AgentTimelineItem): void {
|
|
3148
3512
|
const conversation = this.conversations.get(conversationId);
|
|
3513
|
+
const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
|
|
3514
|
+
const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
|
|
3149
3515
|
this.input.send(createEnvelope({
|
|
3150
3516
|
type: "agent.v2.event",
|
|
3151
3517
|
hostDeviceId: this.input.hostDeviceId,
|
|
3152
|
-
payload: {
|
|
3518
|
+
payload: {
|
|
3519
|
+
conversationId,
|
|
3520
|
+
conversation: event.conversation,
|
|
3521
|
+
item: event.item,
|
|
3522
|
+
revision: event.revision,
|
|
3523
|
+
source: "device-live",
|
|
3524
|
+
canonical: true,
|
|
3525
|
+
},
|
|
3153
3526
|
}));
|
|
3154
3527
|
}
|
|
3155
3528
|
|
|
3156
3529
|
private emitConversation(conversation: AgentConversation): void {
|
|
3530
|
+
const event = this.recordRevisionEvent(conversation.id, { conversation });
|
|
3157
3531
|
this.input.send(createEnvelope({
|
|
3158
3532
|
type: "agent.v2.event",
|
|
3159
3533
|
hostDeviceId: this.input.hostDeviceId,
|
|
3160
|
-
payload: {
|
|
3534
|
+
payload: {
|
|
3535
|
+
conversationId: conversation.id,
|
|
3536
|
+
conversation: event.conversation,
|
|
3537
|
+
revision: event.revision,
|
|
3538
|
+
source: "device-live",
|
|
3539
|
+
canonical: true,
|
|
3540
|
+
},
|
|
3541
|
+
}));
|
|
3542
|
+
this.input.send(createEnvelope({
|
|
3543
|
+
type: "agent.v2.running_state",
|
|
3544
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3545
|
+
payload: {
|
|
3546
|
+
conversationId: conversation.id,
|
|
3547
|
+
status: conversation.status,
|
|
3548
|
+
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
3549
|
+
revision: event.revision,
|
|
3550
|
+
error: conversation.status === "error" ? conversation.lastMessagePreview : undefined,
|
|
3551
|
+
updatedAt: conversation.lastActivityAt,
|
|
3552
|
+
source: "device-live",
|
|
3553
|
+
canonical: true,
|
|
3554
|
+
},
|
|
3161
3555
|
}));
|
|
3162
3556
|
}
|
|
3163
3557
|
|
|
@@ -3206,17 +3600,143 @@ export class AgentWorkspaceProxy {
|
|
|
3206
3600
|
const conversation = this.conversations.get(this.activeConversationId);
|
|
3207
3601
|
if (conversation) this.hydrateStoredTimeline(conversation);
|
|
3208
3602
|
}
|
|
3209
|
-
const conversations = [...this.conversations.values()];
|
|
3210
|
-
const
|
|
3211
|
-
? snapshotTimelineItems(this.timelines.get(conversationId) ?? [])
|
|
3212
|
-
: [];
|
|
3603
|
+
const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
|
|
3604
|
+
const snapshot = conversationId ? this.latestSnapshot(conversationId) : { items: [], cursor: undefined, hasMore: false };
|
|
3213
3605
|
this.input.send(createEnvelope({
|
|
3214
3606
|
type: "agent.v2.snapshot",
|
|
3215
3607
|
hostDeviceId: this.input.hostDeviceId,
|
|
3216
3608
|
payload: {
|
|
3217
3609
|
conversations,
|
|
3218
3610
|
activeConversationId: this.activeConversationId,
|
|
3219
|
-
items,
|
|
3611
|
+
items: snapshot.items,
|
|
3612
|
+
revision: conversationId ? this.getRevision(conversationId) : undefined,
|
|
3613
|
+
cursor: snapshot.cursor,
|
|
3614
|
+
hasMore: snapshot.hasMore,
|
|
3615
|
+
source: "device-history",
|
|
3616
|
+
canonical: true,
|
|
3617
|
+
},
|
|
3618
|
+
}));
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
private async sendHistoryPage(payload: {
|
|
3622
|
+
conversationId: string;
|
|
3623
|
+
cursor?: string;
|
|
3624
|
+
limit?: number;
|
|
3625
|
+
direction?: "older" | "newer";
|
|
3626
|
+
}): Promise<void> {
|
|
3627
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
3628
|
+
if (!conversation) return;
|
|
3629
|
+
const limit = Math.min(Math.max(payload.limit ?? MAX_SNAPSHOT_ITEMS, 1), 200);
|
|
3630
|
+
const items = this.mergeCanonicalTimelineItems([
|
|
3631
|
+
...this.canonicalTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3632
|
+
...await this.appServerTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3633
|
+
], HISTORY_PAGE_MAX_ITEMS);
|
|
3634
|
+
this.timelines.set(
|
|
3635
|
+
conversation.id,
|
|
3636
|
+
items.slice(-MAX_TIMELINE_ITEMS).map((item) => this.annotateTimelineItem(item, item.revision, item.source ?? "device-history")),
|
|
3637
|
+
);
|
|
3638
|
+
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
3639
|
+
this.rememberItemConversationId(conversation.id, item);
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
const direction = payload.direction ?? "older";
|
|
3643
|
+
let page: AgentTimelineItem[];
|
|
3644
|
+
let cursor: string | undefined;
|
|
3645
|
+
let hasMore = false;
|
|
3646
|
+
if (direction === "newer") {
|
|
3647
|
+
const start = clampHistoryCursor(payload.cursor, 0, items.length);
|
|
3648
|
+
const end = Math.min(items.length, start + limit);
|
|
3649
|
+
page = items.slice(start, end);
|
|
3650
|
+
hasMore = end < items.length;
|
|
3651
|
+
cursor = hasMore ? String(end) : undefined;
|
|
3652
|
+
} else {
|
|
3653
|
+
const end = clampHistoryCursor(payload.cursor, items.length, items.length);
|
|
3654
|
+
const start = Math.max(0, end - limit);
|
|
3655
|
+
page = items.slice(start, end);
|
|
3656
|
+
hasMore = start > 0;
|
|
3657
|
+
cursor = hasMore ? String(start) : undefined;
|
|
3658
|
+
}
|
|
3659
|
+
conversation.historyComplete = !hasMore;
|
|
3660
|
+
const revision = this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
3661
|
+
this.input.send(createEnvelope({
|
|
3662
|
+
type: "agent.v2.history.page",
|
|
3663
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3664
|
+
payload: {
|
|
3665
|
+
conversationId: conversation.id,
|
|
3666
|
+
conversation: this.conversationSnapshot(conversation),
|
|
3667
|
+
items: page.map((item) => snapshotTimelineItem(item)),
|
|
3668
|
+
revision,
|
|
3669
|
+
cursor,
|
|
3670
|
+
hasMore,
|
|
3671
|
+
source: "device-history",
|
|
3672
|
+
canonical: true,
|
|
3673
|
+
},
|
|
3674
|
+
}));
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
private sendDelta(payload: {
|
|
3678
|
+
conversationId: string;
|
|
3679
|
+
sinceRevision?: number;
|
|
3680
|
+
limit?: number;
|
|
3681
|
+
}): void {
|
|
3682
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
3683
|
+
if (!conversation) return;
|
|
3684
|
+
this.hydrateStoredTimeline(conversation);
|
|
3685
|
+
const sinceRevision = payload.sinceRevision ?? 0;
|
|
3686
|
+
const limit = Math.min(Math.max(payload.limit ?? 100, 1), 500);
|
|
3687
|
+
const events = this.revisionEvents.get(conversation.id) ?? [];
|
|
3688
|
+
const oldestAvailable = events[0]?.revision ?? this.getRevision(conversation.id);
|
|
3689
|
+
const newestRevision = this.getRevision(conversation.id);
|
|
3690
|
+
const reset = sinceRevision > 0 &&
|
|
3691
|
+
(
|
|
3692
|
+
sinceRevision > newestRevision ||
|
|
3693
|
+
(
|
|
3694
|
+
sinceRevision < newestRevision &&
|
|
3695
|
+
(events.length === 0 || sinceRevision < oldestAvailable - 1)
|
|
3696
|
+
)
|
|
3697
|
+
);
|
|
3698
|
+
|
|
3699
|
+
if (reset) {
|
|
3700
|
+
const snapshot = this.latestSnapshot(conversation.id);
|
|
3701
|
+
this.input.send(createEnvelope({
|
|
3702
|
+
type: "agent.v2.delta",
|
|
3703
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3704
|
+
payload: {
|
|
3705
|
+
conversationId: conversation.id,
|
|
3706
|
+
conversation: this.conversationSnapshot(conversation),
|
|
3707
|
+
items: snapshot.items,
|
|
3708
|
+
sinceRevision,
|
|
3709
|
+
revision: newestRevision,
|
|
3710
|
+
reset: true,
|
|
3711
|
+
cursor: snapshot.cursor,
|
|
3712
|
+
hasMore: snapshot.hasMore,
|
|
3713
|
+
source: "device-history",
|
|
3714
|
+
canonical: true,
|
|
3715
|
+
},
|
|
3716
|
+
}));
|
|
3717
|
+
return;
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
const changed = events
|
|
3721
|
+
.filter((event) => event.revision > sinceRevision)
|
|
3722
|
+
.slice(-limit);
|
|
3723
|
+
const itemsById = new Map<string, AgentTimelineItem>();
|
|
3724
|
+
for (const event of changed) {
|
|
3725
|
+
if (event.item) itemsById.set(event.item.id, event.item);
|
|
3726
|
+
}
|
|
3727
|
+
this.input.send(createEnvelope({
|
|
3728
|
+
type: "agent.v2.delta",
|
|
3729
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3730
|
+
payload: {
|
|
3731
|
+
conversationId: conversation.id,
|
|
3732
|
+
conversation: this.conversationSnapshot(conversation),
|
|
3733
|
+
items: [...itemsById.values()].map((item) => snapshotTimelineItem(item)),
|
|
3734
|
+
sinceRevision,
|
|
3735
|
+
revision: newestRevision,
|
|
3736
|
+
reset: false,
|
|
3737
|
+
hasMore: changed.length === limit && events.some((event) => event.revision > sinceRevision && event.revision < changed[0]!.revision),
|
|
3738
|
+
source: "device-live",
|
|
3739
|
+
canonical: true,
|
|
3220
3740
|
},
|
|
3221
3741
|
}));
|
|
3222
3742
|
}
|
|
@@ -3224,18 +3744,25 @@ export class AgentWorkspaceProxy {
|
|
|
3224
3744
|
private hydrateStoredTimeline(conversation: AgentConversation): void {
|
|
3225
3745
|
if (!conversation.agentSessionId) return;
|
|
3226
3746
|
const existing = this.timelines.get(conversation.id) ?? [];
|
|
3227
|
-
if (existing.length > 0)
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
const
|
|
3747
|
+
if (existing.length > 0) {
|
|
3748
|
+
this.setRevisionFloor(conversation.id, Math.max(
|
|
3749
|
+
existing.length,
|
|
3750
|
+
...existing.map((item) => item.revision ?? 0),
|
|
3751
|
+
));
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
const stored = this.storedTimeline(conversation, MAX_TIMELINE_ITEMS);
|
|
3755
|
+
conversation.historyComplete = stored.length <= MAX_SNAPSHOT_ITEMS;
|
|
3756
|
+
if (stored.length === 0) {
|
|
3757
|
+
this.setRevisionFloor(conversation.id, this.getRevision(conversation.id));
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
const items = stored
|
|
3235
3761
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3236
|
-
.slice(-MAX_TIMELINE_ITEMS)
|
|
3762
|
+
.slice(-MAX_TIMELINE_ITEMS);
|
|
3237
3763
|
this.timelines.set(conversation.id, items);
|
|
3238
3764
|
for (const item of items) this.rememberItemConversationId(conversation.id, item);
|
|
3765
|
+
this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
|
|
3239
3766
|
const lastMessage = [...items].reverse().find((item) => item.text?.trim());
|
|
3240
3767
|
if (lastMessage?.text && !conversation.lastMessagePreview) {
|
|
3241
3768
|
conversation.lastMessagePreview = previewText(lastMessage.text);
|
|
@@ -3303,6 +3830,8 @@ export class AgentWorkspaceProxy {
|
|
|
3303
3830
|
private rememberTurnConversationId(conversationId: string, turnId: string): void {
|
|
3304
3831
|
this.currentTurnIds.set(conversationId, turnId);
|
|
3305
3832
|
this.turnConversationIds.set(turnId, conversationId);
|
|
3833
|
+
const conversation = this.conversations.get(conversationId);
|
|
3834
|
+
if (conversation) conversation.runningTurnId = turnId;
|
|
3306
3835
|
}
|
|
3307
3836
|
|
|
3308
3837
|
private forgetCurrentTurn(conversationId: string, turnId?: string): void {
|
|
@@ -3310,6 +3839,8 @@ export class AgentWorkspaceProxy {
|
|
|
3310
3839
|
this.currentTurnIds.delete(conversationId);
|
|
3311
3840
|
if (turnId) this.turnConversationIds.delete(turnId);
|
|
3312
3841
|
if (currentTurnId && currentTurnId !== turnId) this.turnConversationIds.delete(currentTurnId);
|
|
3842
|
+
const conversation = this.conversations.get(conversationId);
|
|
3843
|
+
if (conversation) conversation.runningTurnId = undefined;
|
|
3313
3844
|
}
|
|
3314
3845
|
|
|
3315
3846
|
private rememberItemConversationId(conversationId: string, item: AgentTimelineItem): void {
|
|
@@ -3351,32 +3882,54 @@ export class AgentWorkspaceProxy {
|
|
|
3351
3882
|
|
|
3352
3883
|
private cancelPendingPermissions(conversationId?: string): void {
|
|
3353
3884
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
3885
|
+
const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
|
|
3886
|
+
if (conversationId && ownerConversationId && ownerConversationId !== conversationId) continue;
|
|
3354
3887
|
clearTimeout(waiter.timer);
|
|
3355
3888
|
waiter.resolve(formatPermissionResponse(
|
|
3356
3889
|
this.permissionSources.get(requestId),
|
|
3357
3890
|
"cancelled",
|
|
3358
3891
|
"cancelled",
|
|
3359
3892
|
));
|
|
3893
|
+
if (ownerConversationId) {
|
|
3894
|
+
this.markPermission(ownerConversationId, requestId, {
|
|
3895
|
+
permissionOutcome: "cancelled",
|
|
3896
|
+
optionId: "cancelled",
|
|
3897
|
+
permissionLive: false,
|
|
3898
|
+
permissionPending: false,
|
|
3899
|
+
permissionExpired: false,
|
|
3900
|
+
permissionError: "已停止",
|
|
3901
|
+
});
|
|
3902
|
+
}
|
|
3903
|
+
this.permissionWaiters.delete(requestId);
|
|
3360
3904
|
this.pendingPermissions.delete(requestId);
|
|
3361
3905
|
this.permissionSources.delete(requestId);
|
|
3362
3906
|
}
|
|
3363
|
-
this.permissionWaiters.clear();
|
|
3364
3907
|
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
3908
|
+
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3909
|
+
if (conversationId && pending?.conversationId && pending.conversationId !== conversationId) continue;
|
|
3365
3910
|
clearTimeout(waiter.timer);
|
|
3366
3911
|
waiter.resolve(formatStructuredInputResponse({}));
|
|
3367
|
-
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3368
3912
|
if (pending) {
|
|
3369
3913
|
this.markStructuredInput(pending.conversationId, requestId, {
|
|
3370
3914
|
inputPending: false,
|
|
3371
3915
|
inputError: "已停止",
|
|
3372
3916
|
});
|
|
3373
3917
|
}
|
|
3918
|
+
this.structuredInputWaiters.delete(requestId);
|
|
3374
3919
|
this.pendingStructuredInputs.delete(requestId);
|
|
3375
3920
|
}
|
|
3376
|
-
this.structuredInputWaiters.clear();
|
|
3377
3921
|
if (conversationId) this.updateConversationStatus(conversationId, "idle");
|
|
3378
3922
|
}
|
|
3379
3923
|
|
|
3924
|
+
private conversationIdForPermissionRequest(requestId: string): string | undefined {
|
|
3925
|
+
for (const [conversationId, timeline] of this.timelines) {
|
|
3926
|
+
if (timeline.some((item) => item.type === "permission" && item.permission?.requestId === requestId)) {
|
|
3927
|
+
return conversationId;
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
return undefined;
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3380
3933
|
private extractSessionId(value: unknown): string | undefined {
|
|
3381
3934
|
const raw = asRecord(value);
|
|
3382
3935
|
if (!raw) return undefined;
|