@blade-hq/agent-client 2610.0.0-beta.7 → 2610.0.0-beta.71

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.
Files changed (40) hide show
  1. package/README.md +248 -9
  2. package/dist/auth.d.ts +0 -1
  3. package/dist/blade-client.d.ts +21 -3
  4. package/dist/index.d.ts +35 -6
  5. package/dist/index.js +2210 -268
  6. package/dist/index.js.map +1 -1
  7. package/dist/platform-endpoints.d.ts +14 -0
  8. package/dist/polling-backoff.d.ts +11 -0
  9. package/dist/resources/chat-groups.d.ts +26 -0
  10. package/dist/resources/computers.d.ts +115 -0
  11. package/dist/resources/models.d.ts +2 -0
  12. package/dist/resources/plugins.d.ts +43 -0
  13. package/dist/resources/sessions.d.ts +186 -1
  14. package/dist/resources/site-visits.d.ts +32 -0
  15. package/dist/rest.d.ts +8 -0
  16. package/dist/schemas/context.d.ts +49 -0
  17. package/dist/schemas/event.d.ts +9 -0
  18. package/dist/schemas/mcp-app.d.ts +30 -0
  19. package/dist/schemas/message-utils.d.ts +13 -1
  20. package/dist/schemas/message.d.ts +3 -0
  21. package/dist/schemas/projection.d.ts +37 -2
  22. package/dist/schemas/session.d.ts +9 -1
  23. package/dist/session/agent-session.d.ts +142 -16
  24. package/dist/session/events.d.ts +4 -0
  25. package/dist/session/history-page.d.ts +9 -0
  26. package/dist/session/hub.d.ts +9 -1
  27. package/dist/session/queue.d.ts +99 -0
  28. package/dist/session/state.d.ts +18 -2
  29. package/dist/shared/auth-busy.d.ts +9 -0
  30. package/dist/shared/projection/builder.d.ts +12 -0
  31. package/dist/shared/projection/context.d.ts +3 -0
  32. package/dist/shared/projection/helpers.d.ts +2 -2
  33. package/dist/shared/projection/history.d.ts +1 -0
  34. package/dist/shared/projection/index.d.ts +2 -1
  35. package/dist/shared/projection/state.d.ts +7 -0
  36. package/dist/shared/tool-ui-card.d.ts +58 -0
  37. package/dist/shared/type-guards.d.ts +2 -0
  38. package/dist/types/socket-events.d.ts +195 -6
  39. package/package.json +1 -1
  40. package/public-api.md +918 -12
package/dist/index.js CHANGED
@@ -167,6 +167,10 @@ function decorateAuthError(error) {
167
167
  }
168
168
  return error;
169
169
  }
170
+ function buildBackendUrl(baseUrl, path) {
171
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
172
+ return new URL(path.startsWith("/") ? path.slice(1) : path, base);
173
+ }
170
174
 
171
175
  // src/resources/auth.ts
172
176
  var AuthResource = class {
@@ -214,6 +218,11 @@ var AuthResource = class {
214
218
  }
215
219
  };
216
220
 
221
+ // src/shared/type-guards.ts
222
+ function isRecord(value) {
223
+ return typeof value === "object" && value !== null && !Array.isArray(value);
224
+ }
225
+
217
226
  // src/resources/headless.ts
218
227
  var HeadlessError = class extends Error {
219
228
  constructor(message, detail) {
@@ -352,8 +361,7 @@ var HeadlessResource = class {
352
361
  socket.on("system:error", onSystemError);
353
362
  socket.emit("session:subscribe", {
354
363
  session_id,
355
- event_format: "raw",
356
- stream_tokens: this.client.options.streamTokens !== false
364
+ event_format: "raw"
357
365
  });
358
366
  socket.emit(
359
367
  "chat:send",
@@ -395,9 +403,6 @@ function errorMessageFromResult(result) {
395
403
  }
396
404
  return null;
397
405
  }
398
- function isRecord(value) {
399
- return typeof value === "object" && value !== null && !Array.isArray(value);
400
- }
401
406
  function ensureSocketConnected(socket, timeoutMs, detail) {
402
407
  if (socket.connected) {
403
408
  return Promise.resolve();
@@ -425,6 +430,81 @@ function ensureSocketConnected(socket, timeoutMs, detail) {
425
430
  });
426
431
  }
427
432
 
433
+ // src/resources/computers.ts
434
+ function computerState(computer) {
435
+ if (computer.is_primary) return "primary";
436
+ if (computer.enabled) return "enabled";
437
+ return computer.online ? "idle" : "offline";
438
+ }
439
+ function computerOS(computer) {
440
+ const os = (computer.os ?? "").toLowerCase();
441
+ if (os.startsWith("darwin") || os.startsWith("mac")) return "macos";
442
+ if (os.startsWith("windows")) return "windows";
443
+ if (os.startsWith("linux")) return "linux";
444
+ return "unknown";
445
+ }
446
+ function computerPlatformLabel(computer) {
447
+ const os = (computer.os ?? "").trim();
448
+ const arch = (computer.arch ?? "").trim();
449
+ if (os && arch) return `${os}/${arch}`;
450
+ return os || arch || "";
451
+ }
452
+ function computerDaemonVersion(computer) {
453
+ return (computer.daemon_version ?? "").trim();
454
+ }
455
+ function canToggleComputer(computer) {
456
+ return !computer.is_primary;
457
+ }
458
+ function sortComputers(computers) {
459
+ return [...computers].sort((a, b) => {
460
+ const byCreated = (a.created_at ?? "").localeCompare(b.created_at ?? "");
461
+ if (byCreated !== 0) return byCreated;
462
+ return a.label.localeCompare(b.label);
463
+ });
464
+ }
465
+ var ComputersResource = class {
466
+ constructor(client) {
467
+ this.client = client;
468
+ }
469
+ client;
470
+ /**
471
+ * 列出当前账号登记过的全部电脑及其在线状态。
472
+ *
473
+ * 首页还没有会话,也要能看到同一份清单——判定各写一份必然分叉,一边的兜底会把
474
+ * 另一边的错误盖住(#1597)。所以账号事实只从这一个接口读。
475
+ */
476
+ async listAccount() {
477
+ const data = await this.client.json("GET", "/api/daemon/instances");
478
+ return {
479
+ computers: (data.instances ?? []).map((instance) => ({
480
+ ...instance,
481
+ online: Boolean(instance.online)
482
+ }))
483
+ };
484
+ }
485
+ /**
486
+ * 列出当前用户的全部电脑,带上"在不在线"和"这次会话能不能用"。
487
+ * 服务端只查库,电脑离线时同样返回,所以拿得到"它离线了"这个结论。
488
+ */
489
+ list(sessionId) {
490
+ return this.client.json(
491
+ "GET",
492
+ `/api/sessions/${encodeURIComponent(sessionId)}/computers`
493
+ );
494
+ }
495
+ /**
496
+ * 启动或停止一台电脑。只改这次会话的可用范围,不会碰远程机器,
497
+ * 电脑离线时同样能登记,等它上线就直接可用。
498
+ */
499
+ setEnabled(sessionId, computer, enabled) {
500
+ return this.client.json(
501
+ "PUT",
502
+ `/api/sessions/${encodeURIComponent(sessionId)}/computers/${encodeURIComponent(computer)}/enabled`,
503
+ { enabled }
504
+ );
505
+ }
506
+ };
507
+
428
508
  // src/resources/models.ts
429
509
  var ModelsResource = class {
430
510
  constructor(client) {
@@ -473,7 +553,11 @@ var SessionInfo = type({
473
553
  "disable_tools?": "string[]",
474
554
  "runtime_type?": "string | null",
475
555
  "daemon_id?": "string | null",
556
+ "agent_runtime_id?": "string | null",
476
557
  "workspace_path?": "string | null",
558
+ "ba_version?": "string | null",
559
+ "sandbox_version?": "string | null",
560
+ "chat_group_id?": "string | null",
477
561
  "match?": "unknown"
478
562
  });
479
563
 
@@ -539,7 +623,7 @@ function upsertToolCall(state, toolCallId, toolName, displayName, args) {
539
623
  });
540
624
  }
541
625
  }
542
- function applyToolResult(state, toolCallId, result, status, durationMs, sourceLoop) {
626
+ function applyToolResult(state, toolCallId, result, status, durationMs, ranInParallel, sourceLoop, startedAt = null) {
543
627
  let toolName = "";
544
628
  let displayName = "";
545
629
  let finalResult = result;
@@ -551,6 +635,8 @@ function applyToolResult(state, toolCallId, result, status, durationMs, sourceLo
551
635
  }
552
636
  const update = { status, result: finalResult };
553
637
  if (durationMs != null) update.duration_ms = durationMs;
638
+ if (ranInParallel != null) update.ran_in_parallel = ranInParallel;
639
+ if (startedAt != null) update.started_at = startedAt;
554
640
  state.toolCalls[i] = { ...tc, ...update };
555
641
  toolName = tc.tool_name;
556
642
  displayName = tc.display_name;
@@ -732,11 +818,17 @@ function buildCompactionTurn(sequence, compactionId, loopId, status, data) {
732
818
  summary_preview: summaryPreview,
733
819
  summary_full: summaryFull,
734
820
  archived_count: archivedCount,
821
+ archived_files: Array.isArray(data.archived_files) ? data.archived_files.filter(
822
+ (item) => typeof item === "object" && item !== null && !Array.isArray(item)
823
+ ) : null,
824
+ archived_tool_calls: Array.isArray(data.archived_tool_calls) ? data.archived_tool_calls.filter(
825
+ (item) => typeof item === "object" && item !== null && !Array.isArray(item)
826
+ ) : null,
735
827
  tokens_before: tokensBefore,
736
828
  tokens_after: tokensAfter,
737
829
  saved_ratio: savedRatio,
738
830
  trigger: asTrigger(data.trigger),
739
- failure_reason: typeof data.reason === "string" && data.reason ? data.reason : null,
831
+ failure_reason: [data.failure_reason, data.fallback_reason, data.reason].find((value) => typeof value === "string" && value.length > 0) ?? null,
740
832
  fallback_applied: typeof data.fallback_applied === "boolean" ? data.fallback_applied : null
741
833
  };
742
834
  }
@@ -991,6 +1083,12 @@ function createTurnState(turnId, loopId, opts = {}) {
991
1083
  toolCalls: [],
992
1084
  usage: null,
993
1085
  responseDone: false,
1086
+ firstTokenMs: null,
1087
+ streamDurationMs: null,
1088
+ firstTokenDeliveryMs: null,
1089
+ modelStartedAt: null,
1090
+ modelDurationMs: null,
1091
+ modelStartedAtReported: false,
994
1092
  memoryRefs: opts.memoryRefs ?? null,
995
1093
  parentForkToolCallId: null
996
1094
  };
@@ -1008,8 +1106,13 @@ function snapshot(state, sequence, status) {
1008
1106
  tool_calls: [...state.toolCalls],
1009
1107
  model: state.model,
1010
1108
  usage: state.usage ? { ...state.usage } : null,
1011
- duration_ms: Math.max(0, Date.now() - state.startedAt),
1012
- started_at: new Date(state.startedAt).toISOString(),
1109
+ duration_ms: state.modelDurationMs,
1110
+ first_token_ms: state.firstTokenMs,
1111
+ stream_duration_ms: state.streamDurationMs,
1112
+ first_token_delivery_ms: state.firstTokenDeliveryMs,
1113
+ started_at: state.modelStartedAt ?? new Date(state.startedAt).toISOString(),
1114
+ // 没有 payload 上报时用的是 loop:turn 的浏览器事件时间,标记出来供瀑布区分。
1115
+ has_reported_timing: state.modelStartedAtReported,
1013
1116
  context_window: state.contextWindow,
1014
1117
  memory_refs: state.memoryRefs,
1015
1118
  parent_fork_tool_call_id: state.parentForkToolCallId
@@ -1023,10 +1126,130 @@ function toolStatusFromResult(result) {
1023
1126
  return inferToolStatus(text);
1024
1127
  }
1025
1128
 
1129
+ // src/schemas/context.ts
1130
+ var BUILTIN_CONTEXT_TITLES = {
1131
+ "agent-instructions": "\u9879\u76EE\u8BF4\u660E",
1132
+ "skill-catalog": "\u53EF\u7528\u80FD\u529B",
1133
+ "runtime-context": "\u5F53\u524D\u5DE5\u4F5C\u73AF\u5883",
1134
+ "workspace-context": "\u5F53\u524D\u5DE5\u4F5C\u533A\u73AF\u5883"
1135
+ };
1136
+ var BUILTIN_CONTEXT_DETAILS = {
1137
+ "agent-instructions": "\u8FD9\u4E9B\u8BF4\u660E\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u7406\u89E3\u5F53\u524D\u9879\u76EE\u548C\u5DE5\u4F5C\u8981\u6C42\u3002",
1138
+ "skill-catalog": "\u8FD9\u4E9B\u80FD\u529B\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u9009\u62E9\u5408\u9002\u7684\u65B9\u5F0F\u5B8C\u6210\u4EFB\u52A1\u3002",
1139
+ "runtime-context": "\u8FD9\u4E9B\u4FE1\u606F\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u4E86\u89E3\u5F53\u524D\u6587\u4EF6\u5939\u548C\u8FD0\u884C\u6761\u4EF6\u3002",
1140
+ "workspace-context": "\u8FD9\u4E9B\u4FE1\u606F\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u4E86\u89E3\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u3001\u6587\u4EF6\u548C\u8FD0\u884C\u73AF\u5883\u3002"
1141
+ };
1142
+ function nonEmptyString(value) {
1143
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1144
+ }
1145
+ function contextProjectionData(fields) {
1146
+ const contextKind = nonEmptyString(fields.context_kind);
1147
+ const contextKey = nonEmptyString(fields.context_key);
1148
+ const contextName = nonEmptyString(fields.context_name);
1149
+ const contextRevision = nonEmptyString(fields.context_revision);
1150
+ if (!contextKind || !contextKey || !contextName || !contextRevision || typeof fields.context_order !== "number" || !Number.isFinite(fields.context_order) || !fields.context_action) {
1151
+ return null;
1152
+ }
1153
+ return {
1154
+ context_kind: contextKind,
1155
+ context_key: contextKey,
1156
+ context_name: contextName,
1157
+ context_order: fields.context_order,
1158
+ context_action: fields.context_action,
1159
+ context_revision: contextRevision,
1160
+ display_title: nonEmptyString(fields.display_title),
1161
+ display_summary: nonEmptyString(fields.display_summary),
1162
+ sources: Array.isArray(fields.sources) ? fields.sources : []
1163
+ };
1164
+ }
1165
+ function getContextDisplayState(context) {
1166
+ const title = nonEmptyString(context.display_title) ?? BUILTIN_CONTEXT_TITLES[context.context_kind] ?? "\u5BF9\u8BDD\u80CC\u666F";
1167
+ const isRemoved = context.context_action === "removed";
1168
+ const summary = nonEmptyString(context.display_summary) ?? (isRemoved ? "\u5DF2\u505C\u6B62\u7528\u4E8E\u540E\u7EED\u5BF9\u8BDD" : context.context_action === "retained" ? "\u7EE7\u7EED\u7528\u4E8E\u540E\u7EED\u5BF9\u8BDD" : context.context_kind === "agent-instructions" ? "\u5DF2\u66F4\u65B0\u672C\u6B21\u5BF9\u8BDD\u4F7F\u7528\u7684\u9879\u76EE\u8BF4\u660E" : context.context_kind === "skill-catalog" ? "\u5DF2\u66F4\u65B0\u672C\u6B21\u53EF\u7528\u80FD\u529B" : context.context_kind === "runtime-context" ? "\u5DF2\u66F4\u65B0\u672C\u6B21\u5DE5\u4F5C\u73AF\u5883" : context.context_kind === "workspace-context" ? "\u5DF2\u66F4\u65B0\u672C\u6B21\u5DE5\u4F5C\u533A\u73AF\u5883" : "\u5DF2\u66F4\u65B0\u672C\u6B21\u5BF9\u8BDD\u80CC\u666F");
1169
+ const detail = isRemoved ? `${title}\u5DF2\u4E0D\u518D\u7528\u4E8E\u540E\u7EED\u5BF9\u8BDD\u3002` : BUILTIN_CONTEXT_DETAILS[context.context_kind] ?? "\u8FD9\u4E9B\u4FE1\u606F\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u7406\u89E3\u5F53\u524D\u4EFB\u52A1\u3002";
1170
+ return { title, summary, detail, isRemoved };
1171
+ }
1172
+ function groupAdjacentContextRuns(kinds) {
1173
+ const runs = [];
1174
+ let current = null;
1175
+ kinds.forEach((kind, index) => {
1176
+ if (kind === null) {
1177
+ current = null;
1178
+ return;
1179
+ }
1180
+ if (current === null) {
1181
+ current = [];
1182
+ runs.push(current);
1183
+ }
1184
+ current.push(index);
1185
+ });
1186
+ return runs;
1187
+ }
1188
+ function getContextGroupDisplayState(contexts) {
1189
+ const count = contexts.length;
1190
+ if (count === 1) {
1191
+ const single = getContextDisplayState(contexts[0]);
1192
+ return { title: single.title, summary: single.summary, count };
1193
+ }
1194
+ const removed = contexts.filter(
1195
+ (context) => context.context_action === "removed"
1196
+ ).length;
1197
+ const updated = count - removed;
1198
+ const parts = [];
1199
+ if (updated > 0) parts.push(`${updated} \u9879\u5DF2\u66F4\u65B0`);
1200
+ if (removed > 0) parts.push(`${removed} \u9879\u5DF2\u505C\u7528`);
1201
+ return {
1202
+ title: "\u5BF9\u8BDD\u80CC\u666F",
1203
+ summary: parts.join(" \xB7 ") || `${count} \u9879`,
1204
+ count
1205
+ };
1206
+ }
1207
+
1208
+ // src/shared/projection/context.ts
1209
+ function buildContextTurn(entry, sequence) {
1210
+ if (entry.kind !== "context" || !isRecord(entry.source) || !isRecord(entry.data)) {
1211
+ return null;
1212
+ }
1213
+ const display = isRecord(entry.data.display) ? entry.data.display : {};
1214
+ const message = isRecord(entry.message) ? entry.message : {};
1215
+ const context = contextProjectionData({
1216
+ context_kind: typeof entry.source.kind === "string" ? entry.source.kind : null,
1217
+ context_key: typeof entry.source.key === "string" ? entry.source.key : null,
1218
+ context_name: typeof entry.data.contribution_name === "string" ? entry.data.contribution_name : null,
1219
+ context_order: typeof entry.data.order === "number" ? entry.data.order : null,
1220
+ context_action: entry.data.action === "published" || entry.data.action === "retained" || entry.data.action === "removed" ? entry.data.action : null,
1221
+ context_revision: typeof entry.data.revision === "string" ? entry.data.revision : null,
1222
+ display_title: typeof display.title === "string" ? display.title : null,
1223
+ display_summary: typeof display.summary === "string" ? display.summary : null,
1224
+ sources: Array.isArray(display.sources) ? display.sources.filter(isRecord) : []
1225
+ });
1226
+ if (!context) return null;
1227
+ const turnId = typeof entry.id === "string" && entry.id ? entry.id : `context-${sequence}`;
1228
+ return {
1229
+ id: turnId,
1230
+ sequence,
1231
+ turn_id: turnId,
1232
+ loop_id: typeof entry.loop_name === "string" && entry.loop_name ? entry.loop_name : "root",
1233
+ kind: "context",
1234
+ role: "system",
1235
+ status: "completed",
1236
+ blocks: [],
1237
+ tool_calls: [],
1238
+ model: null,
1239
+ usage: null,
1240
+ duration_ms: 0,
1241
+ context_content: Object.hasOwn(message, "content") ? message.content : null,
1242
+ ...context
1243
+ };
1244
+ }
1245
+
1026
1246
  // src/shared/projection/builder.ts
1247
+ function acknowledgeTurnEvents(acknowledge) {
1248
+ acknowledge?.();
1249
+ }
1027
1250
  function hasChatRunEvent(events) {
1028
1251
  return events.some(
1029
- (event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
1252
+ (event) => event.type !== "llm:first_token:delivered" && event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
1030
1253
  );
1031
1254
  }
1032
1255
  function acceptedPostChatFollowupCompletesLatestRun(events, updates) {
@@ -1058,7 +1281,8 @@ function completedResponseText(content) {
1058
1281
  ).filter((part) => part.type === "text").map((part) => String(part.text ?? "")).join("");
1059
1282
  }
1060
1283
  function replaceTextContent(state, content) {
1061
- if (state.blocks.some((block) => block.type === "text")) return;
1284
+ const currentContent = state.blocks.filter((block) => block.type === "text").map((block) => String(block.content ?? "")).join("");
1285
+ if (currentContent === content) return;
1062
1286
  const replacement = { type: "text", content };
1063
1287
  const blocks = [];
1064
1288
  let inserted = false;
@@ -1078,11 +1302,39 @@ function replaceTextContent(state, content) {
1078
1302
  }
1079
1303
  state.blocks = blocks;
1080
1304
  }
1305
+ function applyResponseBlockOrder(state, value) {
1306
+ if (!Array.isArray(value)) return false;
1307
+ const ordered = [];
1308
+ for (const item of value) {
1309
+ if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
1310
+ const block = item;
1311
+ if (block.type === "text" || block.type === "thinking") {
1312
+ const content = String(block[block.type] ?? "");
1313
+ if (content) ordered.push({ type: block.type, content });
1314
+ continue;
1315
+ }
1316
+ if (block.type !== "tool_call") continue;
1317
+ const toolCallId = String(block.id ?? "");
1318
+ const toolBlock = state.blocks.find(
1319
+ (candidate) => candidate.type === "tool_use" && candidate.tool_call_id === toolCallId
1320
+ );
1321
+ if (toolBlock) ordered.push(toolBlock);
1322
+ }
1323
+ if (ordered.length === 0 && value.length > 0) return false;
1324
+ const unrelatedBlocks = state.blocks.filter(
1325
+ (block) => block.type !== "text" && block.type !== "thinking" && block.type !== "tool_use"
1326
+ );
1327
+ state.blocks = [...ordered, ...unrelatedBlocks];
1328
+ return true;
1329
+ }
1081
1330
  var ClientProjectionBuilder = class {
1082
1331
  activeTurns = /* @__PURE__ */ new Map();
1083
1332
  lastTurnIds = /* @__PURE__ */ new Map();
1084
1333
  latestTurns = /* @__PURE__ */ new Map();
1334
+ turnsById = /* @__PURE__ */ new Map();
1085
1335
  loopDescriptions = /* @__PURE__ */ new Map();
1336
+ /** loop_name → 正在运行的子智能体起点,agent:end 结算并挂到父工具调用上。 */
1337
+ activeSubagentRuns = /* @__PURE__ */ new Map();
1086
1338
  pendingChildPauses = /* @__PURE__ */ new Map();
1087
1339
  pendingMemoryRefs = /* @__PURE__ */ new Map();
1088
1340
  activeCompactions = /* @__PURE__ */ new Map();
@@ -1117,6 +1369,8 @@ var ClientProjectionBuilder = class {
1117
1369
  return this.onMemoryInject(loopId, payload);
1118
1370
  case "memory:inject:none":
1119
1371
  return null;
1372
+ case "context:entry":
1373
+ return this.onContextEntry(payload);
1120
1374
  case "loop:turn":
1121
1375
  return this.onLoopTurn(loopId, payload);
1122
1376
  case "mode:change":
@@ -1140,6 +1394,25 @@ var ClientProjectionBuilder = class {
1140
1394
  }
1141
1395
  if (event.type.startsWith("compaction:"))
1142
1396
  return this.onCompaction(event.type, loopId, payload);
1397
+ if (event.type === "llm:first_token:delivered") {
1398
+ const targetTurnId = String(payload.turn_id ?? "");
1399
+ const active = this.activeTurns.get(loopId);
1400
+ if (active && (!targetTurnId || active.turnId === targetTurnId)) {
1401
+ this.applyFirstTokenDelivery(active, payload);
1402
+ return this.syncPatch(active);
1403
+ }
1404
+ const latest = targetTurnId ? this.turnsById.get(targetTurnId) : this.latestTurns.get(loopId);
1405
+ const duration = payload.first_token_delivery_ms;
1406
+ if (!latest || latest.role !== "assistant" || typeof duration !== "number" || duration < 0) {
1407
+ return null;
1408
+ }
1409
+ const turn = { ...latest, first_token_delivery_ms: duration };
1410
+ this.turnsById.set(turn.turn_id, turn);
1411
+ if (this.latestTurns.get(loopId)?.turn_id === turn.turn_id) {
1412
+ this.latestTurns.set(loopId, turn);
1413
+ }
1414
+ return [{ kind: "upsert", turn }];
1415
+ }
1143
1416
  const state = this.activeTurns.get(loopId);
1144
1417
  if (!state) {
1145
1418
  if (event.type === "chat:end") {
@@ -1155,16 +1428,28 @@ var ClientProjectionBuilder = class {
1155
1428
  state.toolCalls = [];
1156
1429
  state.usage = null;
1157
1430
  state.responseDone = false;
1431
+ state.firstTokenMs = null;
1432
+ state.streamDurationMs = null;
1433
+ state.firstTokenDeliveryMs = null;
1158
1434
  return this.syncPatch(state);
1159
1435
  case "llm:thinking:delta":
1436
+ this.applyFirstTokenDelivery(state, payload);
1160
1437
  return this.onDelta(state, "thinking", "", String(payload.content ?? ""));
1161
1438
  case "llm:content:delta":
1439
+ this.applyFirstTokenDelivery(state, payload);
1162
1440
  return this.onDelta(state, "text", "", String(payload.content ?? ""));
1163
1441
  case "llm:content:complete":
1164
1442
  replaceTextContent(state, String(payload.content ?? ""));
1165
1443
  return this.syncPatch(state);
1444
+ case "llm:content:done":
1445
+ this.applyStreamTiming(state, payload);
1446
+ return this.syncPatch(state);
1166
1447
  case "llm:tool_call:created":
1448
+ this.applyFirstTokenDelivery(state, payload);
1167
1449
  return this.onToolCallCreated(state, payload);
1450
+ case "llm:tool_call:arguments:delta":
1451
+ this.applyFirstTokenDelivery(state, payload);
1452
+ return this.onToolCallArgumentsDelta(state, payload);
1168
1453
  case "llm:response:done":
1169
1454
  return this.onResponseDone(state, loopId, payload);
1170
1455
  case "tool:result:delta":
@@ -1191,7 +1476,9 @@ var ClientProjectionBuilder = class {
1191
1476
  this.activeTurns.clear();
1192
1477
  this.lastTurnIds.clear();
1193
1478
  this.latestTurns.clear();
1479
+ this.turnsById.clear();
1194
1480
  this.loopDescriptions.clear();
1481
+ this.activeSubagentRuns.clear();
1195
1482
  this.pendingChildPauses.clear();
1196
1483
  this.pendingMemoryRefs.clear();
1197
1484
  this.activeCompactions.clear();
@@ -1206,15 +1493,18 @@ var ClientProjectionBuilder = class {
1206
1493
  }
1207
1494
  _seedTurns(turns) {
1208
1495
  const latestTurns = /* @__PURE__ */ new Map();
1496
+ const turnsById = /* @__PURE__ */ new Map();
1209
1497
  for (const turn of turns) {
1210
1498
  this.seedSequence(turn.sequence);
1211
1499
  if (!isConversationTurn(turn)) continue;
1500
+ turnsById.set(turn.turn_id, turn);
1212
1501
  const current = latestTurns.get(turn.loop_id);
1213
1502
  if (!current || turn.sequence >= current.sequence) {
1214
1503
  latestTurns.set(turn.loop_id, turn);
1215
1504
  }
1216
1505
  }
1217
1506
  this.latestTurns = latestTurns;
1507
+ this.turnsById = turnsById;
1218
1508
  }
1219
1509
  // --- Event handlers ---
1220
1510
  onMemoryInject(loopId, payload) {
@@ -1232,11 +1522,16 @@ var ClientProjectionBuilder = class {
1232
1522
  }
1233
1523
  return null;
1234
1524
  }
1525
+ onContextEntry(payload) {
1526
+ if (!isRecord(payload.entry)) return null;
1527
+ const turn = buildContextTurn(payload.entry, this.nextSeq());
1528
+ return turn ? [{ kind: "upsert", turn }] : null;
1529
+ }
1235
1530
  onLoopTurn(loopId, payload) {
1236
1531
  let turnId = String(payload.turn_id ?? this.nextTurnId(`turn:${loopId}`));
1237
1532
  let memoryRefs = null;
1238
1533
  const pending = this.pendingMemoryRefs.get(loopId);
1239
- if (pending) {
1534
+ if (pending && (!pending.turnId || pending.turnId === turnId)) {
1240
1535
  this.pendingMemoryRefs.delete(loopId);
1241
1536
  if (pending.turnId) turnId = pending.turnId;
1242
1537
  memoryRefs = pending.refs;
@@ -1252,6 +1547,7 @@ var ClientProjectionBuilder = class {
1252
1547
  this.lastTurnIds.set(loopId, turnId);
1253
1548
  const turn = snapshot(state, this.nextSeq(), "streaming");
1254
1549
  this.latestTurns.set(loopId, turn);
1550
+ this.turnsById.set(turn.turn_id, turn);
1255
1551
  return [{ kind: "upsert", turn }];
1256
1552
  }
1257
1553
  onModeChange(loopId, payload) {
@@ -1288,11 +1584,11 @@ var ClientProjectionBuilder = class {
1288
1584
  loopId,
1289
1585
  toolCallId: optStr(payload.tool_call_id) ?? void 0,
1290
1586
  toolName: toolNameFromPayload(payload) ?? void 0,
1291
- workspacePath: isRecord2(payload.workspace_change) ? optStr(payload.workspace_change.to) ?? void 0 : void 0
1587
+ workspacePath: isRecord(payload.workspace_change) ? optStr(payload.workspace_change.to) ?? void 0 : void 0
1292
1588
  }
1293
1589
  ];
1294
1590
  const change = payload.workspace_change;
1295
- if (isRecord2(change)) {
1591
+ if (isRecord(change)) {
1296
1592
  const source = optStr(change.source) ?? "";
1297
1593
  const projectName = optStr(change.project_name) ?? "";
1298
1594
  const fromPath = optStr(change.from) ?? "";
@@ -1324,6 +1620,13 @@ var ClientProjectionBuilder = class {
1324
1620
  onAgentStart(loopId, payload) {
1325
1621
  const description = optStr(payload.description);
1326
1622
  if (description) this.loopDescriptions.set(loopId, description);
1623
+ const subagentStartedAt = optStr(payload.started_at);
1624
+ if (subagentStartedAt) {
1625
+ this.activeSubagentRuns.set(loopId, {
1626
+ runId: optStr(payload.run_id) ?? loopId,
1627
+ startedAt: subagentStartedAt
1628
+ });
1629
+ }
1327
1630
  const turn = buildSystemNotificationTurn(
1328
1631
  this.nextSeq(),
1329
1632
  this.nextTurnId("agent:start"),
@@ -1385,7 +1688,32 @@ var ClientProjectionBuilder = class {
1385
1688
  }
1386
1689
  const directTurn = this.activeTurns.get(loopId) ?? this.latestTurns.get(loopId);
1387
1690
  setToolCallStatus(directTurn, toolCallId, "done", null, null);
1388
- return this.collectLoopUpdates(loopId, parentLoopId);
1691
+ const updates = this.collectLoopUpdates(loopId, parentLoopId);
1692
+ const entryId = typeof payload.entry_id === "string" ? payload.entry_id : "";
1693
+ if (!entryId) return updates;
1694
+ const answerData = isRecord(payload.answer_data) ? payload.answer_data : {};
1695
+ const answerTurn = {
1696
+ id: entryId,
1697
+ sequence: this.nextSeq(),
1698
+ turn_id: entryId,
1699
+ loop_id: loopId,
1700
+ kind: "message",
1701
+ role: "assistant",
1702
+ status: "completed",
1703
+ blocks: [
1704
+ {
1705
+ type: "ask_user_answer",
1706
+ content: answerData,
1707
+ tool_call_id: toolCallId || null
1708
+ }
1709
+ ],
1710
+ tool_calls: [],
1711
+ model: null,
1712
+ usage: null,
1713
+ duration_ms: 0,
1714
+ client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
1715
+ };
1716
+ return [...updates, { kind: "upsert", turn: answerTurn }];
1389
1717
  }
1390
1718
  onUserMessage(_loopId, payload) {
1391
1719
  const entryId = String(payload.entry_id ?? this.nextTurnId("user"));
@@ -1402,7 +1730,8 @@ var ClientProjectionBuilder = class {
1402
1730
  tool_calls: [],
1403
1731
  model: null,
1404
1732
  usage: null,
1405
- duration_ms: 0
1733
+ duration_ms: 0,
1734
+ client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
1406
1735
  };
1407
1736
  this.latestTurns.set("root", turn);
1408
1737
  return [{ kind: "upsert", turn }];
@@ -1430,13 +1759,7 @@ var ClientProjectionBuilder = class {
1430
1759
  }
1431
1760
  onToolCallCreated(state, payload) {
1432
1761
  if ("arguments_delta" in payload) {
1433
- const delta = String(payload.arguments_delta ?? "");
1434
- const toolCallId2 = String(payload.id ?? "");
1435
- if (delta && toolCallId2) {
1436
- appendToolCallArguments(state, toolCallId2, delta);
1437
- return this.syncPatch(state);
1438
- }
1439
- return null;
1762
+ return this.onToolCallArgumentsDelta(state, payload);
1440
1763
  }
1441
1764
  const toolCallId = String(payload.id ?? "");
1442
1765
  const toolName = toolNameFromPayload(payload);
@@ -1445,16 +1768,21 @@ var ClientProjectionBuilder = class {
1445
1768
  upsertToolCall(state, toolCallId, toolName, displayName, args);
1446
1769
  return this.syncPatch(state);
1447
1770
  }
1771
+ onToolCallArgumentsDelta(state, payload) {
1772
+ const delta = String(payload.arguments_delta ?? "");
1773
+ const toolCallId = String(payload.id ?? "");
1774
+ if (!delta || !toolCallId) return null;
1775
+ appendToolCallArguments(state, toolCallId, delta);
1776
+ return this.syncPatch(state);
1777
+ }
1448
1778
  onResponseDone(state, loopId, payload) {
1449
1779
  state.responseDone = true;
1780
+ this.applyStreamTiming(state, payload);
1450
1781
  const model = optStr(payload.model);
1451
1782
  if (model) state.model = model;
1452
1783
  if (typeof payload.usage === "object" && payload.usage !== null) {
1453
1784
  state.usage = { ...payload.usage };
1454
1785
  }
1455
- if ("content" in payload) {
1456
- replaceTextContent(state, completedResponseText(payload.content));
1457
- }
1458
1786
  const toolCalls = payload.tool_calls;
1459
1787
  if (Array.isArray(toolCalls)) {
1460
1788
  for (const raw of toolCalls) {
@@ -1472,11 +1800,42 @@ var ClientProjectionBuilder = class {
1472
1800
  );
1473
1801
  }
1474
1802
  }
1475
- if (state.toolCalls.length === 0) {
1803
+ if (!applyResponseBlockOrder(state, payload.response_blocks) && "content" in payload) {
1804
+ replaceTextContent(state, completedResponseText(payload.content));
1805
+ }
1806
+ const isNativeProviderResponse = typeof payload.provider === "string" && payload.provider !== "";
1807
+ if (state.toolCalls.length === 0 || isNativeProviderResponse && state.toolCalls.every(
1808
+ (toolCall) => toolCall.status !== "pending" && toolCall.status !== "awaiting_answer"
1809
+ )) {
1476
1810
  return this.finalize(loopId, "completed");
1477
1811
  }
1478
1812
  return this.syncPatch(state);
1479
1813
  }
1814
+ applyFirstTokenDelivery(state, payload) {
1815
+ const firstTokenMs = payload.first_token_ms;
1816
+ if (state.firstTokenMs === null && typeof firstTokenMs === "number" && firstTokenMs >= 0) {
1817
+ state.firstTokenMs = Math.round(firstTokenMs);
1818
+ }
1819
+ const duration = payload.first_token_delivery_ms;
1820
+ if (state.firstTokenDeliveryMs === null && typeof duration === "number" && duration >= 0) {
1821
+ state.firstTokenDeliveryMs = duration;
1822
+ }
1823
+ }
1824
+ applyStreamTiming(state, payload) {
1825
+ if (typeof payload.first_token_ms === "number" && payload.first_token_ms >= 0) {
1826
+ state.firstTokenMs = Math.round(payload.first_token_ms);
1827
+ }
1828
+ if (typeof payload.stream_duration_ms === "number" && payload.stream_duration_ms >= 0) {
1829
+ state.streamDurationMs = Math.round(payload.stream_duration_ms);
1830
+ }
1831
+ if (typeof payload.started_at === "string" && payload.started_at) {
1832
+ state.modelStartedAt = payload.started_at;
1833
+ state.modelStartedAtReported = true;
1834
+ }
1835
+ if (typeof payload.duration_ms === "number" && payload.duration_ms >= 0) {
1836
+ state.modelDurationMs = Math.round(payload.duration_ms);
1837
+ }
1838
+ }
1480
1839
  onToolResultDone(state, loopId, payload) {
1481
1840
  const toolCallId = String(payload.tool_call_id ?? "");
1482
1841
  const streamedResult = state.toolCalls.find((toolCall) => toolCall.id === toolCallId)?.result;
@@ -1484,9 +1843,12 @@ var ClientProjectionBuilder = class {
1484
1843
  const status = toolStatusFromResult(content);
1485
1844
  const rawDuration = payload.duration_ms;
1486
1845
  const durationMs = typeof rawDuration === "number" && rawDuration >= 0 ? Math.round(rawDuration) : null;
1846
+ const ranInParallel = typeof payload.ran_in_parallel === "boolean" ? payload.ran_in_parallel : null;
1487
1847
  const sl = sourceLoopFor(state.loopId, this.loopDescriptions);
1488
- applyToolResult(state, toolCallId, content, status, durationMs, sl);
1489
- if (this.shouldFinalizeAfterToolResults(state)) {
1848
+ const startedAt = typeof payload.started_at === "string" && payload.started_at ? payload.started_at : null;
1849
+ applyToolResult(state, toolCallId, content, status, durationMs, ranInParallel, sl, startedAt);
1850
+ const isNativeProviderResult = typeof payload.provider === "string" && payload.provider !== "";
1851
+ if (!isNativeProviderResult && this.shouldFinalizeAfterToolResults(state)) {
1490
1852
  return this.finalize(loopId, "completed");
1491
1853
  }
1492
1854
  return this.syncPatch(state);
@@ -1515,10 +1877,13 @@ var ClientProjectionBuilder = class {
1515
1877
  onToolBridge(state, payload) {
1516
1878
  const bridge = payload.bridge;
1517
1879
  if (typeof bridge !== "object" || bridge === null) return null;
1880
+ const toolCallId = String(payload.tool_call_id ?? "");
1881
+ if (!toolCallId || !state.toolCalls.some((toolCall) => toolCall.id === toolCallId)) return null;
1518
1882
  state.blocks.push({
1519
1883
  type: "tool_bridge",
1520
1884
  content: bridge,
1521
- tool_call_id: String(payload.tool_call_id ?? "") || null
1885
+ delivery_id: String(payload.delivery_id ?? "") || null,
1886
+ tool_call_id: toolCallId
1522
1887
  });
1523
1888
  return this.syncPatch(state);
1524
1889
  }
@@ -1527,6 +1892,11 @@ var ClientProjectionBuilder = class {
1527
1892
  const toolName = String(payload.tool_name ?? "");
1528
1893
  const displayName = this.displayNameResolver(toolName);
1529
1894
  upsertToolCall(state, toolCallId, toolName, displayName, "");
1895
+ const relatedToolCallId = String(payload.related_tool_call_id ?? "");
1896
+ if (relatedToolCallId && relatedToolCallId !== toolCallId) {
1897
+ const related = state.toolCalls.find((toolCall) => toolCall.id === relatedToolCallId);
1898
+ if (related) related.status = "awaiting_answer";
1899
+ }
1530
1900
  return this.syncPatch(state);
1531
1901
  }
1532
1902
  onChildPause(state, loopId, payload) {
@@ -1632,6 +2002,7 @@ var ClientProjectionBuilder = class {
1632
2002
  }
1633
2003
  onAgentEnd(state, loopId, payload) {
1634
2004
  const ok = payload.ok !== false;
2005
+ this.settleSubagentRun(loopId, payload);
1635
2006
  const status = ok ? "done" : "error";
1636
2007
  const title = `Agent: ${payload.description ?? payload.skill_id ?? "agent"}`;
1637
2008
  const detail = optStr(payload.error);
@@ -1658,6 +2029,32 @@ var ClientProjectionBuilder = class {
1658
2029
  );
1659
2030
  return [{ kind: "upsert", turn }];
1660
2031
  }
2032
+ settleSubagentRun(loopId, payload) {
2033
+ const active = this.activeSubagentRuns.get(loopId);
2034
+ this.activeSubagentRuns.delete(loopId);
2035
+ const parentToolCallId = optStr(payload.parent_fork_tool_call_id);
2036
+ if (!parentToolCallId) return;
2037
+ const rawDuration = payload.run_duration_ms;
2038
+ const durationMs = typeof rawDuration === "number" && rawDuration >= 0 ? Math.round(rawDuration) : null;
2039
+ const rawStatus = optStr(payload.run_status);
2040
+ const status = rawStatus === "failed" || rawStatus === "cancelled" || rawStatus === "paused" || rawStatus === "incomplete" ? rawStatus : payload.ok === false ? "failed" : "completed";
2041
+ const run = {
2042
+ run_id: active?.runId ?? optStr(payload.run_id) ?? loopId,
2043
+ scope: "subagent",
2044
+ loop_id: loopId,
2045
+ started_at: active?.startedAt ?? optStr(payload.run_started_at),
2046
+ duration_ms: durationMs,
2047
+ status
2048
+ };
2049
+ const seen = /* @__PURE__ */ new Set();
2050
+ for (const turn of [...this.latestTurns.values(), ...this.turnsById.values()]) {
2051
+ if (seen.has(turn)) continue;
2052
+ seen.add(turn);
2053
+ const toolCall = turn.tool_calls.find((item) => item.id === parentToolCallId);
2054
+ if (!toolCall) continue;
2055
+ toolCall.subagent_runs = [...toolCall.subagent_runs ?? [], run];
2056
+ }
2057
+ }
1661
2058
  dropIncompleteToolCalls(state) {
1662
2059
  if (state.responseDone) return;
1663
2060
  state.toolCalls = [];
@@ -1707,6 +2104,7 @@ var ClientProjectionBuilder = class {
1707
2104
  syncPatch(state) {
1708
2105
  const turn = snapshot(state, this.nextSeq(), "streaming");
1709
2106
  this.latestTurns.set(state.loopId, turn);
2107
+ this.turnsById.set(turn.turn_id, turn);
1710
2108
  this.lastTurnIds.set(state.loopId, turn.turn_id);
1711
2109
  return [{ kind: "upsert", turn }];
1712
2110
  }
@@ -1717,6 +2115,7 @@ var ClientProjectionBuilder = class {
1717
2115
  const turn = snapshot(state, this.nextSeq(), status);
1718
2116
  this.lastTurnIds.set(loopId, turn.turn_id);
1719
2117
  this.latestTurns.set(loopId, turn);
2118
+ this.turnsById.set(turn.turn_id, turn);
1720
2119
  return [{ kind: "upsert", turn }];
1721
2120
  }
1722
2121
  shouldFinalizeAfterToolResults(state) {
@@ -1754,9 +2153,6 @@ var ClientProjectionBuilder = class {
1754
2153
  return `${prefix}:${this.syntheticCounter}:${hex}`;
1755
2154
  }
1756
2155
  };
1757
- function isRecord2(value) {
1758
- return typeof value === "object" && value !== null && !Array.isArray(value);
1759
- }
1760
2156
  function optStr(value) {
1761
2157
  if (value == null || value === "") return null;
1762
2158
  return String(value);
@@ -1783,6 +2179,8 @@ function projectHistory(entries) {
1783
2179
  const latestAssistantByLoop = /* @__PURE__ */ new Map();
1784
2180
  const pendingChildPauses = /* @__PURE__ */ new Map();
1785
2181
  const answeredToolCallIds = /* @__PURE__ */ new Set();
2182
+ const nativeToolTurns = /* @__PURE__ */ new Set();
2183
+ const turnEntryAliases = /* @__PURE__ */ new Map();
1786
2184
  let latestRootMessage = null;
1787
2185
  let latestRootAssistant = null;
1788
2186
  let sequence = 0;
@@ -1790,7 +2188,40 @@ function projectHistory(entries) {
1790
2188
  const kind = String(entry.kind ?? "");
1791
2189
  const loopId = String(entry.loop_name ?? "root");
1792
2190
  const id = String(entry.id ?? `history-${sequence + 1}`);
1793
- if (kind === "post_chat_followup" && isRecord3(entry.data)) {
2191
+ if (kind === "turn_timing" && isRecord(entry.data)) {
2192
+ const turnId = typeof entry.data.turn_id === "string" ? entry.data.turn_id : "";
2193
+ const duration = numberOrNull(entry.data.first_token_delivery_ms);
2194
+ const target = turnEntryAliases.get(turnId);
2195
+ if (target && duration !== null) target.first_token_delivery_ms = duration;
2196
+ continue;
2197
+ }
2198
+ if (kind === "run_timing" && isRecord(entry.data)) {
2199
+ const data = entry.data;
2200
+ if (data.scope === "subagent") {
2201
+ const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
2202
+ const duration = numberOrNull(data.duration_ms);
2203
+ const rawStatus = String(data.status ?? "completed");
2204
+ const status = rawStatus === "failed" || rawStatus === "cancelled" || rawStatus === "paused" || rawStatus === "incomplete" ? rawStatus : "completed";
2205
+ for (const turn of turns) {
2206
+ const toolCall = turn.tool_calls.find((item) => item.id === parentToolCallId);
2207
+ if (!toolCall) continue;
2208
+ toolCall.subagent_runs = [
2209
+ ...toolCall.subagent_runs ?? [],
2210
+ {
2211
+ run_id: String(data.run_id ?? ""),
2212
+ scope: "subagent",
2213
+ loop_id: loopId,
2214
+ started_at: typeof data.started_at === "string" ? data.started_at : null,
2215
+ duration_ms: duration,
2216
+ status
2217
+ }
2218
+ ];
2219
+ break;
2220
+ }
2221
+ }
2222
+ continue;
2223
+ }
2224
+ if (kind === "post_chat_followup" && isRecord(entry.data)) {
1794
2225
  const assistantEntryId = String(entry.data.assistant_entry_id ?? "");
1795
2226
  if (latestRootMessage?.role === "assistant" && latestRootMessage.turn_id === assistantEntryId) {
1796
2227
  const followup = normalizePostChatFollowup(entry.data, assistantEntryId);
@@ -1798,7 +2229,15 @@ function projectHistory(entries) {
1798
2229
  }
1799
2230
  continue;
1800
2231
  }
1801
- if (kind === "message" && isRecord3(entry.message)) {
2232
+ if (kind === "context") {
2233
+ const turn = buildContextTurn(entry, sequence + 1);
2234
+ if (turn) {
2235
+ sequence += 1;
2236
+ turns.push(turn);
2237
+ }
2238
+ continue;
2239
+ }
2240
+ if (kind === "message" && isRecord(entry.message)) {
1802
2241
  const message = entry.message;
1803
2242
  const role = String(message.role ?? "");
1804
2243
  if (role === "tool") {
@@ -1808,6 +2247,12 @@ function projectHistory(entries) {
1808
2247
  if (owner && toolCall) {
1809
2248
  toolCall.status = message._error ? "error" : inferToolStatus(String(message.content ?? ""));
1810
2249
  toolCall.result = message.content ?? "";
2250
+ const duration = numberOrNull(message._duration_ms);
2251
+ if (duration !== null) toolCall.duration_ms = duration;
2252
+ if (typeof message._started_at === "string") {
2253
+ toolCall.started_at = message._started_at;
2254
+ }
2255
+ if (message._ran_in_parallel === true) toolCall.ran_in_parallel = true;
1811
2256
  owner.blocks.push({
1812
2257
  type: "tool_result",
1813
2258
  content: message.content ?? "",
@@ -1817,7 +2262,7 @@ function projectHistory(entries) {
1817
2262
  continue;
1818
2263
  }
1819
2264
  if (role !== "user" && role !== "assistant" && role !== "system") continue;
1820
- const toolCalls = role === "assistant" ? buildToolCalls(message.tool_calls) : [];
2265
+ const toolCalls = role === "assistant" && !message._error ? buildToolCalls(message.tool_calls) : [];
1821
2266
  const blocks = buildBlocks(message, toolCalls);
1822
2267
  const turn = {
1823
2268
  id,
@@ -1830,14 +2275,52 @@ function projectHistory(entries) {
1830
2275
  blocks,
1831
2276
  tool_calls: toolCalls,
1832
2277
  model: stringOrNull(message.model),
1833
- usage: isRecord3(message._usage) ? { ...message._usage } : null,
1834
- duration_ms: numberOrZero(message._duration_ms),
1835
- started_at: typeof entry.timestamp === "string" ? entry.timestamp : void 0
2278
+ usage: isRecord(message._usage) ? { ...message._usage } : null,
2279
+ // 模型请求区间来自持久化字段;旧会话缺失时为 null,不用落盘时刻冒充(#2581 A1)。
2280
+ duration_ms: numberOrNull(message._duration_ms),
2281
+ first_token_ms: numberOrNull(message._first_token_ms),
2282
+ stream_duration_ms: numberOrNull(message._stream_duration_ms),
2283
+ started_at: typeof message._started_at === "string" ? message._started_at : typeof entry.timestamp === "string" ? entry.timestamp : void 0,
2284
+ has_reported_timing: typeof message._started_at === "string",
2285
+ // fork 子会话的父锚点:历史里落在消息级 _parent_fork_tool_call_id。丢掉它,
2286
+ // 刷新后工作流步骤 fork 就配不回 workflow:run 工具调用(普通 Agent fork 有
2287
+ // root Agent 工具调用兜底配对,工作流 fork 没有,子智能体卡会整体消失)。
2288
+ parent_fork_tool_call_id: stringOrNull(message._parent_fork_tool_call_id)
1836
2289
  };
2290
+ const isNativeToolEntry = role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.some(
2291
+ (toolCall) => isRecord(toolCall) && typeof toolCall.provider === "string"
2292
+ );
2293
+ const previousTurn = turns[turns.length - 1];
2294
+ if (role === "assistant" && previousTurn?.role === "assistant" && previousTurn.loop_id === loopId && nativeToolTurns.has(previousTurn)) {
2295
+ nativeToolTurns.delete(previousTurn);
2296
+ previousTurn.blocks.push(...turn.blocks);
2297
+ previousTurn.tool_calls.push(...turn.tool_calls);
2298
+ previousTurn.status = turn.status;
2299
+ previousTurn.model = turn.model ?? previousTurn.model;
2300
+ previousTurn.usage = turn.usage ?? previousTurn.usage;
2301
+ if (turn.duration_ms) previousTurn.duration_ms = turn.duration_ms;
2302
+ previousTurn.first_token_ms = turn.first_token_ms ?? previousTurn.first_token_ms;
2303
+ previousTurn.stream_duration_ms = turn.stream_duration_ms ?? previousTurn.stream_duration_ms;
2304
+ if (turn.has_reported_timing) {
2305
+ previousTurn.started_at = turn.started_at;
2306
+ previousTurn.has_reported_timing = true;
2307
+ } else if (!previousTurn.has_reported_timing) {
2308
+ previousTurn.started_at = previousTurn.started_at ?? turn.started_at;
2309
+ }
2310
+ previousTurn.id = turn.id;
2311
+ previousTurn.turn_id = turn.turn_id;
2312
+ if (isNativeToolEntry) nativeToolTurns.add(previousTurn);
2313
+ turnEntryAliases.set(id, previousTurn);
2314
+ if (loopId === "root") latestRootMessage = previousTurn;
2315
+ latestAssistantByLoop.set(loopId, previousTurn);
2316
+ continue;
2317
+ }
1837
2318
  if (loopId === "root" && (role === "user" || role === "assistant")) {
1838
2319
  latestRootMessage = turn;
1839
2320
  }
1840
2321
  turns.push(turn);
2322
+ turnEntryAliases.set(id, turn);
2323
+ if (isNativeToolEntry) nativeToolTurns.add(turn);
1841
2324
  if (role === "assistant") latestAssistantByLoop.set(loopId, turn);
1842
2325
  if (loopId === "root") {
1843
2326
  if (role === "assistant") latestRootAssistant = turn;
@@ -1846,10 +2329,25 @@ function projectHistory(entries) {
1846
2329
  for (const toolCall of toolCalls) toolOwners.set(toolCall.id, turn);
1847
2330
  continue;
1848
2331
  }
2332
+ if (kind === "tool_result") {
2333
+ const data = entry.data ?? {};
2334
+ const toolCallId = String(data.tool_call_id ?? "");
2335
+ const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
2336
+ if (owner && toolCallId) {
2337
+ const rawContent = data.content;
2338
+ const content = typeof rawContent === "string" ? rawContent : Array.isArray(rawContent) ? rawContent : String(rawContent ?? "");
2339
+ const status = data.is_error === true ? "error" : inferToolStatus(typeof content === "string" ? content : JSON.stringify(content));
2340
+ setToolCallStatus(owner, toolCallId, status, content, null);
2341
+ if (!owner.blocks.some((block) => block.type === "tool_result" && block.tool_call_id === toolCallId)) {
2342
+ owner.blocks.push({ type: "tool_result", content, tool_call_id: toolCallId });
2343
+ }
2344
+ }
2345
+ continue;
2346
+ }
1849
2347
  if (kind === "mode_change") {
1850
2348
  turns.push(markerTurn(id, ++sequence, loopId, "mode_change", entry.data ?? {}));
1851
2349
  } else if (kind === "workspace_change") {
1852
- const change = isRecord3(entry.data) ? entry.data : {};
2350
+ const change = isRecord(entry.data) ? entry.data : {};
1853
2351
  const source = stringOrNull(change.source) ?? "";
1854
2352
  const projectName = stringOrNull(change.project_name) ?? "";
1855
2353
  const fromPath = stringOrNull(change.from) ?? "";
@@ -1878,19 +2376,21 @@ function projectHistory(entries) {
1878
2376
  } else if (kind === "tool_ui" || kind === "tool_bridge") {
1879
2377
  const data = entry.data ?? {};
1880
2378
  const toolCallId = String(data.tool_call_id ?? "");
1881
- const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
1882
- const content = kind === "tool_ui" ? data.ui : data.bridge;
2379
+ const owner = kind === "tool_bridge" ? toolCallId ? toolOwners.get(toolCallId) : void 0 : toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
2380
+ const rawContent = kind === "tool_ui" ? data.ui : data.bridge;
2381
+ const content = kind === "tool_ui" && isRecord(rawContent) ? { ...rawContent, archived: true } : rawContent;
1883
2382
  if (owner && content !== void 0) {
1884
2383
  owner.blocks.push({
1885
2384
  type: kind,
1886
2385
  content,
2386
+ ...kind === "tool_bridge" ? { delivery_id: id } : {},
1887
2387
  tool_call_id: toolCallId || null
1888
2388
  });
1889
2389
  }
1890
2390
  } else if (kind === "compaction" || kind === "tool_result_archive") {
1891
2391
  const data = entry.data ?? {};
1892
2392
  const compactionId = String(data.compaction_id ?? entry.id ?? `history-${sequence + 1}`);
1893
- let content = isRecord3(entry.message) ? entry.message.content : void 0;
2393
+ let content = isRecord(entry.message) ? entry.message.content : void 0;
1894
2394
  if (kind === "tool_result_archive") {
1895
2395
  const archivedCount = Array.isArray(data.archived_files) ? data.archived_files.length : 0;
1896
2396
  content = `<compaction-summary>
@@ -1898,18 +2398,24 @@ function projectHistory(entries) {
1898
2398
  </compaction-summary>`;
1899
2399
  }
1900
2400
  turns.push(
1901
- buildCompactionTurn(++sequence, compactionId, loopId, "completed", {
1902
- ...data,
1903
- content
1904
- })
2401
+ buildCompactionTurn(
2402
+ ++sequence,
2403
+ compactionId,
2404
+ loopId,
2405
+ data.status === "failed" ? "failed" : "completed",
2406
+ {
2407
+ ...data,
2408
+ content
2409
+ }
2410
+ )
1905
2411
  );
1906
2412
  } else if (kind === "child_pause") {
1907
2413
  const data = entry.data ?? {};
1908
- const sourceLoop = isRecord3(data.source_loop) ? data.source_loop : {};
2414
+ const sourceLoop = isRecord(data.source_loop) ? data.source_loop : {};
1909
2415
  const childLoopId = String(data.child_loop_name ?? sourceLoop.name ?? "");
1910
2416
  const childToolCallId = String(data.child_pause_tool_call_id ?? "");
1911
2417
  const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
1912
- const pauseToolData = isRecord3(data.pause_tool_data) ? data.pause_tool_data : {};
2418
+ const pauseToolData = isRecord(data.pause_tool_data) ? data.pause_tool_data : {};
1913
2419
  const description = String(sourceLoop.description ?? "");
1914
2420
  applyAskUserPauseToTurn(
1915
2421
  latestAssistantByLoop.get(childLoopId),
@@ -1997,15 +2503,15 @@ function parseToolArguments(value) {
1997
2503
  if (!value) return {};
1998
2504
  try {
1999
2505
  const parsed = JSON.parse(value);
2000
- return isRecord3(parsed) ? parsed : {};
2506
+ return isRecord(parsed) ? parsed : {};
2001
2507
  } catch {
2002
2508
  return {};
2003
2509
  }
2004
2510
  }
2005
2511
  function buildToolCalls(value) {
2006
2512
  if (!Array.isArray(value)) return [];
2007
- return value.filter(isRecord3).map((raw) => {
2008
- const fn = isRecord3(raw.function) ? raw.function : {};
2513
+ return value.filter(isRecord).map((raw) => {
2514
+ const fn = isRecord(raw.function) ? raw.function : {};
2009
2515
  const name = String(fn.name ?? "");
2010
2516
  return {
2011
2517
  id: String(raw.id ?? ""),
@@ -2021,23 +2527,40 @@ function buildBlocks(message, toolCalls) {
2021
2527
  const displayContent = message.role === "user" ? message._display_content : void 0;
2022
2528
  const stored = message._blocks;
2023
2529
  const blocks = [];
2530
+ const storedToolCallIds = /* @__PURE__ */ new Set();
2024
2531
  if (displayContent !== void 0) {
2025
2532
  blocks.push({ type: "text", content: displayContent });
2026
2533
  } else if (Array.isArray(stored)) {
2027
2534
  for (const raw of stored) {
2028
- if (!isRecord3(raw)) continue;
2535
+ if (!isRecord(raw)) continue;
2029
2536
  const type3 = String(raw.type ?? "");
2030
2537
  if (type3 === "thinking") blocks.push({ type: type3, content: raw.thinking ?? raw.content ?? "" });
2031
2538
  if (type3 === "text") blocks.push({ type: type3, content: raw.text ?? raw.content ?? "" });
2539
+ if (type3 === "tool_call") {
2540
+ const toolCallId = String(raw.id ?? "");
2541
+ const toolCall = toolCalls.find((candidate) => candidate.id === toolCallId);
2542
+ if (!toolCall) continue;
2543
+ storedToolCallIds.add(toolCallId);
2544
+ blocks.push({
2545
+ type: "tool_use",
2546
+ content: toolCall.arguments,
2547
+ tool_call_id: toolCall.id,
2548
+ tool_name: toolCall.tool_name,
2549
+ display_name: toolCall.display_name
2550
+ });
2551
+ }
2032
2552
  }
2033
2553
  } else if (message.content !== void 0 && message.content !== "") {
2034
2554
  blocks.push({ type: "text", content: message.content });
2035
2555
  }
2036
2556
  for (const toolCall of toolCalls) {
2557
+ if (storedToolCallIds.has(toolCall.id)) continue;
2037
2558
  blocks.push({
2038
2559
  type: "tool_use",
2039
- content: { name: toolCall.tool_name, arguments: toolCall.arguments },
2040
- tool_call_id: toolCall.id
2560
+ content: toolCall.arguments,
2561
+ tool_call_id: toolCall.id,
2562
+ tool_name: toolCall.tool_name,
2563
+ display_name: toolCall.display_name
2041
2564
  });
2042
2565
  }
2043
2566
  return blocks;
@@ -2058,14 +2581,11 @@ function markerTurn(id, sequence, loopId, type3, content, toolCallId = null) {
2058
2581
  duration_ms: 0
2059
2582
  };
2060
2583
  }
2061
- function isRecord3(value) {
2062
- return typeof value === "object" && value !== null && !Array.isArray(value);
2063
- }
2064
2584
  function stringOrNull(value) {
2065
2585
  return typeof value === "string" ? value : null;
2066
2586
  }
2067
- function numberOrZero(value) {
2068
- return typeof value === "number" ? value : 0;
2587
+ function numberOrNull(value) {
2588
+ return typeof value === "number" && value >= 0 ? value : null;
2069
2589
  }
2070
2590
  function findLatestAssistant(turns, loopId) {
2071
2591
  for (let index = turns.length - 1; index >= 0; index -= 1) {
@@ -2076,6 +2596,10 @@ function findLatestAssistant(turns, loopId) {
2076
2596
  }
2077
2597
 
2078
2598
  // src/resources/sessions.ts
2599
+ function isEntryGoneError(error) {
2600
+ const status = error?.status;
2601
+ return status === 404 || status === 409;
2602
+ }
2079
2603
  function toCreateSessionPayload(request) {
2080
2604
  const solutionRef = request.solution_ref ?? externalSolutionRef(request.solution_id);
2081
2605
  return {
@@ -2094,13 +2618,14 @@ function toCreateSessionPayload(request) {
2094
2618
  solution_snapshot_session_id: request.solution_snapshot_session_id ?? null,
2095
2619
  compaction_ratio: request.compaction_ratio ?? null,
2096
2620
  ...request.runtime_type ? { runtime_type: request.runtime_type } : {},
2097
- ...request.daemon_id ? { daemon_id: request.daemon_id } : {}
2621
+ ...request.daemon_id ? { daemon_id: request.daemon_id } : {},
2622
+ ...request.agent_runtime_id ? { agent_runtime_id: request.agent_runtime_id } : {},
2623
+ ...request.chat_group_id ? { chat_group_id: request.chat_group_id } : {}
2098
2624
  };
2099
2625
  }
2100
2626
  var builtinSolutionIds = /* @__PURE__ */ new Set([
2101
2627
  "app-dev",
2102
2628
  "general_chat",
2103
- "multica",
2104
2629
  "night_build",
2105
2630
  "skill_editor",
2106
2631
  "smart_bid",
@@ -2123,6 +2648,36 @@ var SessionsResource = class {
2123
2648
  this.client = client;
2124
2649
  }
2125
2650
  client;
2651
+ getMcpApp(sessionId, sourceId, init) {
2652
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}`, init);
2653
+ }
2654
+ onMcpAppChanged(sessionId, sourceId, listener) {
2655
+ let socket = this.client.socket();
2656
+ const changed = (payload) => {
2657
+ if (payload.session_id === sessionId && payload.source_id === sourceId) listener();
2658
+ };
2659
+ socket.on("mcp-app:changed", changed);
2660
+ socket.on("connect", listener);
2661
+ const off = this.client.onSocketReplaced(() => {
2662
+ socket.off("mcp-app:changed", changed);
2663
+ socket.off("connect", listener);
2664
+ socket = this.client.socket();
2665
+ socket.on("mcp-app:changed", changed);
2666
+ socket.on("connect", listener);
2667
+ });
2668
+ return () => {
2669
+ off();
2670
+ socket.off("mcp-app:changed", changed);
2671
+ socket.off("connect", listener);
2672
+ };
2673
+ }
2674
+ callMcpApp(sessionId, sourceId, request, init) {
2675
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/calls`, {
2676
+ ...init,
2677
+ method: "POST",
2678
+ body: JSON.stringify(request)
2679
+ });
2680
+ }
2126
2681
  /**
2127
2682
  * 连接一个已存在的会话,返回实时会话对象 AgentSession
2128
2683
  * (自动完成历史加载、Socket.IO 订阅、断线重连补数)。
@@ -2130,6 +2685,56 @@ var SessionsResource = class {
2130
2685
  connect(sessionId) {
2131
2686
  return this.client.hub.connect(sessionId);
2132
2687
  }
2688
+ listSessionPlugins(sessionId, init) {
2689
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins`, init);
2690
+ }
2691
+ setSessionPluginActivation(sessionId, name, active, init) {
2692
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}:activation`, { method: "POST", body: JSON.stringify({ active }), ...init });
2693
+ }
2694
+ /**
2695
+ * Fetch a plugin's package-local icon as bytes.
2696
+ *
2697
+ * The request carries the client's own bearer credentials, so callers turn
2698
+ * the blob into an object URL instead of pointing `<img>` at an API path and
2699
+ * hoping the request is authenticated.
2700
+ */
2701
+ fetchSessionPluginIcon(sessionId, name) {
2702
+ return this.client.blob("GET", `/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/icon`);
2703
+ }
2704
+ /** Authenticated URL for the icon; useful where a blob URL cannot be used. */
2705
+ sessionPluginIconUrl(sessionId, name) {
2706
+ return this.client.buildAuthedUrl(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/icon`);
2707
+ }
2708
+ /**
2709
+ * 准备一个插件的配置:把包准备好并读出用户的配置契约,**不启用**它。
2710
+ *
2711
+ * 开启插件前必须先走这一步:缺配置时用户只会看到配置表单,服务端不会先把
2712
+ * active 持久化成 true 再靠补偿关掉。它不修改会话的插件选择、不连接 MCP。
2713
+ */
2714
+ prepareSessionPluginConfig(sessionId, name, init) {
2715
+ return this.client.jsonFromInit(
2716
+ `/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}:prepare-config`,
2717
+ { method: "POST", ...init }
2718
+ );
2719
+ }
2720
+ /**
2721
+ * Read a plugin's configuration contract and the user's stored values.
2722
+ *
2723
+ * Secrets are never returned: `write_only` lists the pointers that hold one.
2724
+ */
2725
+ getSessionPluginConfig(sessionId, name, init) {
2726
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/config`, init);
2727
+ }
2728
+ /**
2729
+ * Publish explicit set/remove operations to the user's keychain file.
2730
+ *
2731
+ * Fields absent from both lists keep their stored value, so a form never has
2732
+ * to round-trip a secret. Rejections leave the previous file untouched; the
2733
+ * call never restarts MCP servers.
2734
+ */
2735
+ updateSessionPluginConfig(sessionId, name, update, init) {
2736
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/config`, { method: "PUT", body: JSON.stringify(update), ...init });
2737
+ }
2133
2738
  /** 创建新会话并直接连接,返回可收发消息的 AgentSession。 */
2134
2739
  async create(request = {}) {
2135
2740
  if (isSessionDefinition(request)) {
@@ -2226,7 +2831,8 @@ var SessionsResource = class {
2226
2831
  template_id_prefix,
2227
2832
  solution_id,
2228
2833
  solution_registry_id,
2229
- q
2834
+ q,
2835
+ chat_group_id
2230
2836
  }) {
2231
2837
  const searchParams = new URLSearchParams();
2232
2838
  searchParams.set("limit", String(limit));
@@ -2239,6 +2845,7 @@ var SessionsResource = class {
2239
2845
  }
2240
2846
  const trimmedQuery = q?.trim();
2241
2847
  if (trimmedQuery) searchParams.set("q", trimmedQuery);
2848
+ if (chat_group_id) searchParams.set("chat_group_id", chat_group_id);
2242
2849
  return this.client.json("GET", `/api/sessions?${searchParams.toString()}`);
2243
2850
  }
2244
2851
  async listSessions(template_id_prefix, solution_id) {
@@ -2329,6 +2936,55 @@ var SessionsResource = class {
2329
2936
  );
2330
2937
  return projectHistory(entries);
2331
2938
  }
2939
+ async getSessionTurnsPage(sessionId, options = {}, init = {}) {
2940
+ const query = new URLSearchParams();
2941
+ if (options.limit !== void 0) query.set("limit", String(options.limit));
2942
+ if (options.before !== void 0) query.set("before", options.before);
2943
+ if (options.fromEntryId !== void 0) query.set("from_entry_id", options.fromEntryId);
2944
+ const suffix = query.size ? `?${query}` : "";
2945
+ const page = await this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/turns${suffix}`, init);
2946
+ return {
2947
+ turns: page.turns,
2948
+ nextBefore: page.next_before,
2949
+ currentMode: page.current_mode
2950
+ };
2951
+ }
2952
+ /**
2953
+ * 当前分支上可导航的用户发言目录(消息导航侧栏)。
2954
+ *
2955
+ * 只回 id、标题和时间:侧栏要列的是**整条会话**的发言,而窗口外的发言 id 只能
2956
+ * 从服务端拿;消息体仍然走 `/turns`。
2957
+ */
2958
+ async getSessionTurnIndex(sessionId) {
2959
+ const index = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turn-index`);
2960
+ return {
2961
+ sessionId: index.session_id,
2962
+ items: index.items.map((item) => ({
2963
+ entryId: item.entry_id,
2964
+ preview: item.preview,
2965
+ createdAt: item.created_at
2966
+ }))
2967
+ };
2968
+ }
2969
+ getMcpAppContext(sessionId, sourceId, init) {
2970
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/context`, init);
2971
+ }
2972
+ updateMcpAppContext(sessionId, sourceId, body, init) {
2973
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/context`, {
2974
+ ...init,
2975
+ method: "POST",
2976
+ body: JSON.stringify(body)
2977
+ });
2978
+ }
2979
+ async getSessionTurnCommands(sessionId) {
2980
+ const commands = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turn-commands`);
2981
+ return commands.map((command) => ({
2982
+ deliveryId: command.delivery_id,
2983
+ toolCallId: command.tool_call_id ?? void 0,
2984
+ action: command.action,
2985
+ payload: command.payload
2986
+ }));
2987
+ }
2332
2988
  listResultFeedback(sessionId) {
2333
2989
  return this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/feedback`);
2334
2990
  }
@@ -2592,6 +3248,129 @@ function profileToCreateSessionRequest(profile) {
2592
3248
  };
2593
3249
  }
2594
3250
 
3251
+ // src/resources/plugins.ts
3252
+ var PluginsResource = class {
3253
+ constructor(client) {
3254
+ this.client = client;
3255
+ }
3256
+ client;
3257
+ /**
3258
+ * 列出当前用户安装的插件及其展示信息。
3259
+ *
3260
+ * 服务端可能为了补齐名称/图标惰性获取包内容,但绝不激活插件、不连 MCP;
3261
+ * 单个插件的展示信息读不出来只会降级成占位,不会让整份目录失败。
3262
+ */
3263
+ catalog(init) {
3264
+ return this.client.jsonFromInit("/api/plugins/catalog", init);
3265
+ }
3266
+ /** 取一个插件的包内图标字节(用户级,不需要会话)。 */
3267
+ fetchIcon(name) {
3268
+ return this.client.blob("GET", `/api/plugins/${encodeURIComponent(name)}/icon`);
3269
+ }
3270
+ /** 带凭据的图标地址;能直接用 blob URL 的地方优先用 `fetchIcon`。 */
3271
+ iconUrl(name) {
3272
+ return this.client.buildAuthedUrl(`/api/plugins/${encodeURIComponent(name)}/icon`);
3273
+ }
3274
+ };
3275
+
3276
+ // src/resources/chat-groups.ts
3277
+ var ChatGroupsResource = class {
3278
+ constructor(client) {
3279
+ this.client = client;
3280
+ }
3281
+ client;
3282
+ list() {
3283
+ return this.client.json("GET", "/api/chat-groups");
3284
+ }
3285
+ create(payload) {
3286
+ return this.client.json("POST", "/api/chat-groups", payload);
3287
+ }
3288
+ update(groupId, payload) {
3289
+ return this.client.json("PATCH", `/api/chat-groups/${encodeURIComponent(groupId)}`, payload);
3290
+ }
3291
+ delete(groupId) {
3292
+ return this.client.json("DELETE", `/api/chat-groups/${encodeURIComponent(groupId)}`);
3293
+ }
3294
+ };
3295
+
3296
+ // src/resources/site-visits.ts
3297
+ var SITE_VISITS_PATH = "/api/site-visits";
3298
+ var REQUEST_TIMEOUT_MS = 5e3;
3299
+ var SiteVisitsResource = class {
3300
+ constructor(client) {
3301
+ this.client = client;
3302
+ }
3303
+ client;
3304
+ /** 上报一次页面访问,返回当前快照;不承诺本次已计入或已落盘。 */
3305
+ record() {
3306
+ return this.request("POST");
3307
+ }
3308
+ /** 只读快照:不设置访客 Cookie,也不增加任何计数。 */
3309
+ summary() {
3310
+ return this.request("GET");
3311
+ }
3312
+ async request(method) {
3313
+ const url = buildBackendUrl(this.client.options.baseUrl, SITE_VISITS_PATH);
3314
+ const controller = new AbortController();
3315
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
3316
+ try {
3317
+ const response = await (this.client.options.fetchImpl ?? fetch)(url.toString(), {
3318
+ method,
3319
+ // 访客 Cookie 是唯一的去重依据,必须随请求往返。
3320
+ credentials: "include",
3321
+ signal: controller.signal
3322
+ });
3323
+ if (!response.ok) {
3324
+ throw new BladeApiError(response, await extractErrorDetail(response));
3325
+ }
3326
+ return await response.json();
3327
+ } finally {
3328
+ clearTimeout(timeout);
3329
+ }
3330
+ }
3331
+ };
3332
+
3333
+ // src/shared/auth-busy.ts
3334
+ var DEFAULT_RETRY_AFTER_SECONDS = 1;
3335
+ var MAX_TIMER_DELAY_MS = 2147483647;
3336
+ function errorData(error) {
3337
+ if (typeof error !== "object" || error === null || !("data" in error)) return null;
3338
+ const data = error.data;
3339
+ return typeof data === "object" && data !== null ? data : null;
3340
+ }
3341
+ function authBusyRetryDelayMs(error) {
3342
+ const data = errorData(error);
3343
+ if (data?.code !== "auth_busy" || Number(data.status) !== 429) return null;
3344
+ const rawRetryAfter = data.retry_after;
3345
+ const seconds = rawRetryAfter == null || String(rawRetryAfter).trim() === "" ? Number.NaN : Number(rawRetryAfter);
3346
+ const delaySeconds = Number.isFinite(seconds) && seconds >= 0 ? seconds : DEFAULT_RETRY_AFTER_SECONDS;
3347
+ return Math.min(delaySeconds * 1e3, MAX_TIMER_DELAY_MS);
3348
+ }
3349
+ var AuthBusyReconnect = class {
3350
+ timer = null;
3351
+ get pending() {
3352
+ return this.timer !== null;
3353
+ }
3354
+ schedule(error, reconnect) {
3355
+ const delay = authBusyRetryDelayMs(error);
3356
+ if (delay === null) {
3357
+ this.cancel();
3358
+ return false;
3359
+ }
3360
+ this.cancel();
3361
+ this.timer = globalThis.setTimeout(() => {
3362
+ this.timer = null;
3363
+ reconnect();
3364
+ }, delay);
3365
+ return true;
3366
+ }
3367
+ cancel() {
3368
+ if (this.timer === null) return;
3369
+ globalThis.clearTimeout(this.timer);
3370
+ this.timer = null;
3371
+ }
3372
+ };
3373
+
2595
3374
  // src/commands/registry.ts
2596
3375
  var BUFFER_LIMIT_PER_ACTION = 20;
2597
3376
  var CommandRegistry = class {
@@ -2700,16 +3479,284 @@ var Emitter = class {
2700
3479
  }
2701
3480
  };
2702
3481
 
2703
- // src/schemas/message-utils.ts
2704
- var SYSTEM_REMINDER_PATTERN = /^<system-reminder>\s*[\s\S]*?\s*<\/system-reminder>$/i;
2705
- var SYSTEM_NOTIFICATION_PATTERN = /^<system-notification>\s*[\s\S]*?\s*<\/system-notification>$/i;
2706
- var LEGACY_FORK_CONTEXT_MARKERS = [
2707
- "\u7236\u667A\u80FD\u4F53\u5728\u6D3E\u751F\u4F60\u4E4B\u524D\u5DF2\u7ECF\u5B8C\u6210\u4E86\u4E00\u4E9B\u64CD\u4F5C\u3002",
2708
- "\u53EF\u901A\u8FC7 `read_file` \u8BFB\u53D6\u3002",
2709
- "# \u4F60\u7684\u4EFB\u52A1"
2710
- ];
2711
- function normalizeMessageContent(content) {
2712
- if (typeof content === "string") return content;
3482
+ // src/session/history-page.ts
3483
+ function sortedUnique(turns) {
3484
+ const byId = /* @__PURE__ */ new Map();
3485
+ for (const turn of turns) byId.set(turn.turn_id, turn);
3486
+ return [...byId.values()].sort((left, right) => left.sequence - right.sequence);
3487
+ }
3488
+ function prependOlder(current, page) {
3489
+ const currentById = new Map(current.map((turn) => [turn.turn_id, turn]));
3490
+ return sortedUnique([
3491
+ ...current,
3492
+ ...page.map((turn) => {
3493
+ const live = currentById.get(turn.turn_id);
3494
+ return live ? { ...live, sequence: turn.sequence } : turn;
3495
+ })
3496
+ ]);
3497
+ }
3498
+ function replaceWindow(current, page, liveRevisionAtStart, liveRevisions) {
3499
+ const pageById = new Map(page.map((turn) => [turn.turn_id, turn]));
3500
+ const newerLive = current.filter(
3501
+ (turn) => turn.turn_id.startsWith("local-user-") || (liveRevisions.byTurnId.get(turn.turn_id) ?? 0) > liveRevisionAtStart
3502
+ );
3503
+ return sortedUnique([
3504
+ ...page,
3505
+ ...newerLive.map((turn) => {
3506
+ const snapshot2 = pageById.get(turn.turn_id);
3507
+ return snapshot2 ? { ...turn, sequence: snapshot2.sequence } : turn;
3508
+ })
3509
+ ]);
3510
+ }
3511
+
3512
+ // src/session/queue.ts
3513
+ var EMPTY_SESSION_QUEUE = {
3514
+ session_id: "",
3515
+ revision: 0,
3516
+ paused: false,
3517
+ pause_reason: null,
3518
+ items: []
3519
+ };
3520
+ var QUEUE_FAILURE_MESSAGE = "\u961F\u5217\u64CD\u4F5C\u5931\u8D25";
3521
+ var QUEUED_MESSAGE_STATUSES = [
3522
+ "pending",
3523
+ "delivering",
3524
+ "claimed",
3525
+ "consumed",
3526
+ "cancelled"
3527
+ ];
3528
+ function toQueuedMessage(value) {
3529
+ if (!isRecord(value)) return null;
3530
+ const status = typeof value.status === "string" && QUEUED_MESSAGE_STATUSES.includes(value.status) ? value.status : null;
3531
+ if (typeof value.id !== "string" || typeof value.content !== "string" || typeof value.position !== "number" || typeof value.version !== "number" || typeof value.created_at !== "string" || typeof value.updated_at !== "string" || status === null) {
3532
+ return null;
3533
+ }
3534
+ const item = {
3535
+ id: value.id,
3536
+ content: value.content,
3537
+ position: value.position,
3538
+ version: value.version,
3539
+ status,
3540
+ created_at: value.created_at,
3541
+ updated_at: value.updated_at
3542
+ };
3543
+ if (typeof value.target_run_id === "string") item.target_run_id = value.target_run_id;
3544
+ if (typeof value.error === "string") item.error = value.error;
3545
+ return item;
3546
+ }
3547
+ function toQueueSnapshot(value, fallbackSessionId) {
3548
+ if (!isRecord(value)) return null;
3549
+ if (typeof value.revision !== "number" || !Number.isInteger(value.revision) || value.revision < 0) {
3550
+ return null;
3551
+ }
3552
+ if (typeof value.paused !== "boolean") return null;
3553
+ if (!Array.isArray(value.items)) return null;
3554
+ const pauseReason = value.pause_reason;
3555
+ if (pauseReason !== null && pauseReason !== void 0 && typeof pauseReason !== "string") {
3556
+ return null;
3557
+ }
3558
+ const items = [];
3559
+ for (const raw of value.items) {
3560
+ const item = toQueuedMessage(raw);
3561
+ if (!item) return null;
3562
+ items.push(item);
3563
+ }
3564
+ return {
3565
+ session_id: typeof value.session_id === "string" && value.session_id ? value.session_id : fallbackSessionId,
3566
+ revision: value.revision,
3567
+ paused: value.paused,
3568
+ pause_reason: typeof pauseReason === "string" ? pauseReason : null,
3569
+ items
3570
+ };
3571
+ }
3572
+ function isSessionQueueSnapshot(value) {
3573
+ if (!isRecord(value) || typeof value.session_id !== "string") return false;
3574
+ return toQueueSnapshot(value, value.session_id) !== null;
3575
+ }
3576
+ function shouldApplySnapshot(current, next) {
3577
+ if (!isSessionQueueSnapshot(next)) return false;
3578
+ if (current?.session_id && next.session_id !== current.session_id) return false;
3579
+ return next.revision > (current?.revision ?? -1);
3580
+ }
3581
+ function parseQueueAck(response, fallbackSessionId) {
3582
+ if (!isRecord(response)) {
3583
+ return { ok: false, conflict: false, snapshot: null, message: QUEUE_FAILURE_MESSAGE };
3584
+ }
3585
+ const message = typeof response.message === "string" && response.message ? response.message : null;
3586
+ const status = response.status;
3587
+ if (status !== "ok" && status !== "conflict" && status !== "queued") {
3588
+ return { ok: false, conflict: false, snapshot: null, message: message ?? QUEUE_FAILURE_MESSAGE };
3589
+ }
3590
+ const snapshot2 = toQueueSnapshot(response, fallbackSessionId);
3591
+ if (status === "conflict") {
3592
+ return { ok: false, conflict: true, snapshot: snapshot2, message: message ?? QUEUE_FAILURE_MESSAGE };
3593
+ }
3594
+ if (!snapshot2) {
3595
+ return { ok: false, conflict: false, snapshot: null, message: message ?? QUEUE_FAILURE_MESSAGE };
3596
+ }
3597
+ return { ok: true, conflict: false, snapshot: snapshot2, message };
3598
+ }
3599
+ function canEditQueuedMessage(item) {
3600
+ return item.status === "pending";
3601
+ }
3602
+ function canCancelQueuedMessage(item) {
3603
+ return item.status === "pending";
3604
+ }
3605
+ function canDeliverQueuedMessage(item) {
3606
+ return item.status === "pending";
3607
+ }
3608
+ var QUEUED_MESSAGE_STATUS_LABELS = {
3609
+ pending: "\u5F85\u6267\u884C",
3610
+ delivering: "\u7B49\u5F85\u63A5\u6536",
3611
+ claimed: "",
3612
+ consumed: "",
3613
+ cancelled: ""
3614
+ };
3615
+ function queuedMessageStatusLabel(status) {
3616
+ return QUEUED_MESSAGE_STATUS_LABELS[status] ?? "";
3617
+ }
3618
+ var QUEUE_PAUSE_REASON_LABELS = {
3619
+ user_stop: "\u5DF2\u6682\u505C",
3620
+ run_failed: "\u4EFB\u52A1\u5F02\u5E38\u5DF2\u6682\u505C",
3621
+ run_unknown: "\u4EFB\u52A1\u7ED3\u675F\u72B6\u6001\u672A\u77E5\uFF0C\u5DF2\u6682\u505C",
3622
+ server_restart: "\u670D\u52A1\u91CD\u542F\u5DF2\u6682\u505C"
3623
+ };
3624
+ function queuePauseReasonLabel(reason) {
3625
+ if (!reason) return null;
3626
+ return QUEUE_PAUSE_REASON_LABELS[reason] ?? reason;
3627
+ }
3628
+
3629
+ // src/schemas/mcp-app.ts
3630
+ function isMcpAppData(value) {
3631
+ return isRecord(value) && typeof value.sourceId === "string" && value.sourceId.length > 0 && typeof value.resourceUri === "string" && value.resourceUri.startsWith("ui://") && typeof value.mimeType === "string" && isRecord(value.resourceMeta) && isRecord(value.toolInput) && isRecord(value.toolResult);
3632
+ }
3633
+
3634
+ // src/shared/tool-ui-card.ts
3635
+ function isNonEmptyString(value) {
3636
+ return typeof value === "string" && value.trim().length > 0;
3637
+ }
3638
+ function isPositiveNumber(value) {
3639
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
3640
+ }
3641
+ function isToolUiCard(value) {
3642
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3643
+ return false;
3644
+ }
3645
+ const raw = value;
3646
+ if (raw.target !== "inline" && raw.target !== "preview") {
3647
+ return false;
3648
+ }
3649
+ if (!isPositiveNumber(raw.height)) {
3650
+ return false;
3651
+ }
3652
+ if (raw.archived === true && !isNonEmptyString(raw.resourceHTML)) {
3653
+ return false;
3654
+ }
3655
+ if (!isNonEmptyString(raw.resourceHTML) && !isNonEmptyString(raw.resourceUri) && !isNonEmptyString(raw.resourceURI)) {
3656
+ return false;
3657
+ }
3658
+ if (raw.title != null && !isNonEmptyString(raw.title)) {
3659
+ return false;
3660
+ }
3661
+ if (raw.mcpApp !== void 0 && !isMcpAppData(raw.mcpApp)) {
3662
+ return false;
3663
+ }
3664
+ return true;
3665
+ }
3666
+ function resolveToolUiCardContent(card) {
3667
+ if (isNonEmptyString(card.resourceHTML)) {
3668
+ return { type: "resource-html", content: card.resourceHTML };
3669
+ }
3670
+ if (card.archived) return null;
3671
+ const uri = isNonEmptyString(card.resourceUri) ? card.resourceUri : card.resourceURI;
3672
+ return isNonEmptyString(uri) ? { type: "resource-uri", content: uri } : null;
3673
+ }
3674
+ function buildToolUiCardKey(toolCallId, type3, content) {
3675
+ if (type3 === "resource-uri") {
3676
+ return `tool-preview-uri:${content}`;
3677
+ }
3678
+ return `tool-preview:${toolCallId}`;
3679
+ }
3680
+ function isInternalStatusToolUiCard(card) {
3681
+ return card?.title?.trim() === "\u9636\u6BB5\u8FDB\u5EA6";
3682
+ }
3683
+ function isAppDevToolUiCard(card, isAppDev) {
3684
+ return isAppDev && card.target === "preview" && !card.resourceHTML && Boolean(card.resourceUri || card.resourceURI);
3685
+ }
3686
+ function collectInlineToolUiCards(messages) {
3687
+ return messages.flatMap(
3688
+ (message) => (message.blocks ?? []).flatMap(
3689
+ (block, index) => block.type === "tool_ui" && block.tool_call_id && isToolUiCard(block.content) && block.content.target === "inline" ? [
3690
+ {
3691
+ key: `${message.entry_id ?? message.timestamp ?? "message"}-${block.tool_call_id}-${index}`,
3692
+ toolCallId: block.tool_call_id,
3693
+ card: block.content
3694
+ }
3695
+ ] : []
3696
+ )
3697
+ );
3698
+ }
3699
+ function collectPreviewToolUiCards(messages) {
3700
+ return messages.flatMap(
3701
+ (message) => (message.blocks ?? []).flatMap((block, index) => {
3702
+ if (block.type !== "tool_ui" || !block.tool_call_id || !isToolUiCard(block.content) || block.content.target !== "preview") {
3703
+ return [];
3704
+ }
3705
+ const toolCall = (message.tool_calls ?? []).find(
3706
+ (candidate) => candidate.id === block.tool_call_id
3707
+ );
3708
+ return toolCall ? [
3709
+ {
3710
+ key: `${message.entry_id ?? message.timestamp ?? "message"}-${block.tool_call_id}-${index}`,
3711
+ toolCall,
3712
+ card: block.content,
3713
+ blocks: message.blocks ?? []
3714
+ }
3715
+ ] : [];
3716
+ })
3717
+ );
3718
+ }
3719
+
3720
+ // src/schemas/message-utils.ts
3721
+ var SYSTEM_REMINDER_PATTERN = /^<system-reminder>\s*[\s\S]*?\s*<\/system-reminder>$/i;
3722
+ var SYSTEM_NOTIFICATION_PATTERN = /^<system-notification>\s*[\s\S]*?\s*<\/system-notification>$/i;
3723
+ var LEGACY_FORK_CONTEXT_MARKERS = [
3724
+ "\u7236\u667A\u80FD\u4F53\u5728\u6D3E\u751F\u4F60\u4E4B\u524D\u5DF2\u7ECF\u5B8C\u6210\u4E86\u4E00\u4E9B\u64CD\u4F5C\u3002",
3725
+ "\u53EF\u901A\u8FC7 `read_file` \u8BFB\u53D6\u3002",
3726
+ "# \u4F60\u7684\u4EFB\u52A1"
3727
+ ];
3728
+ var INTERNAL_CHAT_ERROR_PATTERNS = [
3729
+ /\b(?:ENOENT|EACCES|EPERM)\b/i,
3730
+ /\[(?:Errno -?\d+|WinError \d+)\]/i,
3731
+ /\b(?:ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|ENOTFOUND|EAI_AGAIN)\b/i,
3732
+ /\b(?:Connection refused|Connection reset by peer|All connection attempts failed|getaddrinfo failed)\b/i,
3733
+ /(?:^|[\s("'`])\/(?:root|home|Users|tmp|var|opt|etc)\//,
3734
+ /[A-Za-z]:\\(?:Users|Windows|Program Files|AppData)\\/i,
3735
+ /(?:^|\n)Traceback \(most recent call last\):/,
3736
+ /(?:^|\n)\s*at\s+\S+(?:\s+\(|:\d+:\d+)/,
3737
+ /(?:^|\n)File "[^"]+", line \d+/
3738
+ ];
3739
+ var SAFE_CHAT_ERROR_PREFIXES = [
3740
+ "\u6C99\u76D2\u8FD0\u884C\u73AF\u5883\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
3741
+ "\u6A21\u578B\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u68C0\u67E5\u6A21\u578B\u914D\u7F6E\u540E\u91CD\u8BD5\u3002"
3742
+ ];
3743
+ var GENERIC_CHAT_ERROR = "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002";
3744
+ function chatErrorForDisplay(message, developer = false) {
3745
+ const normalized = message.trim();
3746
+ if (!developer) {
3747
+ const safePrefix = SAFE_CHAT_ERROR_PREFIXES.find((prefix) => normalized.startsWith(prefix));
3748
+ if (safePrefix) return safePrefix;
3749
+ }
3750
+ if (!developer && /no source matches this model/i.test(normalized)) {
3751
+ return "\u5F53\u524D\u6A21\u578B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u6216\u91CD\u65B0\u9009\u62E9\u6A21\u578B\u3002";
3752
+ }
3753
+ if (!developer && INTERNAL_CHAT_ERROR_PATTERNS.some((pattern) => pattern.test(normalized))) {
3754
+ return GENERIC_CHAT_ERROR;
3755
+ }
3756
+ return normalized || GENERIC_CHAT_ERROR;
3757
+ }
3758
+ function normalizeMessageContent(content) {
3759
+ if (typeof content === "string") return content;
2713
3760
  if (Array.isArray(content)) return content;
2714
3761
  return String(content ?? "");
2715
3762
  }
@@ -2727,10 +3774,10 @@ function isLegacyForkContextText(text) {
2727
3774
  const normalized = text.trim();
2728
3775
  return LEGACY_FORK_CONTEXT_MARKERS.every((marker) => normalized.includes(marker));
2729
3776
  }
2730
- function isHiddenInternalMessage(message) {
3777
+ function isHiddenInternalMessage(message, options = {}) {
2731
3778
  if (message.role !== "user") return false;
2732
3779
  const text = getTextContent(normalizeMessageContent(message.content));
2733
- return isSystemReminderText(text) || isSystemNotificationText(text) || isLegacyForkContextText(text);
3780
+ return isSystemReminderText(text) || isSystemNotificationText(text) || (options.includeLegacyForkContext ?? true) && isLegacyForkContextText(text);
2734
3781
  }
2735
3782
  function buildMessageContent(text, attachments) {
2736
3783
  if (attachments.length === 0) return text;
@@ -2877,7 +3924,10 @@ function createInitialSessionState(sessionId) {
2877
3924
  agentLoops: {},
2878
3925
  activeCompaction: null,
2879
3926
  replay: null,
2880
- viewerRole: null
3927
+ queue: EMPTY_SESSION_QUEUE,
3928
+ viewerRole: null,
3929
+ nextBefore: null,
3930
+ loadingOlder: false
2881
3931
  };
2882
3932
  }
2883
3933
  function parseAgentDescription(argumentsJson) {
@@ -2903,10 +3953,10 @@ function inferLoopStatusFromMessages(messages) {
2903
3953
  }
2904
3954
  function inferLoopStatusFromTurns(turns, messages) {
2905
3955
  const latestAgentNotification = [...turns].reverse().flatMap((turn) => turn.blocks).find((block) => {
2906
- if (block.type !== "system_notification" || !isRecord4(block.content)) return false;
3956
+ if (block.type !== "system_notification" || !isRecord(block.content)) return false;
2907
3957
  return block.content.notification_type === "agent:start" || block.content.notification_type === "agent:end";
2908
3958
  });
2909
- if (latestAgentNotification?.type === "system_notification" && isRecord4(latestAgentNotification.content)) {
3959
+ if (latestAgentNotification?.type === "system_notification" && isRecord(latestAgentNotification.content)) {
2910
3960
  const notificationType = latestAgentNotification.content.notification_type;
2911
3961
  const status = latestAgentNotification.content.status;
2912
3962
  if (notificationType === "agent:start" || status === "running") return "running";
@@ -2915,17 +3965,44 @@ function inferLoopStatusFromTurns(turns, messages) {
2915
3965
  }
2916
3966
  return inferLoopStatusFromMessages(messages);
2917
3967
  }
2918
- function isRecord4(value) {
2919
- return typeof value === "object" && value !== null && !Array.isArray(value);
3968
+ function toSelectionMap(value) {
3969
+ if (!isRecord(value)) return {};
3970
+ const entries = Object.entries(value).map(([questionKey, optionIndexes]) => {
3971
+ if (!Array.isArray(optionIndexes)) return null;
3972
+ const parsedIndexes = optionIndexes.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item));
3973
+ return [Number(questionKey), parsedIndexes];
3974
+ }).filter((entry) => entry !== null);
3975
+ return Object.fromEntries(entries);
3976
+ }
3977
+ function toCustomMap(value) {
3978
+ if (!isRecord(value)) return {};
3979
+ const entries = Object.entries(value).filter(([, text]) => typeof text === "string").map(([questionKey, text]) => [Number(questionKey), text]);
3980
+ return Object.fromEntries(entries);
3981
+ }
3982
+ function extractAskAnswers(turns) {
3983
+ const answers = {};
3984
+ for (const turn of turns) {
3985
+ for (const block of turn.blocks) {
3986
+ if (block.type !== "ask_user_answer" || typeof block.tool_call_id !== "string") continue;
3987
+ if (!isRecord(block.content)) continue;
3988
+ const note = typeof block.content.note === "string" ? block.content.note.trim() : "";
3989
+ answers[block.tool_call_id] = {
3990
+ selections: toSelectionMap(block.content.selections),
3991
+ custom: toCustomMap(block.content.custom),
3992
+ ...note ? { note } : {}
3993
+ };
3994
+ }
3995
+ }
3996
+ return answers;
2920
3997
  }
2921
3998
  function parentForkToolCallIdFromTurn(turn) {
2922
3999
  if (typeof turn.parent_fork_tool_call_id === "string" && turn.parent_fork_tool_call_id.length > 0) {
2923
4000
  return turn.parent_fork_tool_call_id;
2924
4001
  }
2925
4002
  for (const block of turn.blocks) {
2926
- if (block.type !== "system_notification" || !isRecord4(block.content)) continue;
4003
+ if (block.type !== "system_notification" || !isRecord(block.content)) continue;
2927
4004
  const metadata = block.content.metadata;
2928
- if (!isRecord4(metadata)) continue;
4005
+ if (!isRecord(metadata)) continue;
2929
4006
  const parentId = metadata.parent_fork_tool_call_id;
2930
4007
  if (typeof parentId === "string" && parentId.length > 0) return parentId;
2931
4008
  }
@@ -2942,16 +4019,16 @@ function buildMessageContent2(turn) {
2942
4019
  }
2943
4020
  function workspaceNotificationContent(turn) {
2944
4021
  const block = turn.blocks.find(
2945
- (candidate) => candidate.type === "system_notification" && isRecord4(candidate.content) && candidate.content.notification_type === "workspace_change"
4022
+ (candidate) => candidate.type === "system_notification" && isRecord(candidate.content) && candidate.content.notification_type === "workspace_change"
2946
4023
  );
2947
- if (!block || !isRecord4(block.content)) return "";
4024
+ if (!block || !isRecord(block.content)) return "";
2948
4025
  const title = typeof block.content.title === "string" ? block.content.title.trim() : "";
2949
4026
  const detail = typeof block.content.detail === "string" ? block.content.detail.trim() : "";
2950
4027
  return [title ? `**${title}**` : "", detail].filter(Boolean).join("\n\n");
2951
4028
  }
2952
4029
  function askUserAnswerContent(turn) {
2953
4030
  const answerBlock = turn.blocks.find((block) => block.type === "ask_user_answer");
2954
- if (!answerBlock || !isRecord4(answerBlock.content)) return null;
4031
+ if (!answerBlock || !isRecord(answerBlock.content)) return null;
2955
4032
  const answer = answerBlock.content.answer;
2956
4033
  return typeof answer === "string" && answer.trim().length > 0 ? answer : null;
2957
4034
  }
@@ -2971,6 +4048,19 @@ function extractModeFromBlocks(blocks) {
2971
4048
  return null;
2972
4049
  }
2973
4050
  function projectionToMessage(turn) {
4051
+ if (turn.kind === "context") {
4052
+ const context = contextProjectionData(turn);
4053
+ if (!context) return null;
4054
+ return {
4055
+ role: "assistant",
4056
+ content: "",
4057
+ kind: "context",
4058
+ context,
4059
+ loop_name: turn.loop_id,
4060
+ entry_id: turn.turn_id,
4061
+ status: turn.status
4062
+ };
4063
+ }
2974
4064
  if (turn.kind === "compaction" && turn.compaction_id) {
2975
4065
  return {
2976
4066
  role: "assistant",
@@ -3141,21 +4231,58 @@ function orderTurns(turns) {
3141
4231
  return turns;
3142
4232
  }
3143
4233
  function reconcileOptimisticUserTurns(currentTurns, incomingTurns) {
3144
- const currentById = new Map(currentTurns.map((turn) => [turn.turn_id, turn]));
3145
- const optimisticUserTurn = currentTurns.find(
3146
- (turn) => turn.role === "user" && turn.turn_id.startsWith("local-user-")
4234
+ return reconcileUserTurns(currentTurns, incomingTurns, true, false);
4235
+ }
4236
+ function reconcileHistoricalUserTurns(currentTurns, incomingTurns) {
4237
+ return reconcileUserTurns(currentTurns, incomingTurns, false, true);
4238
+ }
4239
+ function reconcileUserTurns(currentTurns, incomingTurns, allowLiveFallback, keepUnclaimedOptimistic) {
4240
+ const preservedTurns = preserveClientRenderIds(currentTurns, incomingTurns);
4241
+ const optimisticUserTurns = currentTurns.filter(
4242
+ (turn) => turn.role === "user" && (turn.kind ?? "message") === "message" && turn.turn_id.startsWith("local-user-")
4243
+ );
4244
+ const optimisticByRequestId = new Map(
4245
+ optimisticUserTurns.map((turn) => [
4246
+ turn.turn_id.slice("local-user-".length),
4247
+ turn
4248
+ ])
3147
4249
  );
3148
- let optimisticUserMatched = false;
4250
+ const currentTurnIds = new Set(currentTurns.map((turn) => turn.turn_id));
4251
+ const claimedOptimisticIds = /* @__PURE__ */ new Set();
4252
+ const reconciled = preservedTurns.map((turn) => {
4253
+ const exactOptimistic = typeof turn.client_request_id === "string" ? optimisticByRequestId.get(turn.client_request_id) : void 0;
4254
+ if (exactOptimistic && !claimedOptimisticIds.has(exactOptimistic.turn_id) && isUserConfirmation(turn)) {
4255
+ claimedOptimisticIds.add(exactOptimistic.turn_id);
4256
+ return { ...turn, client_render_id: exactOptimistic.turn_id };
4257
+ }
4258
+ const fallbackOptimistic = optimisticUserTurns.find(
4259
+ (candidate) => !claimedOptimisticIds.has(candidate.turn_id)
4260
+ );
4261
+ if (allowLiveFallback && typeof turn.client_request_id !== "string" && fallbackOptimistic && isUserConfirmation(turn) && !turn.turn_id.startsWith("local-user-") && !currentTurnIds.has(turn.turn_id)) {
4262
+ claimedOptimisticIds.add(fallbackOptimistic.turn_id);
4263
+ return { ...turn, client_render_id: fallbackOptimistic.turn_id };
4264
+ }
4265
+ return turn;
4266
+ });
4267
+ const result = claimedOptimisticIds.size === 0 ? reconciled : reconciled.filter((turn) => !claimedOptimisticIds.has(turn.turn_id));
4268
+ if (!keepUnclaimedOptimistic) return result;
4269
+ const resultIds = new Set(result.map((turn) => turn.turn_id));
4270
+ const unclaimedOptimisticTurns = optimisticUserTurns.filter(
4271
+ (turn) => !claimedOptimisticIds.has(turn.turn_id) && !resultIds.has(turn.turn_id)
4272
+ );
4273
+ return unclaimedOptimisticTurns.length === 0 ? result : orderTurns([...result, ...unclaimedOptimisticTurns]);
4274
+ }
4275
+ function isUserConfirmation(turn) {
4276
+ return turn.role === "user" && (turn.kind ?? "message") === "message" || turn.blocks.some((block) => block.type === "ask_user_answer");
4277
+ }
4278
+ function preserveClientRenderIds(currentTurns, incomingTurns) {
4279
+ const currentById = new Map(currentTurns.map((turn) => [turn.turn_id, turn]));
3149
4280
  return incomingTurns.map((turn) => {
3150
4281
  const current = currentById.get(turn.turn_id);
3151
4282
  if (current) {
3152
4283
  const currentRenderId = current.client_render_id;
3153
4284
  return turn.client_render_id || !currentRenderId ? turn : { ...turn, client_render_id: currentRenderId };
3154
4285
  }
3155
- if (!optimisticUserMatched && optimisticUserTurn && turn.role === "user" && !turn.turn_id.startsWith("local-user-")) {
3156
- optimisticUserMatched = true;
3157
- return { ...turn, client_render_id: optimisticUserTurn.turn_id };
3158
- }
3159
4286
  return turn;
3160
4287
  });
3161
4288
  }
@@ -3170,7 +4297,7 @@ function areAgentLoopsEqual(left, right) {
3170
4297
  });
3171
4298
  }
3172
4299
  function withTurns(state, turns) {
3173
- const orderedTurns = orderTurns(reconcileOptimisticUserTurns(state.turns, turns));
4300
+ const orderedTurns = orderTurns(reconcileHistoricalUserTurns(state.turns, turns));
3174
4301
  const { messages, agentLoops, activeCompaction } = materialize(orderedTurns);
3175
4302
  const isWaitingForInput = orderedTurns.some(
3176
4303
  (turn) => turn.tool_calls.some((toolCall) => toolCall.status === "awaiting_answer")
@@ -3188,26 +4315,45 @@ function withTurns(state, turns) {
3188
4315
  });
3189
4316
  const mergedMessages = preservedErrors.length > 0 ? [...messagesWithRenderIds, ...preservedErrors] : messagesWithRenderIds;
3190
4317
  const latestMode = [...orderedTurns].reverse().map((turn) => extractModeFromBlocks(turn.blocks)).find((mode) => mode !== null);
4318
+ const askAnswersFromHistory = extractAskAnswers(orderedTurns);
4319
+ const presentToolCallIds = new Set(
4320
+ orderedTurns.flatMap((turn) => turn.tool_calls.map((toolCall) => toolCall.id))
4321
+ );
4322
+ const askAnswers = { ...askAnswersFromHistory };
4323
+ for (const [toolCallId, answer] of Object.entries(state.askAnswers)) {
4324
+ if (!(toolCallId in askAnswers) && presentToolCallIds.has(toolCallId)) {
4325
+ askAnswers[toolCallId] = answer;
4326
+ }
4327
+ }
3191
4328
  return {
3192
4329
  ...state,
3193
4330
  turns: orderedTurns,
3194
4331
  messages: mergedMessages,
3195
4332
  agentLoops: stableAgentLoops,
3196
4333
  activeCompaction,
4334
+ askAnswers,
3197
4335
  mode: latestMode ?? state.mode,
3198
4336
  // 历史恢复时 chat:end(paused) 可能已不在 socket replay 缓存中;
3199
4337
  // awaiting_answer 是投影里的持久权威状态,必须据此恢复问答交互。
3200
4338
  status: isWaitingForInput ? "waiting_for_input" : state.status
3201
4339
  };
3202
4340
  }
4341
+ function withQueue(state, snapshot2) {
4342
+ if (!shouldApplySnapshot(state.queue, snapshot2)) return state;
4343
+ return { ...state, queue: snapshot2 };
4344
+ }
3203
4345
  var localTurnCounter = 0;
3204
- function addUserMessage(state, content) {
4346
+ function addUserMessage(state, content, clientRequestId) {
3205
4347
  localTurnCounter += 1;
3206
- const turnId = `local-user-${Date.now()}-${localTurnCounter}`;
4348
+ const turnId = `local-user-${clientRequestId ?? `${Date.now()}-${localTurnCounter}`}`;
3207
4349
  const optimisticTurn = {
3208
4350
  id: turnId,
3209
4351
  sequence: Math.max(0, ...state.turns.map((turn) => turn.sequence)) + 1,
3210
4352
  turn_id: turnId,
4353
+ // 乐观态就自赋,跟服务端确认后 reconcileUserTurns 补上的
4354
+ // client_render_id(= 乐观 turn 自己的 turn_id)保持同一个值,
4355
+ // 消费方才能拿它当"确认前后不变"的稳定标识使用。
4356
+ client_render_id: turnId,
3211
4357
  loop_id: "root",
3212
4358
  role: "user",
3213
4359
  status: "streaming",
@@ -3222,10 +4368,9 @@ function addUserMessage(state, content) {
3222
4368
  function upsertTurn(state, turn) {
3223
4369
  let existing = [...state.turns];
3224
4370
  const [nextTurn] = reconcileOptimisticUserTurns(existing, [turn]);
3225
- if (nextTurn.client_render_id && turn.role === "user") {
3226
- existing = existing.filter(
3227
- (item) => !(item.role === "user" && item.turn_id.startsWith("local-user-"))
3228
- );
4371
+ const claimedOptimisticId = nextTurn.client_render_id;
4372
+ if (claimedOptimisticId?.startsWith("local-user-")) {
4373
+ existing = existing.filter((item) => item.turn_id !== claimedOptimisticId);
3229
4374
  }
3230
4375
  const index = existing.findIndex((item) => item.turn_id === nextTurn.turn_id);
3231
4376
  if (index >= 0) {
@@ -3233,7 +4378,7 @@ function upsertTurn(state, turn) {
3233
4378
  } else {
3234
4379
  existing.push(nextTurn);
3235
4380
  }
3236
- return withTurns(state, existing);
4381
+ return withTurns({ ...state, turns: existing }, existing);
3237
4382
  }
3238
4383
  function applyTurnPatch(state, patch) {
3239
4384
  const turn = patch.data.turn;
@@ -3244,13 +4389,7 @@ function applyTurnPatch(state, patch) {
3244
4389
  if (lastSequence !== null && patch.sequence <= lastSequence) {
3245
4390
  return state;
3246
4391
  }
3247
- const nextTurn = { ...turn, sequence: patch.sequence };
3248
- if (index >= 0) {
3249
- existing[index] = nextTurn;
3250
- } else {
3251
- existing.push(nextTurn);
3252
- }
3253
- return withTurns(state, existing);
4392
+ return upsertTurn(state, { ...turn, sequence: patch.sequence });
3254
4393
  }
3255
4394
  function addErrorMessage(state, content) {
3256
4395
  const anchorTurnId = state.turns[state.turns.length - 1]?.turn_id ?? null;
@@ -3282,19 +4421,27 @@ function markStreamingTurns(state, turnStatus, toolCallStatus) {
3282
4421
 
3283
4422
  // src/session/agent-session.ts
3284
4423
  var OOM_MESSAGE = "\u6C99\u76D2\u5185\u5B58\u4F7F\u7528\u8D85\u51FA\u9650\u5236\uFF0C\u5DF2\u81EA\u52A8\u91CD\u542F\u3002\u5982\u679C\u7ECF\u5E38\u89E6\u53D1\uFF0C\u53EF\u4EE5\u8054\u7CFB\u7BA1\u7406\u5458\u8C03\u6574\u989D\u5EA6\u3002";
3285
- var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
3286
- function isRecord5(value) {
3287
- return typeof value === "object" && value !== null && !Array.isArray(value);
4424
+ var COMMAND_RESTORE_RETRY_BASE_MS = 1e3;
4425
+ var COMMAND_RESTORE_RETRY_MAX_MS = 3e4;
4426
+ var QUEUE_ACK_TIMEOUT_MS = 1e4;
4427
+ var fallbackRequestCounter = 0;
4428
+ function createClientRequestId() {
4429
+ if (typeof globalThis.crypto?.randomUUID === "function") {
4430
+ return globalThis.crypto.randomUUID();
4431
+ }
4432
+ fallbackRequestCounter += 1;
4433
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}-${fallbackRequestCounter}`;
3288
4434
  }
4435
+ var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
3289
4436
  function isOomText(value) {
3290
4437
  return typeof value === "string" && OOM_KEYWORDS_RE.test(value);
3291
4438
  }
3292
4439
  function parseJsonRecord(value) {
3293
- if (isRecord5(value)) return value;
4440
+ if (isRecord(value)) return value;
3294
4441
  if (typeof value !== "string") return null;
3295
4442
  try {
3296
4443
  const parsed = JSON.parse(value);
3297
- return isRecord5(parsed) ? parsed : null;
4444
+ return isRecord(parsed) ? parsed : null;
3298
4445
  } catch {
3299
4446
  return null;
3300
4447
  }
@@ -3320,6 +4467,20 @@ function isOomResult(value) {
3320
4467
  function isToolBridgeContent(value) {
3321
4468
  return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.action === "string";
3322
4469
  }
4470
+ function isCursorInvalidError(error) {
4471
+ return typeof error === "object" && error !== null && "status" in error && error.status === 409;
4472
+ }
4473
+ function queueRequestFailure(error) {
4474
+ return {
4475
+ ok: false,
4476
+ conflict: false,
4477
+ snapshot: null,
4478
+ message: error instanceof Error && error.message ? error.message : "\u961F\u5217\u64CD\u4F5C\u5931\u8D25"
4479
+ };
4480
+ }
4481
+ function commandDeliveryKey(deliveryId) {
4482
+ return `tool-command:${deliveryId}`;
4483
+ }
3323
4484
  function sessionStatusFromChatEnd(status) {
3324
4485
  if (status === "paused") return "waiting_for_input";
3325
4486
  if (status === "interrupted") return "interrupted";
@@ -3335,14 +4496,6 @@ function extractTerminalChatEndStatus(events) {
3335
4496
  }
3336
4497
  return null;
3337
4498
  }
3338
- function hasChatRunEvent2(events) {
3339
- return events.some(
3340
- (event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
3341
- );
3342
- }
3343
- function isUiMetaLike(value) {
3344
- return isRecord5(value) && ("resourceHTML" in value || "resourceUri" in value || "resourceURI" in value);
3345
- }
3346
4499
  var AgentSession = class _AgentSession {
3347
4500
  sessionId;
3348
4501
  state;
@@ -3356,14 +4509,41 @@ var AgentSession = class _AgentSession {
3356
4509
  patchFlushCancel = null;
3357
4510
  joined = false;
3358
4511
  joinPending = null;
4512
+ // bootstrap 与每次 reconnect 共用一条订阅生命周期队列。它们都会重置 join
4513
+ // 状态并临时设置 replay source,禁止并发,否则较早操作的 finally 会清掉
4514
+ // 较晚操作仍在使用的来源上下文。
4515
+ subscriptionQueue = Promise.resolve();
4516
+ replaceQueue = Promise.resolve();
4517
+ timelineEpoch = 0;
4518
+ /** 定位请求的流水号:被后一次选择取代的那次定位不再提交窗口(见 locateEntry)。 */
4519
+ locateToken = 0;
4520
+ /** 在途那次定位的请求句柄。**只**服务定位:权威刷新不共用它(见 cancelPendingLocate)。 */
4521
+ locateAbort = null;
4522
+ pagingGeneration = 0;
4523
+ liveRevision = 0;
4524
+ modeRevision = 0;
4525
+ liveRevisions = /* @__PURE__ */ new Map();
4526
+ pendingOptimisticTurnIds = /* @__PURE__ */ new Set();
4527
+ olderPending = null;
4528
+ replaceInFlight = 0;
3359
4529
  // 回放控制请求排成一条队列。连着点"5x"再点"退出回放"时,并发发出去的两个
3360
4530
  // 请求响应可能乱序回来,晚到的旧值会把已经退出的会话又显示成回放中;用
3361
4531
  // 序号丢弃过期响应又会在失败时留下中间态。串行执行就没有这些情况:每个
3362
4532
  // 响应都是当时最新的,失败也只影响自己。
3363
4533
  replayQueue = Promise.resolve();
4534
+ // Historical snapshots and live commands share one delivery barrier. A
4535
+ // repair closes the barrier before joining, snapshots the raw command log
4536
+ // after the join, then releases buffered commands in socket arrival order.
4537
+ commandRestoreQueue = Promise.resolve();
4538
+ commandRecoveryGeneration = 0;
4539
+ commandRestoreRetryAttempt = 0;
4540
+ commandRestoreRetryHandle = null;
4541
+ bufferedCommandDeliveries = null;
3364
4542
  pendingReplayMessage = null;
3365
4543
  pendingReplayMode;
4544
+ pendingReplayKbIds;
3366
4545
  recentChatEndAt = 0;
4546
+ subscriptionReplaySource = null;
3367
4547
  disposed = false;
3368
4548
  getAppContext = null;
3369
4549
  sendPending = false;
@@ -3374,11 +4554,19 @@ var AgentSession = class _AgentSession {
3374
4554
  seenToolCallIds = /* @__PURE__ */ new Set();
3375
4555
  finishedToolCallIds = /* @__PURE__ */ new Set();
3376
4556
  emittedMessageTurnIds = /* @__PURE__ */ new Set();
3377
- constructor(sessionId, runtime, connection) {
4557
+ constructor(sessionId, runtime, connection, options = {}) {
3378
4558
  this.sessionId = sessionId;
3379
4559
  this.runtime = runtime;
3380
4560
  this.state = { ...createInitialSessionState(sessionId), connection };
4561
+ this.isStatic = options.staticHistory === true;
3381
4562
  }
4563
+ /**
4564
+ * 静态历史(分享页 / `fromTurns`):没有目录接口,也**没有**"目录还没到位"这个中间态。
4565
+ *
4566
+ * 宿主据此决定要不要拿已加载窗口当兜底条目——真实会话在 timeline 被重写后,窗口里还是
4567
+ * 旧分支的发言,拿它兜底会把已经失效的 entry id 重新摆到轨道上,点了必然找不到。
4568
+ */
4569
+ isStatic;
3382
4570
  /** 从一份静态 turn 快照构造只展示历史的会话(例如公开分享页)。 */
3383
4571
  static fromTurns(sessionId, turns) {
3384
4572
  const readonlyError = () => {
@@ -3387,11 +4575,13 @@ var AgentSession = class _AgentSession {
3387
4575
  const session = new _AgentSession(
3388
4576
  sessionId,
3389
4577
  {
3390
- streamTokens: false,
3391
4578
  emitEvent: readonlyError,
3392
4579
  emitWithAck: async () => readonlyError(),
3393
4580
  ensureConnected: readonlyError,
3394
- fetchTurns: async () => turns,
4581
+ fetchTurnsPage: async () => ({ turns, nextBefore: null, currentMode: "executing" }),
4582
+ // 静态会话(分享页)没有会话目录接口,宿主用已加载的历史兜底条目。
4583
+ fetchTurnIndex: async () => ({ sessionId, items: [] }),
4584
+ fetchCommands: async () => [],
3395
4585
  // 静态历史没有回放身份可言,给个空详情即可,不要抛错——_bootstrap
3396
4586
  // 不会跑到这里,但保持只读语义一致。
3397
4587
  fetchSessionInfo: async () => ({ replay_state: null }),
@@ -3402,7 +4592,8 @@ var AgentSession = class _AgentSession {
3402
4592
  attachAppCli: async () => readonlyError(),
3403
4593
  onDispose: () => true
3404
4594
  },
3405
- "connected"
4595
+ "connected",
4596
+ { staticHistory: true }
3406
4597
  );
3407
4598
  session.state = {
3408
4599
  ...withTurns(session.state, turns),
@@ -3415,6 +4606,101 @@ var AgentSession = class _AgentSession {
3415
4606
  getState() {
3416
4607
  return this.state;
3417
4608
  }
4609
+ get hasOlderHistory() {
4610
+ return this.state.nextBefore !== null;
4611
+ }
4612
+ /** Load the page immediately preceding the current display window. */
4613
+ loadOlderHistory(options = {}) {
4614
+ if (this.disposed) return Promise.resolve();
4615
+ if (this.olderPending) return this.olderPending;
4616
+ if (this.replaceInFlight > 0) {
4617
+ return this.replaceQueue.then(() => this.loadOlderHistory(options));
4618
+ }
4619
+ const before = this.state.nextBefore;
4620
+ if (!before) return Promise.resolve();
4621
+ const epoch = this.timelineEpoch;
4622
+ const generation = this.pagingGeneration;
4623
+ this.update((state) => ({ ...state, loadingOlder: true }));
4624
+ const request = this.runtime.fetchTurnsPage(this.sessionId, { before }).then((page) => {
4625
+ if (epoch !== this.timelineEpoch || generation !== this.pagingGeneration || this.state.nextBefore !== before) return;
4626
+ options.beforeCommit?.();
4627
+ this.update((state) => ({
4628
+ ...withTurns(state, prependOlder(state.turns, page.turns)),
4629
+ nextBefore: page.nextBefore,
4630
+ loadingOlder: false
4631
+ }));
4632
+ }).catch(async (error) => {
4633
+ if (isCursorInvalidError(error) && epoch === this.timelineEpoch && generation === this.pagingGeneration && this.state.nextBefore === before) {
4634
+ this.invalidateTimeline();
4635
+ await this.replaceFromHistory();
4636
+ return;
4637
+ }
4638
+ }).finally(() => {
4639
+ if (this.olderPending !== request) return;
4640
+ this.olderPending = null;
4641
+ this.update((state) => ({ ...state, loadingOlder: false }));
4642
+ });
4643
+ this.olderPending = request;
4644
+ return request;
4645
+ }
4646
+ /**
4647
+ * 定位到一条窗口外的历史发言:请服务端返回"从这条发言到最新"的窗口并整窗替换。
4648
+ *
4649
+ * 返回 false 表示这次定位没有落地(请求失败,或期间 timeline 被重写、会话已释放),
4650
+ * 当前窗口保持不变,调用方可以原样重试。已加载的发言不要走这里——宿主直接滚动,
4651
+ * 一次网络请求都不该发。
4652
+ */
4653
+ async locateEntry(entryId) {
4654
+ const token = ++this.locateToken;
4655
+ const controller = new AbortController();
4656
+ this.locateAbort?.abort();
4657
+ this.locateAbort = controller;
4658
+ try {
4659
+ return await this.replaceFromHistory({
4660
+ suppressErrors: false,
4661
+ fromEntryId: entryId,
4662
+ signal: controller.signal,
4663
+ isStale: () => this.locateToken !== token
4664
+ });
4665
+ } catch {
4666
+ return false;
4667
+ } finally {
4668
+ if (this.locateAbort === controller) this.locateAbort = null;
4669
+ }
4670
+ }
4671
+ /**
4672
+ * 取消在途定位:用户改选了另一条发言(哪怕那条已经加载、不需要请求),
4673
+ * 先前那次定位既不该再落地,也不该继续占着替换队列——排队中的下一次定位
4674
+ * 不然要等它跑完(用户看到的是"点了新目标一直转圈")。
4675
+ *
4676
+ * **只**中止定位自己的请求:权威刷新(运行结束 / 重连补数)不共用这个 controller,
4677
+ * 否则"运行刚结束时点一下侧栏"就会把终态对账一起中止,流式与历史对不上。
4678
+ */
4679
+ cancelPendingLocate() {
4680
+ this.locateToken += 1;
4681
+ this.locateAbort?.abort();
4682
+ this.locateAbort = null;
4683
+ }
4684
+ /**
4685
+ * timeline 被整体改写(rewind / checkout / 释放):推进纪元,并把在途定位请求一并中止
4686
+ * ——它已经属于旧 timeline,却还占着替换队列,会让改写后的权威窗口排在后面等它跑完。
4687
+ * 作废号仍然保留:中止只是让它更早结束。
4688
+ */
4689
+ invalidateTimeline() {
4690
+ this.timelineEpoch += 1;
4691
+ this.locateAbort?.abort();
4692
+ this.locateAbort = null;
4693
+ }
4694
+ /**
4695
+ * 拉取当前分支的用户发言目录(消息导航侧栏用)。
4696
+ *
4697
+ * 不在这里缓存:什么时候该刷新只有调用方知道(会话切换、历史被替换、新发言),
4698
+ * 缓存只会让旧条目在 rewind 之后继续可点。
4699
+ */
4700
+ async loadTurnIndex() {
4701
+ const index = await this.runtime.fetchTurnIndex(this.sessionId);
4702
+ return index.items;
4703
+ }
3418
4704
  /** 订阅状态变化。返回取消订阅函数。 */
3419
4705
  subscribe(listener) {
3420
4706
  this.listeners.add(listener);
@@ -3467,7 +4753,7 @@ var AgentSession = class _AgentSession {
3467
4753
  /** 发送一条消息。字符串或多模态内容均可。 */
3468
4754
  async send(content, options = {}) {
3469
4755
  this.runtime.ensureConnected();
3470
- if (this.state.isStreaming || this.state.activeCompaction || this.sendPending || this.manualCompactionPending) {
4756
+ if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.sendPending || this.manualCompactionPending) {
3471
4757
  this.update(
3472
4758
  (s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\u6216\u5BF9\u8BDD\u6B63\u5728\u6574\u7406\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002")
3473
4759
  );
@@ -3480,9 +4766,125 @@ var AgentSession = class _AgentSession {
3480
4766
  this.sendPending = false;
3481
4767
  }
3482
4768
  }
4769
+ /**
4770
+ * 入队一条等待消息(运行中排队 / 暂停期间照常入队)。
4771
+ *
4772
+ * ack 超时或传输失败时结果未知:用**同一个** `client_request_id` 重试一次,
4773
+ * 服务端按 `(session_id, client_request_id)` 幂等,不会制造第二条。
4774
+ */
4775
+ async queueMessage(content) {
4776
+ const text = content.trim();
4777
+ if (!text) {
4778
+ return { ok: false, conflict: false, snapshot: null, message: "\u6D88\u606F\u4E0D\u80FD\u4E3A\u7A7A" };
4779
+ }
4780
+ const payload = {
4781
+ session_id: this.sessionId,
4782
+ message: text,
4783
+ client_request_id: createClientRequestId()
4784
+ };
4785
+ try {
4786
+ return await this.requestQueueAck("chat:append", payload);
4787
+ } catch {
4788
+ try {
4789
+ return await this.requestQueueAck("chat:append", payload);
4790
+ } catch (error) {
4791
+ return queueRequestFailure(error);
4792
+ }
4793
+ }
4794
+ }
4795
+ /** 修改一条等待消息的正文;`expectedVersion` 是客户端看到的条目版本。 */
4796
+ async updateQueuedMessage(itemId, expectedVersion, content) {
4797
+ return this.runQueueCommand("chat:queue:update", {
4798
+ session_id: this.sessionId,
4799
+ item_id: itemId,
4800
+ expected_version: expectedVersion,
4801
+ message: content
4802
+ });
4803
+ }
4804
+ /**
4805
+ * 重排等待消息。`orderedIds` 必须是当前 pending 条目 id 的**完整集合**(顺序任意),
4806
+ * 服务端按 `expectedRevision` 校验后整体替换顺序。
4807
+ *
4808
+ * 拖动排序就该像本地操作一样落地:版本冲突(运行中的任务刚领走一条 / 另一个端也在
4809
+ * 动队列)不甩给用户「刷新后重试」,而是拿冲突响应里那份**最新快照**重算一次再提交
4810
+ * ——掉队的条目(已经被领取执行的)从顺序里剔除,剩下的保持用户拖出来的相对次序。
4811
+ * 最多补两次;仍旧冲突才把结果交回调用方。
4812
+ */
4813
+ async reorderQueuedMessages(expectedRevision, orderedIds) {
4814
+ let revision = expectedRevision;
4815
+ let ids = orderedIds;
4816
+ let result = await this.runQueueCommand("chat:queue:reorder", {
4817
+ session_id: this.sessionId,
4818
+ expected_revision: revision,
4819
+ ordered_ids: ids
4820
+ });
4821
+ for (let attempt = 0; attempt < 2 && !result.ok && result.conflict; attempt++) {
4822
+ const snapshot2 = result.snapshot;
4823
+ if (!snapshot2) break;
4824
+ const pending = snapshot2.items.filter(canEditQueuedMessage);
4825
+ const live = new Set(pending.map((item) => item.id));
4826
+ const next = ids.filter((id) => live.has(id));
4827
+ for (const item of pending) if (!next.includes(item.id)) next.push(item.id);
4828
+ if (next.length === 0) {
4829
+ return { ok: true, conflict: false, snapshot: snapshot2, message: null };
4830
+ }
4831
+ revision = snapshot2.revision;
4832
+ ids = next;
4833
+ result = await this.runQueueCommand("chat:queue:reorder", {
4834
+ session_id: this.sessionId,
4835
+ expected_revision: revision,
4836
+ ordered_ids: ids
4837
+ });
4838
+ }
4839
+ return result;
4840
+ }
4841
+ /** 删除一条等待消息;不传版本时由服务端自行判定。 */
4842
+ async cancelQueuedMessage(itemId, expectedVersion) {
4843
+ return this.runQueueCommand("chat:queue:cancel", {
4844
+ session_id: this.sessionId,
4845
+ item_id: itemId,
4846
+ expected_version: expectedVersion
4847
+ });
4848
+ }
4849
+ /** 立即补充:把等待消息交给当前运行,在下一个接收点消费,不停止也不另起一轮。 */
4850
+ async deliverQueuedMessage(itemId, expectedVersion) {
4851
+ return this.runQueueCommand("chat:queue:deliver", {
4852
+ session_id: this.sessionId,
4853
+ item_id: itemId,
4854
+ expected_version: expectedVersion
4855
+ });
4856
+ }
4857
+ /** 继续执行:清除暂停,并按当前顺序恢复逐条接续。 */
4858
+ async resumeQueue() {
4859
+ return this.runQueueCommand("chat:queue:resume", { session_id: this.sessionId });
4860
+ }
4861
+ /** 拉取一份权威队列快照。首次连接与每次重连后由实例自动调用,也可以手动刷新。 */
4862
+ async refreshQueue() {
4863
+ return this.runQueueCommand("chat:queue:list", { session_id: this.sessionId });
4864
+ }
4865
+ /**
4866
+ * 发出一条队列命令并安装 ack 里的快照。
4867
+ * 传输层失败(超时 / 断线)会 reject,由调用方决定是否重试。
4868
+ */
4869
+ async requestQueueAck(event, payload) {
4870
+ this.runtime.ensureConnected();
4871
+ const response = await this.runtime.emitWithAck(event, payload, QUEUE_ACK_TIMEOUT_MS);
4872
+ const result = parseQueueAck(response, this.sessionId);
4873
+ const snapshot2 = result.snapshot;
4874
+ if (snapshot2) this.update((state) => withQueue(state, snapshot2));
4875
+ return result;
4876
+ }
4877
+ /** 队列命令的对外入口:传输失败只体现为失败结果,不向调用方抛异常。 */
4878
+ async runQueueCommand(event, payload) {
4879
+ try {
4880
+ return await this.requestQueueAck(event, payload);
4881
+ } catch (error) {
4882
+ return queueRequestFailure(error);
4883
+ }
4884
+ }
3483
4885
  async sendUnlocked(content, options) {
3484
4886
  await this.ensureIdleBeforeSend();
3485
- if (this.state.isStreaming || this.state.activeCompaction || this.manualCompactionPending) {
4887
+ if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.manualCompactionPending) {
3486
4888
  this.update(
3487
4889
  (s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\u6216\u5BF9\u8BDD\u6B63\u5728\u6574\u7406\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002")
3488
4890
  );
@@ -3492,7 +4894,7 @@ var AgentSession = class _AgentSession {
3492
4894
  if (this.getAppContext) {
3493
4895
  try {
3494
4896
  const rawContext = await this.getAppContext();
3495
- if (!isRecord5(rawContext)) {
4897
+ if (!isRecord(rawContext)) {
3496
4898
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
3497
4899
  }
3498
4900
  const serialized = JSON.stringify(rawContext);
@@ -3500,7 +4902,7 @@ var AgentSession = class _AgentSession {
3500
4902
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE\u53EF\u5E8F\u5217\u5316\u7684 JSON \u5BF9\u8C61");
3501
4903
  }
3502
4904
  const normalized = JSON.parse(serialized);
3503
- if (!isRecord5(normalized)) {
4905
+ if (!isRecord(normalized)) {
3504
4906
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
3505
4907
  }
3506
4908
  appContext = normalized;
@@ -3512,15 +4914,15 @@ var AgentSession = class _AgentSession {
3512
4914
  }
3513
4915
  const askUserAnswer = options.askUserAnswer;
3514
4916
  let optimisticTurnId = null;
4917
+ const clientRequestId = createClientRequestId();
3515
4918
  let optimisticAskAnswer;
3516
4919
  let previousAskAnswer;
3517
- if (!(askUserAnswer && typeof askUserAnswer.tool_call_id === "string")) {
3518
- this.update((state) => {
3519
- const next = addUserMessage(state, content);
3520
- optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
3521
- return next;
3522
- });
3523
- }
4920
+ this.update((state) => {
4921
+ const next = addUserMessage(state, content, clientRequestId);
4922
+ optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
4923
+ return next;
4924
+ });
4925
+ if (optimisticTurnId) this.markOptimisticTurnPending(optimisticTurnId);
3524
4926
  this.update((s) => ({ ...s, isStreaming: true }));
3525
4927
  if (askUserAnswer && typeof askUserAnswer.tool_call_id === "string") {
3526
4928
  const { tool_call_id, ...rest } = askUserAnswer;
@@ -3534,17 +4936,22 @@ var AgentSession = class _AgentSession {
3534
4936
  if (!options.replayDecision) {
3535
4937
  this.pendingReplayMessage = content;
3536
4938
  this.pendingReplayMode = options.mode;
4939
+ this.pendingReplayKbIds = options.kbIds;
3537
4940
  }
3538
4941
  const payload = {
3539
4942
  session_id: this.sessionId,
3540
4943
  message: content,
4944
+ client_request_id: clientRequestId,
3541
4945
  mode: options.mode,
3542
4946
  askuser_answer: askUserAnswer,
3543
4947
  model: options.model,
3544
4948
  thinking_override: options.thinkingOverride,
3545
4949
  whatif: options.whatif,
3546
4950
  replay_decision: options.replayDecision,
3547
- app_context: appContext
4951
+ app_context: appContext,
4952
+ kb_ids: options.kbIds,
4953
+ // 运行中提交不必由客户端挑命令:服务端按会话运行状态决定直接执行还是入队。
4954
+ queue_when_running: options.queueWhenRunning === true
3548
4955
  };
3549
4956
  try {
3550
4957
  await this.ensureJoined();
@@ -3560,6 +4967,21 @@ var AgentSession = class _AgentSession {
3560
4967
  }
3561
4968
  try {
3562
4969
  const response = await this.runtime.emitWithAck("chat:send", payload, 3e4);
4970
+ if (response?.status === "queued") {
4971
+ this.discardOptimisticSend(
4972
+ optimisticTurnId,
4973
+ askUserAnswer,
4974
+ optimisticAskAnswer,
4975
+ previousAskAnswer
4976
+ );
4977
+ const snapshot2 = parseQueueAck(response, this.sessionId).snapshot;
4978
+ const running = response.running === true;
4979
+ this.update((current) => {
4980
+ const next = running ? current : { ...current, isStreaming: false };
4981
+ return snapshot2 ? withQueue(next, snapshot2) : next;
4982
+ });
4983
+ return true;
4984
+ }
3563
4985
  if (response?.status !== "accepted") {
3564
4986
  throw new Error(
3565
4987
  typeof response?.message === "string" ? response.message : "\u6D88\u606F\u672A\u88AB\u670D\u52A1\u7AEF\u63A5\u53D7"
@@ -3578,13 +5000,22 @@ var AgentSession = class _AgentSession {
3578
5000
  }
3579
5001
  }
3580
5002
  rollbackOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer, message) {
5003
+ this.discardOptimisticSend(
5004
+ optimisticTurnId,
5005
+ askUserAnswer,
5006
+ optimisticAskAnswer,
5007
+ previousAskAnswer
5008
+ );
5009
+ this.update((current) => addErrorMessage({ ...current, isStreaming: false }, message));
5010
+ }
5011
+ /** 撤回一次乐观提交:移除乐观消息、恢复提问作答、清掉回放草稿。 */
5012
+ discardOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer) {
3581
5013
  this.pendingReplayMessage = null;
3582
5014
  this.pendingReplayMode = void 0;
5015
+ this.pendingReplayKbIds = void 0;
3583
5016
  this.update((current) => {
3584
- let next = optimisticTurnId ? withTurns(
3585
- current,
3586
- current.turns.filter((turn) => turn.turn_id !== optimisticTurnId)
3587
- ) : current;
5017
+ const filtered = optimisticTurnId ? current.turns.filter((turn) => turn.turn_id !== optimisticTurnId) : current.turns;
5018
+ let next = optimisticTurnId ? withTurns({ ...current, turns: filtered }, filtered) : current;
3588
5019
  const toolCallId = askUserAnswer && typeof askUserAnswer.tool_call_id === "string" ? askUserAnswer.tool_call_id : null;
3589
5020
  if (toolCallId && next.askAnswers[toolCallId] === optimisticAskAnswer) {
3590
5021
  const askAnswers = { ...next.askAnswers };
@@ -3595,18 +5026,20 @@ var AgentSession = class _AgentSession {
3595
5026
  }
3596
5027
  next = { ...next, askAnswers };
3597
5028
  }
3598
- return addErrorMessage({ ...next, isStreaming: false }, message);
5029
+ return next;
3599
5030
  });
3600
- }
3601
- /** 智能体运行期间追加补充说明(不打断当前回复)。 */
3602
- append(text) {
3603
- this.runtime.ensureConnected();
3604
- this.runtime.emitEvent("chat:append", { session_id: this.sessionId, message: text });
3605
- this.update((s) => addUserMessage(s, text));
5031
+ if (optimisticTurnId) {
5032
+ this.pendingOptimisticTurnIds.delete(optimisticTurnId);
5033
+ this.liveRevisions.delete(optimisticTurnId);
5034
+ }
3606
5035
  }
3607
5036
  /** 停止当前回复。 */
3608
5037
  stop() {
3609
- return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) => response ?? {});
5038
+ return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) => {
5039
+ const result = response ?? {};
5040
+ if (result.status !== "accepted") this.manualCompactionPending = false;
5041
+ return result;
5042
+ });
3610
5043
  }
3611
5044
  /** 手动触发上下文压缩。 */
3612
5045
  compact() {
@@ -3626,8 +5059,10 @@ var AgentSession = class _AgentSession {
3626
5059
  trigger: "manual"
3627
5060
  }
3628
5061
  }));
3629
- return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {}).finally(() => {
5062
+ return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {}).catch((error) => {
3630
5063
  this.manualCompactionPending = false;
5064
+ throw error;
5065
+ }).finally(() => {
3631
5066
  if (this.state.activeCompaction?.compaction_id === "pending-manual-compaction") {
3632
5067
  this.update((state) => ({ ...state, activeCompaction: null }));
3633
5068
  }
@@ -3666,10 +5101,20 @@ var AgentSession = class _AgentSession {
3666
5101
  if (this.disposed) return;
3667
5102
  if (!this.runtime.onDispose(this)) return;
3668
5103
  this.disposed = true;
5104
+ this.invalidateTimeline();
5105
+ this.invalidateOlderPaging();
5106
+ this.state = { ...this.state, nextBefore: null, loadingOlder: false };
3669
5107
  this.cancelPatchFlush();
3670
5108
  this.listeners.clear();
3671
5109
  this.emitter.clear();
3672
5110
  this.commands.clear();
5111
+ this.commandRecoveryGeneration += 1;
5112
+ this.commandRestoreRetryAttempt = 0;
5113
+ this.bufferedCommandDeliveries = null;
5114
+ if (this.commandRestoreRetryHandle !== null) {
5115
+ globalThis.clearTimeout(this.commandRestoreRetryHandle);
5116
+ this.commandRestoreRetryHandle = null;
5117
+ }
3673
5118
  }
3674
5119
  get isDisposed() {
3675
5120
  return this.disposed;
@@ -3680,28 +5125,64 @@ var AgentSession = class _AgentSession {
3680
5125
  if (this.state.connection === connection) return;
3681
5126
  this.update((s) => ({ ...s, connection }));
3682
5127
  }
5128
+ /**
5129
+ * @internal 服务端广播的权威队列快照。
5130
+ * 结构不合法或 revision 不新的一律丢弃,绝不用旧快照覆盖新状态。
5131
+ */
5132
+ _handleQueueUpdated(payload) {
5133
+ if (!isSessionQueueSnapshot(payload)) return;
5134
+ const snapshot2 = payload;
5135
+ this.update((state) => withQueue(state, snapshot2));
5136
+ }
3683
5137
  /** @internal socket 重连后由 Hub 调用:重新加入房间并补数。 */
3684
- async _rejoinAfterReconnect() {
3685
- const turnsBeforeRejoin = this.state.turns;
3686
- this.joined = false;
3687
- this.joinPending = null;
3688
- try {
3689
- await this.ensureJoined();
3690
- await Promise.all([
3691
- this.syncTurnsFromHistory({
3692
- clearOnEmpty: true,
3693
- preserveChangesSince: turnsBeforeRejoin
3694
- }),
3695
- this.syncReplayFromSession()
3696
- ]);
3697
- } catch {
3698
- }
5138
+ _rejoinAfterReconnect() {
5139
+ this.invalidateOlderPaging();
5140
+ this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
5141
+ const reconnectLiveRevision = this.liveRevision;
5142
+ const reconnectModeRevision = this.modeRevision;
5143
+ const commandRecoveryGeneration = this.beginCommandRecovery();
5144
+ return this.enqueueSubscription(async () => {
5145
+ this.joined = false;
5146
+ this.joinPending = null;
5147
+ try {
5148
+ this.subscriptionReplaySource = "reconnect_replay";
5149
+ try {
5150
+ await this.ensureJoined(reconnectLiveRevision);
5151
+ } finally {
5152
+ this.subscriptionReplaySource = null;
5153
+ this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
5154
+ }
5155
+ await Promise.all([
5156
+ this.refreshAuthoritativeHistory({
5157
+ liveRevisionAtStart: reconnectLiveRevision,
5158
+ modeRevisionAtStart: reconnectModeRevision
5159
+ }),
5160
+ this.syncReplayFromSession()
5161
+ ]);
5162
+ } catch {
5163
+ }
5164
+ });
3699
5165
  }
3700
5166
  /** @internal 首次连接:加载历史 + 加入房间。 */
3701
- async _bootstrap() {
3702
- await this.syncTurnsFromHistory({ suppressErrors: false });
3703
- await this.ensureJoined();
3704
- await this.syncReplayFromSession();
5167
+ _bootstrap() {
5168
+ const commandRecoveryGeneration = this.beginCommandRecovery();
5169
+ return this.enqueueSubscription(async () => {
5170
+ await this.replaceFromHistory({ suppressErrors: false });
5171
+ this.subscriptionReplaySource = "bootstrap_replay";
5172
+ try {
5173
+ await this.ensureJoined();
5174
+ } finally {
5175
+ this.subscriptionReplaySource = null;
5176
+ }
5177
+ this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
5178
+ await Promise.all([this.syncReplayFromSession(), this.refreshQueue()]);
5179
+ });
5180
+ }
5181
+ /** 订阅生命周期串行执行;一次失败只由自己的调用方观察,不截断后续重连。 */
5182
+ enqueueSubscription(operation) {
5183
+ const run = this.subscriptionQueue.then(operation);
5184
+ this.subscriptionQueue = run.catch(() => void 0);
5185
+ return run;
3705
5186
  }
3706
5187
  /** 改回放是 owner 专属(服务端 PATCH 走 _get_owned_session)。 */
3707
5188
  get canControlReplay() {
@@ -3758,10 +5239,16 @@ var AgentSession = class _AgentSession {
3758
5239
  this.applyTurn(turn);
3759
5240
  }
3760
5241
  /** @internal 历史回放通道(event_format: raw)。 */
3761
- _handleTurnEvents(events) {
5242
+ _handleTurnEvents(events, replayed = false) {
3762
5243
  if (!events.length) return;
5244
+ if (events.some(
5245
+ (event) => event.type === "compaction:end" || event.type === "compaction:error" || event.type === "compaction:abort"
5246
+ )) {
5247
+ this.manualCompactionPending = false;
5248
+ }
5249
+ this.dispatchRawCommands(events);
3763
5250
  const terminalChatEndStatus = extractTerminalChatEndStatus(events);
3764
- if (!terminalChatEndStatus && hasChatRunEvent2(events)) {
5251
+ if (!terminalChatEndStatus && hasChatRunEvent(events) && this.state.status !== "waiting_for_input") {
3765
5252
  this.markRunningIfIdle();
3766
5253
  }
3767
5254
  if (this.state.turns.length) {
@@ -3770,7 +5257,10 @@ var AgentSession = class _AgentSession {
3770
5257
  const updates = this.projectionBuilder.processBatch(events);
3771
5258
  for (const update of updates) {
3772
5259
  if (update.kind === "upsert") {
3773
- this.applyTurn(update.turn);
5260
+ this.applyTurn(
5261
+ update.turn,
5262
+ replayed ? this.subscriptionReplaySource ?? "bootstrap_replay" : "live"
5263
+ );
3774
5264
  } else if (update.kind === "workspace_changed") {
3775
5265
  this.emitter.emit("workspaceChanged", {});
3776
5266
  }
@@ -3825,16 +5315,26 @@ var AgentSession = class _AgentSession {
3825
5315
  }
3826
5316
  /** @internal */
3827
5317
  _handleRewind(checkpointId) {
3828
- this.update((s) => ({ ...s, mode: null, errorMessage: null }));
3829
- void this.syncTurnsFromHistory({ clearOnEmpty: true });
5318
+ this.invalidateTimeline();
5319
+ this.invalidateOlderPaging();
5320
+ this.update((s) => ({
5321
+ ...s,
5322
+ mode: null,
5323
+ errorMessage: null,
5324
+ nextBefore: null,
5325
+ loadingOlder: false
5326
+ }));
5327
+ void this.replaceFromHistory();
3830
5328
  this.emitter.emit("rewind", { checkpointId });
3831
5329
  }
3832
5330
  /** @internal */
3833
5331
  _handleReplayMismatch(data) {
3834
5332
  const message = this.pendingReplayMessage ?? data.actual_message ?? "";
3835
5333
  const mode = this.pendingReplayMode;
5334
+ const kbIds = this.pendingReplayKbIds;
3836
5335
  this.pendingReplayMessage = null;
3837
5336
  this.pendingReplayMode = void 0;
5337
+ this.pendingReplayKbIds = void 0;
3838
5338
  const respond = (decision) => {
3839
5339
  void this.runtime.emitWithAck(
3840
5340
  "chat:send",
@@ -3842,7 +5342,8 @@ var AgentSession = class _AgentSession {
3842
5342
  session_id: this.sessionId,
3843
5343
  message,
3844
5344
  mode,
3845
- replay_decision: decision
5345
+ replay_decision: decision,
5346
+ kb_ids: kbIds
3846
5347
  },
3847
5348
  3e4
3848
5349
  ).then((response) => {
@@ -3883,9 +5384,9 @@ var AgentSession = class _AgentSession {
3883
5384
  detail,
3884
5385
  status,
3885
5386
  loopId,
3886
- metadata: isRecord5(notification.metadata) ? notification.metadata : void 0
5387
+ metadata: isRecord(notification.metadata) ? notification.metadata : void 0
3887
5388
  });
3888
- if (notificationType === "bg:started" && isRecord5(notification.metadata)) {
5389
+ if (notificationType === "bg:started" && isRecord(notification.metadata)) {
3889
5390
  const taskId = typeof notification.metadata.task_id === "string" ? notification.metadata.task_id : "";
3890
5391
  if (taskId) {
3891
5392
  this.emitter.emit("backgroundTask", {
@@ -3907,12 +5408,12 @@ var AgentSession = class _AgentSession {
3907
5408
  this.emitter.emit("artifact", event);
3908
5409
  }
3909
5410
  /** @internal 加入房间的应答里带 is_running,据此校准流式状态。 */
3910
- _applyRunningState(isRunning) {
5411
+ _applyRunningState(isRunning, liveRevisionAtJoin = this.liveRevision) {
3911
5412
  if (isRunning) {
3912
5413
  this.update((s) => ({ ...s, status: "running", isStreaming: true }));
3913
5414
  } else if (this.state.isStreaming) {
3914
5415
  this.update((s) => ({ ...s, isStreaming: false }));
3915
- void this.syncTurnsFromHistory();
5416
+ void this.refreshAuthoritativeHistory({ liveRevisionAtStart: liveRevisionAtJoin });
3916
5417
  }
3917
5418
  }
3918
5419
  // ---------------------------------------------------------------- 内部实现
@@ -3933,14 +5434,36 @@ var AgentSession = class _AgentSession {
3933
5434
  completeChat(status) {
3934
5435
  this.finishStreaming(status);
3935
5436
  this.recentChatEndAt = Date.now();
3936
- void this.syncTurnsFromHistory();
5437
+ void this.refreshAuthoritativeHistory();
3937
5438
  this.emitter.emit("chatEnd", { status });
3938
5439
  }
3939
- applyTurn(turn) {
5440
+ markTurnFresh(turnId) {
5441
+ this.liveRevision += 1;
5442
+ this.liveRevisions.set(turnId, this.liveRevision);
5443
+ }
5444
+ markOptimisticTurnPending(turnId) {
5445
+ this.pendingOptimisticTurnIds.add(turnId);
5446
+ this.markTurnFresh(turnId);
5447
+ }
5448
+ pruneOptimisticLiveRevisions() {
5449
+ const retained = new Set(
5450
+ this.state.turns.map((turn) => turn.turn_id).filter((turnId) => turnId.startsWith("local-user-"))
5451
+ );
5452
+ for (const turnId of this.liveRevisions.keys()) {
5453
+ if (turnId.startsWith("local-user-") && !retained.has(turnId)) {
5454
+ this.pendingOptimisticTurnIds.delete(turnId);
5455
+ this.liveRevisions.delete(turnId);
5456
+ }
5457
+ }
5458
+ }
5459
+ applyTurn(turn, source = "live") {
3940
5460
  this.flushPatches();
5461
+ this.markTurnFresh(turn.turn_id);
5462
+ if (extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
3941
5463
  const previous = this.state;
3942
5464
  this.update((s) => upsertTurn(s, turn));
3943
- this.emitTurnDerivedEvents(previous, turn);
5465
+ this.pruneOptimisticLiveRevisions();
5466
+ this.emitTurnDerivedEvents(previous, turn, source);
3944
5467
  }
3945
5468
  schedulePatchFlush() {
3946
5469
  if (this.patchFlushHandle != null) return;
@@ -3973,11 +5496,15 @@ var AgentSession = class _AgentSession {
3973
5496
  this.pendingTurnPatches.clear();
3974
5497
  for (const patch of patches) {
3975
5498
  const previous = this.state;
3976
- this.update((s) => applyTurnPatch(s, patch));
5499
+ const next = applyTurnPatch(previous, patch);
5500
+ if (next === previous) continue;
5501
+ this.liveRevision += 1;
5502
+ this.liveRevisions.set(patch.turn_id, this.liveRevision);
3977
5503
  const turn = patch.data.turn;
3978
- if (turn) {
3979
- this.emitTurnDerivedEvents(previous, turn);
3980
- }
5504
+ if (turn && extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
5505
+ this.setState(next);
5506
+ this.pruneOptimisticLiveRevisions();
5507
+ if (turn) this.emitTurnDerivedEvents(previous, turn);
3981
5508
  }
3982
5509
  if (this.patchFlushCancel) {
3983
5510
  this.patchFlushCancel();
@@ -3986,7 +5513,7 @@ var AgentSession = class _AgentSession {
3986
5513
  }
3987
5514
  }
3988
5515
  /** turn 数据落地后统一派生语义事件(指令 / 工具调用 / 消息 / 模式 / 预览 / OOM)。 */
3989
- emitTurnDerivedEvents(previous, turn) {
5516
+ emitTurnDerivedEvents(previous, turn, source = "live") {
3990
5517
  const mode = extractModeFromBlocks(turn.blocks);
3991
5518
  if (mode && previous.mode !== mode) {
3992
5519
  this.emitter.emit("modeChange", { mode });
@@ -3999,23 +5526,24 @@ var AgentSession = class _AgentSession {
3999
5526
  this.dispatchCommand(turn, block);
4000
5527
  continue;
4001
5528
  }
4002
- if (block.type === "tool_ui" && block.tool_call_id && isUiMetaLike(block.content)) {
5529
+ if (block.type === "tool_ui" && block.tool_call_id && isToolUiCard(block.content)) {
4003
5530
  const ui = block.content;
4004
5531
  if (ui.target !== "preview") continue;
4005
- const previewType = ui.resourceHTML ? "resource-html" : "resource-uri";
4006
- const previewContent = ui.resourceHTML ?? ui.resourceUri ?? ui.resourceURI;
4007
- if (!previewContent) continue;
4008
- const key = `${block.tool_call_id}:${previewType}`;
4009
- if (this.deliveredToolPreviews.get(key) === previewContent) continue;
4010
- this.deliveredToolPreviews.set(key, previewContent);
5532
+ const resolved = resolveToolUiCardContent(ui);
5533
+ if (!resolved) continue;
5534
+ const key = buildToolUiCardKey(block.tool_call_id, resolved.type, resolved.content);
5535
+ const fingerprint = JSON.stringify([resolved.content, ui.mcpApp?.sourceId]);
5536
+ if (this.deliveredToolPreviews.get(key) === fingerprint) continue;
5537
+ this.deliveredToolPreviews.set(key, fingerprint);
4011
5538
  this.emitter.emit("toolPreview", {
4012
5539
  toolCallId: block.tool_call_id,
4013
- type: previewType,
4014
- content: previewContent,
4015
- title: ui.title ?? "\u5DE5\u5177\u9884\u89C8"
5540
+ type: resolved.type,
5541
+ content: resolved.content,
5542
+ title: ui.title ?? "\u5DE5\u5177\u9884\u89C8",
5543
+ ...ui.mcpApp ? { mcpApp: ui.mcpApp } : {}
4016
5544
  });
4017
5545
  }
4018
- if (block.type === "system_notification" && isRecord5(block.content)) {
5546
+ if (block.type === "system_notification" && isRecord(block.content)) {
4019
5547
  this._handleSystemNotification(turn.loop_id, block.content);
4020
5548
  }
4021
5549
  }
@@ -4035,7 +5563,7 @@ var AgentSession = class _AgentSession {
4035
5563
  const finished = toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled";
4036
5564
  if (finished && !this.finishedToolCallIds.has(toolCall.id)) {
4037
5565
  this.finishedToolCallIds.add(toolCall.id);
4038
- this.emitter.emit("toolResult", { toolCall: info, turn });
5566
+ this.emitter.emit("toolResult", { toolCall: info, turn, source });
4039
5567
  if (isOomResult(toolCall.result)) {
4040
5568
  this.notifySandboxOom(toolCall.id);
4041
5569
  }
@@ -4052,17 +5580,36 @@ var AgentSession = class _AgentSession {
4052
5580
  dispatchCommand(turn, block) {
4053
5581
  if (!isToolBridgeContent(block.content)) return;
4054
5582
  const { action, payload } = block.content;
4055
- let payloadKey = "";
4056
- try {
4057
- payloadKey = JSON.stringify(payload ?? null);
4058
- } catch {
4059
- payloadKey = String(payload ?? "");
5583
+ const deliveryKey = block.delivery_id ? commandDeliveryKey(block.delivery_id) : `legacy-tool-command:${turn.turn_id}:${block.tool_call_id ?? ""}:${action}:${JSON.stringify(payload)}`;
5584
+ this.deliverCommand(deliveryKey, action, payload, block.tool_call_id ?? void 0);
5585
+ }
5586
+ dispatchRawCommands(events) {
5587
+ for (const event of events) {
5588
+ if (event.type !== "loop:tool_bridge") continue;
5589
+ const deliveryId = String(event.payload.delivery_id ?? "");
5590
+ const bridge = event.payload.bridge;
5591
+ if (!deliveryId || !isToolBridgeContent(bridge)) continue;
5592
+ this.deliverCommand(
5593
+ commandDeliveryKey(deliveryId),
5594
+ bridge.action,
5595
+ bridge.payload,
5596
+ String(event.payload.tool_call_id ?? "") || void 0
5597
+ );
4060
5598
  }
4061
- const deliveryKey = [block.tool_call_id ?? turn.id, action, payloadKey].join("\0");
5599
+ }
5600
+ deliverCommand(deliveryKey, action, payload, toolCallId) {
5601
+ if (this.bufferedCommandDeliveries) {
5602
+ this.bufferedCommandDeliveries.push({ deliveryKey, action, payload, toolCallId });
5603
+ return;
5604
+ }
5605
+ this.deliverCommandNow({ deliveryKey, action, payload, toolCallId });
5606
+ }
5607
+ deliverCommandNow(command) {
5608
+ const { deliveryKey, action, payload, toolCallId } = command;
4062
5609
  if (this.deliveredCommandKeys.has(deliveryKey)) return;
4063
5610
  this.deliveredCommandKeys.add(deliveryKey);
4064
- const meta = { toolCallId: block.tool_call_id ?? void 0 };
4065
- this.emitter.emit("command", { action, payload, toolCallId: meta.toolCallId });
5611
+ const meta = { toolCallId };
5612
+ this.emitter.emit("command", { action, payload, toolCallId });
4066
5613
  this.commands.dispatch(action, payload, meta);
4067
5614
  }
4068
5615
  notifySandboxOom(key) {
@@ -4071,49 +5618,195 @@ var AgentSession = class _AgentSession {
4071
5618
  this.update((s) => addErrorMessage(s, OOM_MESSAGE));
4072
5619
  this.emitter.emit("sandboxOom", { toolCallId: key });
4073
5620
  }
4074
- async syncTurnsFromHistory(options = {}) {
4075
- const { clearOnEmpty = false, suppressErrors = true, preserveChangesSince } = options;
4076
- const turnsAtRequestStart = this.state.turns;
4077
- try {
4078
- const turns = await this.runtime.fetchTurns(this.sessionId);
4079
- if (turns.length === 0 && !clearOnEmpty) return;
4080
- this.update((state) => {
4081
- const mergeBaseline = preserveChangesSince ?? turnsAtRequestStart;
4082
- if (state.turns === mergeBaseline) return withTurns(state, turns);
4083
- const historyById = new Map(turns.map((turn) => [turn.turn_id, turn]));
4084
- let merged = withTurns(state, turns);
4085
- for (const current of state.turns) {
4086
- const fromHistory = historyById.get(current.turn_id);
4087
- if (!fromHistory || current.sequence > fromHistory.sequence) {
4088
- merged = upsertTurn(merged, current);
4089
- }
4090
- }
4091
- return merged;
4092
- });
4093
- this.replayCommandsFromTurns(turns);
4094
- } catch (error) {
4095
- if (!suppressErrors) throw error;
4096
- }
4097
- }
4098
5621
  /**
4099
- * 把历史里的指令补投给宿主页面。
5622
+ * 权威刷新(运行结束 / 重连补数):**按当前窗口第一条发言取窗口**,不再另记"用户想看哪段"。
4100
5623
  *
4101
- * 重开会话、断线补数时,地图高亮这类"页面当前状态"必须能恢复——
4102
- * 否则刷新一次页面,之前智能体让页面做的事就全丢了。
4103
- * 只回放指令:toolCall / message 这些事件如果按历史全量重放,
4104
- * 监听器会在挂载瞬间收到几百条陈年事件。
4105
- * dispatch 内部按 deliveredCommandKeys 去重,重复调用安全。
5624
+ * 窗口本身就是"用户在看哪段":首屏就是最新一页,向上翻页 / 定位之后首条就是用户滚到的位置。
5625
+ * 直接拿 `from_entry_id = 当前窗口第一条 turn_id` 去取,运行结束、重连补数、翻页之后的对账
5626
+ * 是同一条规则,不需要意图、锚点、已落地锚点这些状态;窗口首条已经不在分支上(404 / 409)时
5627
+ * 回落到最新一页。
4106
5628
  */
4107
- replayCommandsFromTurns(turns) {
5629
+ refreshAuthoritativeHistory(options = {}) {
5630
+ let entryGone = false;
5631
+ const locateTokenAtStart = this.locateToken;
5632
+ return this.replaceFromHistory({
5633
+ ...options,
5634
+ // 锚点在**这次操作真正开始**时采样:排队期间用户可能刚定位过,那时窗口首条才是他要的。
5635
+ fromEntryId: () => this.state.turns[0]?.turn_id,
5636
+ onError: (error) => {
5637
+ entryGone = isEntryGoneError(error);
5638
+ }
5639
+ }).then((located) => {
5640
+ if (located || !entryGone) return located;
5641
+ if (this.locateToken !== locateTokenAtStart) return false;
5642
+ return this.replaceFromHistory(options);
5643
+ });
5644
+ }
5645
+ replaceFromHistory(options = {}) {
5646
+ if (this.disposed) return Promise.resolve(false);
5647
+ const operationEpoch = this.timelineEpoch;
5648
+ const liveRevisionAtStart = options.liveRevisionAtStart ?? this.liveRevision;
5649
+ const modeRevisionAtStart = options.modeRevisionAtStart ?? this.modeRevision;
5650
+ this.invalidateOlderPaging();
5651
+ this.replaceInFlight += 1;
5652
+ const operation = async () => {
5653
+ let previousNextBefore = null;
5654
+ const restoreCursor = () => {
5655
+ if (this.disposed || previousNextBefore === null) return;
5656
+ this.update((state) => ({
5657
+ ...state,
5658
+ nextBefore: previousNextBefore,
5659
+ loadingOlder: false
5660
+ }));
5661
+ };
5662
+ try {
5663
+ if (this.disposed || operationEpoch !== this.timelineEpoch) return false;
5664
+ if (options.isStale?.()) return false;
5665
+ this.invalidateOlderPaging();
5666
+ previousNextBefore = this.state.nextBefore;
5667
+ this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
5668
+ const signal = options.signal;
5669
+ const fromEntryId = typeof options.fromEntryId === "function" ? options.fromEntryId() : options.fromEntryId;
5670
+ const page = await this.runtime.fetchTurnsPage(
5671
+ this.sessionId,
5672
+ fromEntryId ? { fromEntryId } : void 0,
5673
+ signal ? { signal } : void 0
5674
+ );
5675
+ if (this.disposed || operationEpoch !== this.timelineEpoch) {
5676
+ restoreCursor();
5677
+ return false;
5678
+ }
5679
+ if (options.isStale?.()) {
5680
+ restoreCursor();
5681
+ return false;
5682
+ }
5683
+ if (!this.joined) this.seedToolCallDeliveryBaseline(page.turns);
5684
+ this.update((state) => {
5685
+ const next = withTurns(
5686
+ state,
5687
+ replaceWindow(state.turns, page.turns, liveRevisionAtStart, {
5688
+ current: this.liveRevision,
5689
+ byTurnId: this.liveRevisions
5690
+ })
5691
+ );
5692
+ return {
5693
+ ...next,
5694
+ mode: this.modeRevision === modeRevisionAtStart ? page.currentMode : state.mode,
5695
+ nextBefore: page.nextBefore,
5696
+ loadingOlder: false
5697
+ };
5698
+ });
5699
+ this.pruneOptimisticLiveRevisions();
5700
+ this.projectionBuilder._seedTurns(this.state.turns);
5701
+ return true;
5702
+ } catch (error) {
5703
+ options.onError?.(error);
5704
+ if (!this.disposed && operationEpoch === this.timelineEpoch) {
5705
+ this.update((state) => ({
5706
+ ...state,
5707
+ nextBefore: previousNextBefore,
5708
+ loadingOlder: false
5709
+ }));
5710
+ }
5711
+ if (options.suppressErrors === false) throw error;
5712
+ return false;
5713
+ } finally {
5714
+ this.replaceInFlight -= 1;
5715
+ }
5716
+ };
5717
+ const run = this.replaceQueue.then(operation);
5718
+ this.replaceQueue = run.catch(() => void 0);
5719
+ return run;
5720
+ }
5721
+ invalidateOlderPaging() {
5722
+ this.pagingGeneration += 1;
5723
+ this.olderPending = null;
5724
+ }
5725
+ beginCommandRecovery() {
5726
+ this.commandRecoveryGeneration += 1;
5727
+ this.commandRestoreRetryAttempt = 0;
5728
+ this.bufferedCommandDeliveries ??= [];
5729
+ if (this.commandRestoreRetryHandle !== null) {
5730
+ globalThis.clearTimeout(this.commandRestoreRetryHandle);
5731
+ this.commandRestoreRetryHandle = null;
5732
+ }
5733
+ return this.commandRecoveryGeneration;
5734
+ }
5735
+ scheduleHistoricalCommandRestore(generation) {
5736
+ const restore = async () => {
5737
+ if (this.disposed || generation !== this.commandRecoveryGeneration) return;
5738
+ try {
5739
+ const commands = await this.runtime.fetchCommands(this.sessionId);
5740
+ if (this.disposed) return;
5741
+ for (const command of commands) {
5742
+ if (this.disposed) return;
5743
+ this.deliverCommandNow({
5744
+ deliveryKey: commandDeliveryKey(command.deliveryId),
5745
+ action: command.action,
5746
+ payload: command.payload,
5747
+ toolCallId: command.toolCallId
5748
+ });
5749
+ }
5750
+ if (generation !== this.commandRecoveryGeneration) return;
5751
+ this.commandRestoreRetryAttempt = 0;
5752
+ this.flushBufferedCommands(generation);
5753
+ } catch {
5754
+ if (!this.disposed && generation === this.commandRecoveryGeneration && this.commandRestoreRetryHandle === null) {
5755
+ const retryDelay = Math.min(
5756
+ COMMAND_RESTORE_RETRY_BASE_MS * 2 ** this.commandRestoreRetryAttempt,
5757
+ COMMAND_RESTORE_RETRY_MAX_MS
5758
+ );
5759
+ this.commandRestoreRetryAttempt += 1;
5760
+ this.commandRestoreRetryHandle = globalThis.setTimeout(() => {
5761
+ this.commandRestoreRetryHandle = null;
5762
+ if (!this.disposed && generation === this.commandRecoveryGeneration) {
5763
+ this.scheduleHistoricalCommandRestore(generation);
5764
+ }
5765
+ }, retryDelay);
5766
+ }
5767
+ }
5768
+ };
5769
+ const run = this.commandRestoreQueue.then(restore);
5770
+ this.commandRestoreQueue = run.catch(() => void 0);
5771
+ }
5772
+ flushBufferedCommands(generation) {
5773
+ const pending = [...this.bufferedCommandDeliveries ?? []];
5774
+ this.bufferedCommandDeliveries = [];
5775
+ let index = 0;
5776
+ while (index < pending.length) {
5777
+ if (this.disposed) return;
5778
+ if (generation !== this.commandRecoveryGeneration) {
5779
+ const currentGenerationBuffer = this.bufferedCommandDeliveries ?? [];
5780
+ this.bufferedCommandDeliveries = [
5781
+ ...pending.slice(index),
5782
+ ...currentGenerationBuffer
5783
+ ];
5784
+ return;
5785
+ }
5786
+ this.deliverCommandNow(pending[index]);
5787
+ index += 1;
5788
+ const reentrantBuffer = this.bufferedCommandDeliveries;
5789
+ if (index === pending.length && reentrantBuffer && reentrantBuffer.length > 0) {
5790
+ pending.push(...reentrantBuffer);
5791
+ this.bufferedCommandDeliveries = [];
5792
+ }
5793
+ }
5794
+ if (!this.disposed && generation === this.commandRecoveryGeneration) {
5795
+ this.bufferedCommandDeliveries = null;
5796
+ }
5797
+ }
5798
+ /** 首次订阅前的 REST 历史建立语义事件基线,随后同一工具的订阅回放不再投递。 */
5799
+ seedToolCallDeliveryBaseline(turns) {
4108
5800
  for (const turn of turns) {
4109
- for (const block of turn.blocks) {
4110
- if (block.type === "tool_bridge" && isToolBridgeContent(block.content)) {
4111
- this.dispatchCommand(turn, block);
5801
+ for (const toolCall of turn.tool_calls) {
5802
+ this.seenToolCallIds.add(toolCall.id);
5803
+ if (toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled") {
5804
+ this.finishedToolCallIds.add(toolCall.id);
4112
5805
  }
4113
5806
  }
4114
5807
  }
4115
5808
  }
4116
- ensureJoined() {
5809
+ ensureJoined(liveRevisionBoundary = this.liveRevision) {
4117
5810
  if (this.joined) return Promise.resolve();
4118
5811
  if (this.joinPending) return this.joinPending;
4119
5812
  const join = this.runtime.emitWithAck(
@@ -4121,8 +5814,7 @@ var AgentSession = class _AgentSession {
4121
5814
  {
4122
5815
  session_id: this.sessionId,
4123
5816
  event_format: "raw",
4124
- resume: true,
4125
- stream_tokens: this.runtime.streamTokens
5817
+ resume: true
4126
5818
  },
4127
5819
  5e3
4128
5820
  ).then((response) => {
@@ -4132,7 +5824,7 @@ var AgentSession = class _AgentSession {
4132
5824
  );
4133
5825
  }
4134
5826
  this.joined = true;
4135
- this._applyRunningState(Boolean(response?.is_running));
5827
+ this._applyRunningState(Boolean(response?.is_running), liveRevisionBoundary);
4136
5828
  }).finally(() => {
4137
5829
  this.joinPending = null;
4138
5830
  });
@@ -4140,6 +5832,7 @@ var AgentSession = class _AgentSession {
4140
5832
  return join;
4141
5833
  }
4142
5834
  async ensureIdleBeforeSend() {
5835
+ if (this.state.isStreaming) return;
4143
5836
  if (Date.now() - this.recentChatEndAt > 3e3) return;
4144
5837
  const deadline = Date.now() + 3e3;
4145
5838
  while (Date.now() < deadline) {
@@ -4174,6 +5867,7 @@ var SessionHub = class {
4174
5867
  connectionListeners = /* @__PURE__ */ new Set();
4175
5868
  bound = false;
4176
5869
  hasConnected = false;
5870
+ authBusyReconnect = new AuthBusyReconnect();
4177
5871
  constructor(client) {
4178
5872
  this.client = client;
4179
5873
  }
@@ -4195,16 +5889,28 @@ var SessionHub = class {
4195
5889
  * 只有最后一份归还才退订房间——否则 StrictMode 双跑、路由抖动这类"连两次
4196
5890
  * 只释放一次"的场景会把仍在用的会话退订掉,之后所有 turn/chat 事件静默丢失。
4197
5891
  */
4198
- async connect(sessionId) {
5892
+ async connect(sessionId, options = {}) {
4199
5893
  const existing = this.sessions.get(sessionId);
4200
5894
  if (existing && !existing.isDisposed) {
4201
5895
  this.hold(sessionId);
5896
+ try {
5897
+ options.setup?.(existing);
5898
+ } catch (error) {
5899
+ existing.dispose();
5900
+ throw error;
5901
+ }
4202
5902
  return this.bootstrapPending.get(sessionId) ?? existing;
4203
5903
  }
4204
5904
  this.ensureBound();
4205
5905
  const session = new AgentSession(sessionId, this.runtimeFor(), this.connection);
4206
5906
  this.sessions.set(sessionId, session);
4207
5907
  this.hold(sessionId);
5908
+ try {
5909
+ options.setup?.(session);
5910
+ } catch (error) {
5911
+ session.dispose();
5912
+ throw error;
5913
+ }
4208
5914
  this.ensureConnected();
4209
5915
  const bootstrap = session._bootstrap().then(() => session);
4210
5916
  this.bootstrapPending.set(sessionId, bootstrap);
@@ -4228,6 +5934,7 @@ var SessionHub = class {
4228
5934
  /** 手动重连(例如登录态变化后)。 */
4229
5935
  reconnect() {
4230
5936
  this.ensureBound();
5937
+ this.authBusyReconnect.cancel();
4231
5938
  const socket = this.client.socket();
4232
5939
  socket.disconnect();
4233
5940
  this.setConnection("connecting");
@@ -4235,11 +5942,13 @@ var SessionHub = class {
4235
5942
  }
4236
5943
  /** 断开连接(不清理 session 实例)。 */
4237
5944
  disconnect() {
5945
+ this.authBusyReconnect.cancel();
4238
5946
  this.client.socket().disconnect();
4239
5947
  this.setConnection("disconnected");
4240
5948
  }
4241
5949
  /** 清理当前身份下缓存的全部会话,避免切换登录身份后复用旧快照。 */
4242
5950
  disposeAll() {
5951
+ this.authBusyReconnect.cancel();
4243
5952
  this.bootstrapPending.clear();
4244
5953
  for (const session of [...this.sessions.values()]) {
4245
5954
  this.forceRelease(session);
@@ -4251,6 +5960,7 @@ var SessionHub = class {
4251
5960
  * turn:* / chat:* 事件都会静默丢弃。
4252
5961
  */
4253
5962
  _handleSocketReplaced() {
5963
+ this.authBusyReconnect.cancel();
4254
5964
  this.bound = false;
4255
5965
  this.setConnection("disconnected");
4256
5966
  if (this.sessions.size === 0) return;
@@ -4259,7 +5969,6 @@ var SessionHub = class {
4259
5969
  }
4260
5970
  runtimeFor() {
4261
5971
  return {
4262
- streamTokens: this.client.options?.streamTokens !== false,
4263
5972
  emitEvent: (event, data) => {
4264
5973
  ;
4265
5974
  this.client.socket().emit(event, data);
@@ -4275,7 +5984,9 @@ var SessionHub = class {
4275
5984
  });
4276
5985
  }),
4277
5986
  ensureConnected: () => this.ensureConnected(),
4278
- fetchTurns: (sessionId) => this.client.sessions.getSessionTurns(sessionId),
5987
+ fetchTurnsPage: (sessionId, query, init) => this.client.sessions.getSessionTurnsPage(sessionId, query, init),
5988
+ fetchTurnIndex: (sessionId) => this.client.sessions.getSessionTurnIndex(sessionId),
5989
+ fetchCommands: (sessionId) => this.client.sessions.getSessionTurnCommands(sessionId),
4279
5990
  fetchSessionInfo: (sessionId) => this.client.sessions.getSession(sessionId),
4280
5991
  updateReplay: (sessionId, payload) => this.client.sessions.updateReplaySession(sessionId, payload).then((result) => result.replay_state ?? null),
4281
5992
  listDir: (sessionId, dirPath) => this.client.sessions.listDir(sessionId, dirPath),
@@ -4303,6 +6014,9 @@ var SessionHub = class {
4303
6014
  if (this.sessions.get(session.sessionId) === session) {
4304
6015
  this.sessions.delete(session.sessionId);
4305
6016
  }
6017
+ if (this.sessions.size === 0) {
6018
+ this.authBusyReconnect.cancel();
6019
+ }
4306
6020
  ;
4307
6021
  this.client.socket().emit("session:unsubscribe", { session_id: session.sessionId });
4308
6022
  return true;
@@ -4311,10 +6025,13 @@ var SessionHub = class {
4311
6025
  forceRelease(session) {
4312
6026
  this.holdCounts.delete(session.sessionId);
4313
6027
  session.dispose();
6028
+ if (this.sessions.size === 0) {
6029
+ this.authBusyReconnect.cancel();
6030
+ }
4314
6031
  }
4315
6032
  ensureConnected() {
4316
6033
  const socket = this.client.socket();
4317
- if (socket.connected || socket.active) return;
6034
+ if (socket.connected || socket.active || this.authBusyReconnect.pending) return;
4318
6035
  this.setConnection("connecting");
4319
6036
  socket.connect();
4320
6037
  }
@@ -4346,17 +6063,35 @@ var SessionHub = class {
4346
6063
  this.bound = true;
4347
6064
  const socket = this.client.socket();
4348
6065
  socket.on("connect", () => {
6066
+ this.authBusyReconnect.cancel();
4349
6067
  const isReconnect = this.hasConnected;
4350
6068
  this.hasConnected = true;
4351
6069
  this.setConnection("connected");
4352
6070
  if (isReconnect) {
4353
6071
  for (const session of this.sessions.values()) {
4354
6072
  void session._rejoinAfterReconnect();
6073
+ void session.refreshQueue();
4355
6074
  }
4356
6075
  }
4357
6076
  });
4358
- socket.on("connect_error", () => this.setConnection("disconnected"));
4359
- socket.on("disconnect", () => this.setConnection("disconnected"));
6077
+ socket.on("connect_error", (error) => {
6078
+ this.setConnection("disconnected");
6079
+ if (this.sessions.size === 0) {
6080
+ this.authBusyReconnect.cancel();
6081
+ return;
6082
+ }
6083
+ if (this.authBusyReconnect.schedule(error, () => {
6084
+ const current = this.client.socket();
6085
+ if (current !== socket || current.connected || current.active) return;
6086
+ this.setConnection("connecting");
6087
+ current.connect();
6088
+ })) return;
6089
+ this.authBusyReconnect.cancel();
6090
+ });
6091
+ socket.on("disconnect", () => {
6092
+ this.authBusyReconnect.cancel();
6093
+ this.setConnection("disconnected");
6094
+ });
4360
6095
  socket.io.on("reconnect_attempt", () => this.setConnection("reconnecting"));
4361
6096
  socket.on("turn:start", (data) => {
4362
6097
  for (const session of this.route(data.session_id)) session._handleTurnStart(data);
@@ -4367,9 +6102,25 @@ var SessionHub = class {
4367
6102
  socket.on("turn:end", (data) => {
4368
6103
  for (const session of this.route(data.session_id)) session._handleTurnEnd(data);
4369
6104
  });
4370
- socket.on("turn:events", (data) => {
4371
- for (const session of this.route(data.session_id)) session._handleTurnEvents(data.events ?? []);
4372
- });
6105
+ socket.on(
6106
+ "turn:timing",
6107
+ (data) => {
6108
+ for (const session of this.route(data.session_id)) {
6109
+ session._handleTurnEvents([
6110
+ { type: "llm:first_token:delivered", payload: data }
6111
+ ]);
6112
+ }
6113
+ }
6114
+ );
6115
+ socket.on(
6116
+ "turn:events",
6117
+ (data, acknowledge) => {
6118
+ acknowledgeTurnEvents(acknowledge);
6119
+ for (const session of this.route(data.session_id)) {
6120
+ session._handleTurnEvents(data.events ?? [], data.replayed === true);
6121
+ }
6122
+ }
6123
+ );
4373
6124
  socket.on("chat:start", (data) => {
4374
6125
  for (const session of this.route(data?.session_id)) session._handleChatStart();
4375
6126
  });
@@ -4381,6 +6132,9 @@ var SessionHub = class {
4381
6132
  socket.on("system:error", (data) => {
4382
6133
  for (const session of this.route(data.session_id)) session._handleSystemError(data.message);
4383
6134
  });
6135
+ socket.on("chat:queue:updated", (data) => {
6136
+ for (const session of this.route(data?.session_id)) session._handleQueueUpdated(data);
6137
+ });
4384
6138
  socket.on(
4385
6139
  "system:notification",
4386
6140
  (data) => {
@@ -4497,21 +6251,14 @@ function resolveAuthToken(options) {
4497
6251
  return token ? token : null;
4498
6252
  }
4499
6253
 
4500
- // src/version.ts
4501
- var SDK_NAME = "agent-client";
4502
- var SDK_VERSION = true ? "2610.0.0-beta.7" : "1.1.1";
4503
-
4504
6254
  // src/socket.ts
4505
- function withSdkIdentity(auth) {
4506
- return { ...auth ?? {}, sdk: SDK_NAME, sdk_version: SDK_VERSION };
4507
- }
4508
6255
  function createSocket(options) {
4509
6256
  const token = resolveAuthToken(options);
4510
6257
  return io(options.baseUrl, {
4511
6258
  path: options.path ?? "/socket.io",
4512
6259
  withCredentials: true,
4513
6260
  query: token ? { token } : void 0,
4514
- auth: typeof options.token === "function" ? (cb) => cb(withSdkIdentity(buildSocketAuth(options))) : withSdkIdentity(buildSocketAuth(options)),
6261
+ auth: typeof options.token === "function" ? (cb) => cb(buildSocketAuth(options) ?? {}) : buildSocketAuth(options) ?? {},
4515
6262
  autoConnect: false
4516
6263
  });
4517
6264
  }
@@ -4527,8 +6274,17 @@ var BladeClient = class {
4527
6274
  options;
4528
6275
  auth;
4529
6276
  headless;
6277
+ computers;
4530
6278
  models;
4531
6279
  sessions;
6280
+ /** 按用户读取的插件目录与图标;没有会话时也能用。 */
6281
+ plugins;
6282
+ chatGroups;
6283
+ /**
6284
+ * 全站访问统计:匿名读写,不带 bearer 也不触发登录刷新。
6285
+ * 只有内置 Web 的同源页面使用,第三方宿主页面不会被自动统计。
6286
+ */
6287
+ siteVisits;
4532
6288
  /** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
4533
6289
  hub;
4534
6290
  constructor(options) {
@@ -4538,8 +6294,12 @@ var BladeClient = class {
4538
6294
  };
4539
6295
  this.auth = new AuthResource(this);
4540
6296
  this.headless = new HeadlessResource(this);
6297
+ this.computers = new ComputersResource(this);
4541
6298
  this.models = new ModelsResource(this);
4542
6299
  this.sessions = new SessionsResource(this);
6300
+ this.plugins = new PluginsResource(this);
6301
+ this.chatGroups = new ChatGroupsResource(this);
6302
+ this.siteVisits = new SiteVisitsResource(this);
4543
6303
  this.hub = new SessionHub(this);
4544
6304
  if (this.options.token === void 0) {
4545
6305
  this.runtimeToken = readStoredToken(this.options.baseUrl);
@@ -4698,7 +6458,7 @@ var BladeClient = class {
4698
6458
  signal: init.signal
4699
6459
  });
4700
6460
  if (response.status === 401 && !isRetry && this.shouldRefreshFor401(url)) {
4701
- const refreshed = await this.tryRefresh();
6461
+ const refreshed = await this.tryRefreshUnlessAborted(init.signal);
4702
6462
  if (refreshed) {
4703
6463
  return this.fetch(method, path, init, true);
4704
6464
  }
@@ -4710,7 +6470,7 @@ var BladeClient = class {
4710
6470
  return response;
4711
6471
  }
4712
6472
  buildAuthedUrl(path) {
4713
- const url = new URL(this.toBaseRelativePath(path), this.baseUrlWithTrailingSlash());
6473
+ const url = buildBackendUrl(this.options.baseUrl, path);
4714
6474
  const token = this.resolveTokenForUrl(url);
4715
6475
  if (token) {
4716
6476
  url.searchParams.set("token", token);
@@ -4729,14 +6489,11 @@ var BladeClient = class {
4729
6489
  return result;
4730
6490
  }
4731
6491
  buildUrl(path) {
4732
- return new URL(this.toBaseRelativePath(path), this.baseUrlWithTrailingSlash());
6492
+ return buildBackendUrl(this.options.baseUrl, path);
4733
6493
  }
4734
6494
  baseUrlWithTrailingSlash() {
4735
6495
  return this.options.baseUrl.endsWith("/") ? this.options.baseUrl : `${this.options.baseUrl}/`;
4736
6496
  }
4737
- toBaseRelativePath(path) {
4738
- return path.startsWith("/") ? path.slice(1) : path;
4739
- }
4740
6497
  isSameBackendUrl(url) {
4741
6498
  const base = new URL(this.baseUrlWithTrailingSlash());
4742
6499
  if (url.origin !== base.origin) {
@@ -4755,6 +6512,27 @@ var BladeClient = class {
4755
6512
  hasExplicitBearerToken() {
4756
6513
  return Boolean(this.resolveRestToken());
4757
6514
  }
6515
+ /**
6516
+ * 等一次令牌刷新,但调用方中止请求时不再等下去。
6517
+ *
6518
+ * 刷新本身是共享的(`refreshPromise`,别的请求也在等它),这里只是让这次已被取消的
6519
+ * 请求尽早结束——否则它会一直挂在调用方(定位)的串行队列里,用户改选的新目标要等到
6520
+ * 刷新结束才轮得到。
6521
+ */
6522
+ async tryRefreshUnlessAborted(signal) {
6523
+ if (!signal) return this.tryRefresh();
6524
+ if (signal.aborted) throw new DOMException("The operation was aborted.", "AbortError");
6525
+ let onAbort;
6526
+ const aborted = new Promise((_resolve, reject) => {
6527
+ onAbort = () => reject(new DOMException("The operation was aborted.", "AbortError"));
6528
+ signal.addEventListener("abort", onAbort, { once: true });
6529
+ });
6530
+ try {
6531
+ return await Promise.race([this.tryRefresh(), aborted]);
6532
+ } finally {
6533
+ if (onAbort) signal.removeEventListener("abort", onAbort);
6534
+ }
6535
+ }
4758
6536
  async tryRefresh() {
4759
6537
  if (this.refreshPromise) {
4760
6538
  return this.refreshPromise;
@@ -4892,6 +6670,126 @@ function parseXhrHeaders(rawHeaders) {
4892
6670
  return headers;
4893
6671
  }
4894
6672
 
6673
+ // src/polling-backoff.ts
6674
+ var MAX_BACKOFF_MS = 6e4;
6675
+ var MAX_TIMER_MS = 2147483647;
6676
+ var PollingBackoff = class {
6677
+ constructor(intervalMs) {
6678
+ this.intervalMs = intervalMs;
6679
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0 || intervalMs > MAX_BACKOFF_MS) {
6680
+ throw new RangeError("Polling interval must be between 0 and 60000 ms");
6681
+ }
6682
+ }
6683
+ intervalMs;
6684
+ failures = 0;
6685
+ nextAt = 0;
6686
+ reset() {
6687
+ this.failures = 0;
6688
+ this.nextAt = 0;
6689
+ }
6690
+ failed(error) {
6691
+ if (error instanceof Error && error.name === "AbortError" || error instanceof BladeApiError && error.status < 500 && error.status !== 408 && error.status !== 429) {
6692
+ this.nextAt = false;
6693
+ return;
6694
+ }
6695
+ this.failures = Math.min(this.failures + 1, 10);
6696
+ const ceiling = Math.min(MAX_BACKOFF_MS, this.intervalMs * 2 ** this.failures);
6697
+ const backoff = Math.max(this.intervalMs, ceiling * (0.5 + Math.random() * 0.5));
6698
+ const header = error instanceof BladeApiError ? error.response.headers.get("Retry-After")?.trim() : void 0;
6699
+ const retryAfter = header ? /^\d+$/.test(header) ? Number(header) * 1e3 : Date.parse(header) - Date.now() : 0;
6700
+ const delay = Math.max(backoff, Number.isNaN(retryAfter) ? 0 : retryAfter);
6701
+ this.nextAt = delay > MAX_TIMER_MS ? false : Date.now() + delay;
6702
+ }
6703
+ /** Zero means ready, false means stop, otherwise wait at least this many ms. */
6704
+ waitMs() {
6705
+ return this.nextAt === false ? false : Math.max(0, this.nextAt - Date.now());
6706
+ }
6707
+ };
6708
+
6709
+ // src/platform-endpoints.ts
6710
+ var PLATFORM_SERVICE_NAMES = [
6711
+ "os",
6712
+ "hub",
6713
+ "agent",
6714
+ "oauth",
6715
+ "llmGateway",
6716
+ "gitea",
6717
+ "tile"
6718
+ ];
6719
+ var EMPTY_PLATFORM_ENDPOINTS = Object.freeze({
6720
+ services: Object.freeze({})
6721
+ });
6722
+ function defaultDocumentBaseUrl() {
6723
+ if (typeof document === "undefined") return void 0;
6724
+ if (document.querySelector("base[href]")) return document.baseURI;
6725
+ if (typeof location !== "undefined" && location.origin) return `${location.origin}/`;
6726
+ return document.baseURI;
6727
+ }
6728
+ function normalizeServiceUrl(value) {
6729
+ if (typeof value !== "string" || !value.trim()) return null;
6730
+ if (value.includes("?") || value.includes("#")) return null;
6731
+ try {
6732
+ const url = new URL(value.trim());
6733
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
6734
+ if (url.username || url.password || url.search || url.hash) return null;
6735
+ return url.toString().replace(/\/$/, "");
6736
+ } catch {
6737
+ return null;
6738
+ }
6739
+ }
6740
+ function parsePlatformEndpoints(value) {
6741
+ if (!isRecord(value) || !isRecord(value.services)) return null;
6742
+ const services = {};
6743
+ for (const name of PLATFORM_SERVICE_NAMES) {
6744
+ if (!(name in value.services)) continue;
6745
+ const url = normalizeServiceUrl(value.services[name]);
6746
+ if (!url) return null;
6747
+ if (name === "agent" && new URL(url).pathname !== "/") return null;
6748
+ services[name] = url;
6749
+ }
6750
+ return { services };
6751
+ }
6752
+ async function loadPlatformEndpoints(options = {}) {
6753
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
6754
+ const baseUrl = options.baseUrl ?? defaultDocumentBaseUrl();
6755
+ if (!fetchImpl || !baseUrl) return EMPTY_PLATFORM_ENDPOINTS;
6756
+ const controller = new AbortController();
6757
+ const timeout = globalThis.setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
6758
+ try {
6759
+ const response = await fetchImpl(new URL("config.json", baseUrl), {
6760
+ cache: "no-store",
6761
+ credentials: "same-origin",
6762
+ headers: { Accept: "application/json" },
6763
+ signal: controller.signal
6764
+ });
6765
+ if (!response.ok) return EMPTY_PLATFORM_ENDPOINTS;
6766
+ return parsePlatformEndpoints(await response.json()) ?? EMPTY_PLATFORM_ENDPOINTS;
6767
+ } catch {
6768
+ return EMPTY_PLATFORM_ENDPOINTS;
6769
+ } finally {
6770
+ globalThis.clearTimeout(timeout);
6771
+ }
6772
+ }
6773
+ function resolveServiceUrl(endpoints, name, path = "") {
6774
+ const baseUrl = endpoints.services[name];
6775
+ if (!baseUrl) return null;
6776
+ if (!path) return baseUrl;
6777
+ const pathReference = path.trimStart();
6778
+ if (path.includes("\\") || pathReference.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(pathReference)) {
6779
+ return null;
6780
+ }
6781
+ const base = new URL(`${baseUrl}/`);
6782
+ const target = new URL(path.replace(/^\/+/, ""), base);
6783
+ if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) {
6784
+ return null;
6785
+ }
6786
+ return target.toString();
6787
+ }
6788
+
6789
+ // src/version.ts
6790
+ var SDK_NAME = "agent-client";
6791
+ var SDK_VERSION = true ? "2610.0.0-beta.71" : "1.1.1";
6792
+
4895
6793
  // src/commands/protocol.ts
4896
6794
  function isCommandEnvelope(value) {
4897
6795
  if (typeof value !== "object" || value === null) return false;
@@ -4992,12 +6890,19 @@ var Task = type2({
4992
6890
  });
4993
6891
  export {
4994
6892
  AgentSession,
6893
+ AuthBusyReconnect,
4995
6894
  BladeApiError,
4996
6895
  BladeClient,
6896
+ ChatGroupsResource,
4997
6897
  ClientProjectionBuilder,
6898
+ ComputersResource,
4998
6899
  DEFAULT_REPLAY_SPEED,
6900
+ EMPTY_PLATFORM_ENDPOINTS,
6901
+ EMPTY_SESSION_QUEUE,
4999
6902
  LayoutType,
5000
6903
  ModelsResource,
6904
+ PluginsResource,
6905
+ PollingBackoff,
5001
6906
  SDK_NAME,
5002
6907
  SDK_VERSION,
5003
6908
  SessionHub,
@@ -5007,22 +6912,59 @@ export {
5007
6912
  Task,
5008
6913
  TaskStatus,
5009
6914
  acceptedPostChatFollowupCompletesLatestRun,
6915
+ acknowledgeTurnEvents,
6916
+ authBusyRetryDelayMs,
5010
6917
  buildMessageContent,
6918
+ buildToolUiCardKey,
6919
+ canCancelQueuedMessage,
6920
+ canDeliverQueuedMessage,
6921
+ canEditQueuedMessage,
6922
+ canToggleComputer,
6923
+ chatErrorForDisplay,
6924
+ collectInlineToolUiCards,
6925
+ collectPreviewToolUiCards,
6926
+ computerDaemonVersion,
6927
+ computerOS,
6928
+ computerPlatformLabel,
6929
+ computerState,
5011
6930
  connectEmbedded,
5012
6931
  contentPreview,
6932
+ contextProjectionData,
5013
6933
  createInitialSessionState,
5014
6934
  createSocket,
5015
6935
  extractTextAttachments,
6936
+ getContextDisplayState,
6937
+ getContextGroupDisplayState,
5016
6938
  getFileParts,
5017
6939
  getImageParts,
5018
6940
  getTextContent,
6941
+ groupAdjacentContextRuns,
5019
6942
  groupMessagesByLoop,
6943
+ hasChatRunEvent,
6944
+ inferToolStatus,
6945
+ isAppDevToolUiCard,
5020
6946
  isCommandEnvelope,
6947
+ isEntryGoneError,
5021
6948
  isHiddenInternalMessage,
5022
6949
  isInboundEnvelope,
6950
+ isInternalStatusToolUiCard,
6951
+ isMcpAppData,
6952
+ isSessionQueueSnapshot,
6953
+ isToolUiCard,
5023
6954
  latestPostChatFollowup,
6955
+ loadPlatformEndpoints,
5024
6956
  normalizeMessageContent,
6957
+ parseQueueAck,
6958
+ prependOlder,
6959
+ queuePauseReasonLabel,
6960
+ queuedMessageStatusLabel,
6961
+ reconcileHistoricalUserTurns,
5025
6962
  reconcileOptimisticUserTurns,
6963
+ replaceWindow,
6964
+ resolveServiceUrl,
6965
+ resolveToolUiCardContent,
6966
+ shouldApplySnapshot,
6967
+ sortComputers,
5026
6968
  toReplaySnapshot,
5027
6969
  transformSlashCommand
5028
6970
  };