@blade-hq/agent-client 2610.0.1-alpha.1 → 2610.0.2

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 (44) hide show
  1. package/README.md +281 -11
  2. package/dist/auth.d.ts +0 -1
  3. package/dist/blade-client.d.ts +24 -1
  4. package/dist/index.d.ts +38 -7
  5. package/dist/index.js +2520 -283
  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 +31 -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 +238 -2
  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 +15 -1
  20. package/dist/schemas/message.d.ts +8 -0
  21. package/dist/schemas/projection-utils.d.ts +3 -0
  22. package/dist/schemas/projection.d.ts +53 -2
  23. package/dist/schemas/session.d.ts +10 -1
  24. package/dist/session/agent-session.d.ts +160 -16
  25. package/dist/session/definition.d.ts +6 -1
  26. package/dist/session/errors.d.ts +11 -0
  27. package/dist/session/events.d.ts +16 -2
  28. package/dist/session/history-page.d.ts +9 -0
  29. package/dist/session/hub.d.ts +9 -1
  30. package/dist/session/queue.d.ts +104 -0
  31. package/dist/session/state.d.ts +19 -2
  32. package/dist/shared/auth-busy.d.ts +9 -0
  33. package/dist/shared/projection/builder.d.ts +17 -0
  34. package/dist/shared/projection/context.d.ts +3 -0
  35. package/dist/shared/projection/helpers.d.ts +4 -2
  36. package/dist/shared/projection/history.d.ts +1 -0
  37. package/dist/shared/projection/index.d.ts +2 -1
  38. package/dist/shared/projection/state.d.ts +7 -0
  39. package/dist/shared/tool-ui-card.d.ts +60 -0
  40. package/dist/shared/type-guards.d.ts +2 -0
  41. package/dist/socket.d.ts +2 -0
  42. package/dist/types/socket-events.d.ts +201 -2
  43. package/package.json +4 -2
  44. package/public-api.md +1071 -23
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ // src/version.ts
2
+ var SDK_NAME = "agent-client";
3
+ var SDK_VERSION = true ? "2610.0.2" : "1.1.1";
4
+
1
5
  // src/auth-login.ts
2
6
  function isAuthCallbackMessage(value) {
3
7
  return typeof value === "object" && value !== null && value.type === "blade-agent:sdk-auth";
@@ -167,6 +171,10 @@ function decorateAuthError(error) {
167
171
  }
168
172
  return error;
169
173
  }
174
+ function buildBackendUrl(baseUrl, path) {
175
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
176
+ return new URL(path.startsWith("/") ? path.slice(1) : path, base);
177
+ }
170
178
 
171
179
  // src/resources/auth.ts
172
180
  var AuthResource = class {
@@ -214,6 +222,11 @@ var AuthResource = class {
214
222
  }
215
223
  };
216
224
 
225
+ // src/shared/type-guards.ts
226
+ function isRecord(value) {
227
+ return typeof value === "object" && value !== null && !Array.isArray(value);
228
+ }
229
+
217
230
  // src/resources/headless.ts
218
231
  var HeadlessError = class extends Error {
219
232
  constructor(message, detail) {
@@ -350,7 +363,10 @@ var HeadlessResource = class {
350
363
  socket.on("chat:start", onChatStart);
351
364
  socket.on("chat:end", onChatEnd);
352
365
  socket.on("system:error", onSystemError);
353
- socket.emit("session:subscribe", { session_id, event_format: "raw" });
366
+ socket.emit("session:subscribe", {
367
+ session_id,
368
+ event_format: "raw"
369
+ });
354
370
  socket.emit(
355
371
  "chat:send",
356
372
  {
@@ -391,9 +407,6 @@ function errorMessageFromResult(result) {
391
407
  }
392
408
  return null;
393
409
  }
394
- function isRecord(value) {
395
- return typeof value === "object" && value !== null && !Array.isArray(value);
396
- }
397
410
  function ensureSocketConnected(socket, timeoutMs, detail) {
398
411
  if (socket.connected) {
399
412
  return Promise.resolve();
@@ -421,6 +434,81 @@ function ensureSocketConnected(socket, timeoutMs, detail) {
421
434
  });
422
435
  }
423
436
 
437
+ // src/resources/computers.ts
438
+ function computerState(computer) {
439
+ if (computer.is_primary) return "primary";
440
+ if (computer.enabled) return "enabled";
441
+ return computer.online ? "idle" : "offline";
442
+ }
443
+ function computerOS(computer) {
444
+ const os = (computer.os ?? "").toLowerCase();
445
+ if (os.startsWith("darwin") || os.startsWith("mac")) return "macos";
446
+ if (os.startsWith("windows")) return "windows";
447
+ if (os.startsWith("linux")) return "linux";
448
+ return "unknown";
449
+ }
450
+ function computerPlatformLabel(computer) {
451
+ const os = (computer.os ?? "").trim();
452
+ const arch = (computer.arch ?? "").trim();
453
+ if (os && arch) return `${os}/${arch}`;
454
+ return os || arch || "";
455
+ }
456
+ function computerDaemonVersion(computer) {
457
+ return (computer.daemon_version ?? "").trim();
458
+ }
459
+ function canToggleComputer(computer) {
460
+ return !computer.is_primary;
461
+ }
462
+ function sortComputers(computers) {
463
+ return [...computers].sort((a, b) => {
464
+ const byCreated = (a.created_at ?? "").localeCompare(b.created_at ?? "");
465
+ if (byCreated !== 0) return byCreated;
466
+ return a.label.localeCompare(b.label);
467
+ });
468
+ }
469
+ var ComputersResource = class {
470
+ constructor(client) {
471
+ this.client = client;
472
+ }
473
+ client;
474
+ /**
475
+ * 列出当前账号登记过的全部电脑及其在线状态。
476
+ *
477
+ * 首页还没有会话,也要能看到同一份清单——判定各写一份必然分叉,一边的兜底会把
478
+ * 另一边的错误盖住(#1597)。所以账号事实只从这一个接口读。
479
+ */
480
+ async listAccount() {
481
+ const data = await this.client.json("GET", "/api/daemon/instances");
482
+ return {
483
+ computers: (data.instances ?? []).map((instance) => ({
484
+ ...instance,
485
+ online: Boolean(instance.online)
486
+ }))
487
+ };
488
+ }
489
+ /**
490
+ * 列出当前用户的全部电脑,带上"在不在线"和"这次会话能不能用"。
491
+ * 服务端只查库,电脑离线时同样返回,所以拿得到"它离线了"这个结论。
492
+ */
493
+ list(sessionId) {
494
+ return this.client.json(
495
+ "GET",
496
+ `/api/sessions/${encodeURIComponent(sessionId)}/computers`
497
+ );
498
+ }
499
+ /**
500
+ * 启动或停止一台电脑。只改这次会话的可用范围,不会碰远程机器,
501
+ * 电脑离线时同样能登记,等它上线就直接可用。
502
+ */
503
+ setEnabled(sessionId, computer, enabled) {
504
+ return this.client.json(
505
+ "PUT",
506
+ `/api/sessions/${encodeURIComponent(sessionId)}/computers/${encodeURIComponent(computer)}/enabled`,
507
+ { enabled }
508
+ );
509
+ }
510
+ };
511
+
424
512
  // src/resources/models.ts
425
513
  var ModelsResource = class {
426
514
  constructor(client) {
@@ -447,6 +535,7 @@ var SessionInfo = type({
447
535
  "shared?": "boolean",
448
536
  "memory_enabled?": "boolean",
449
537
  "is_persistent?": "boolean",
538
+ "initiator?": "'ui' | 'sdk' | 'unknown' | null",
450
539
  "is_headless?": "boolean",
451
540
  "viewer_role?": "'owner' | 'viewer'",
452
541
  "template_id?": "string | null",
@@ -469,7 +558,11 @@ var SessionInfo = type({
469
558
  "disable_tools?": "string[]",
470
559
  "runtime_type?": "string | null",
471
560
  "daemon_id?": "string | null",
561
+ "agent_runtime_id?": "string | null",
472
562
  "workspace_path?": "string | null",
563
+ "ba_version?": "string | null",
564
+ "sandbox_version?": "string | null",
565
+ "chat_group_id?": "string | null",
473
566
  "match?": "unknown"
474
567
  });
475
568
 
@@ -535,7 +628,7 @@ function upsertToolCall(state, toolCallId, toolName, displayName, args) {
535
628
  });
536
629
  }
537
630
  }
538
- function applyToolResult(state, toolCallId, result, status, durationMs, sourceLoop) {
631
+ function applyToolResult(state, toolCallId, result, status, durationMs, ranInParallel, sourceLoop, startedAt = null) {
539
632
  let toolName = "";
540
633
  let displayName = "";
541
634
  let finalResult = result;
@@ -547,6 +640,8 @@ function applyToolResult(state, toolCallId, result, status, durationMs, sourceLo
547
640
  }
548
641
  const update = { status, result: finalResult };
549
642
  if (durationMs != null) update.duration_ms = durationMs;
643
+ if (ranInParallel != null) update.ran_in_parallel = ranInParallel;
644
+ if (startedAt != null) update.started_at = startedAt;
550
645
  state.toolCalls[i] = { ...tc, ...update };
551
646
  toolName = tc.tool_name;
552
647
  displayName = tc.display_name;
@@ -728,11 +823,17 @@ function buildCompactionTurn(sequence, compactionId, loopId, status, data) {
728
823
  summary_preview: summaryPreview,
729
824
  summary_full: summaryFull,
730
825
  archived_count: archivedCount,
826
+ archived_files: Array.isArray(data.archived_files) ? data.archived_files.filter(
827
+ (item) => typeof item === "object" && item !== null && !Array.isArray(item)
828
+ ) : null,
829
+ archived_tool_calls: Array.isArray(data.archived_tool_calls) ? data.archived_tool_calls.filter(
830
+ (item) => typeof item === "object" && item !== null && !Array.isArray(item)
831
+ ) : null,
731
832
  tokens_before: tokensBefore,
732
833
  tokens_after: tokensAfter,
733
834
  saved_ratio: savedRatio,
734
835
  trigger: asTrigger(data.trigger),
735
- failure_reason: typeof data.reason === "string" && data.reason ? data.reason : null,
836
+ failure_reason: [data.failure_reason, data.fallback_reason, data.reason].find((value) => typeof value === "string" && value.length > 0) ?? null,
736
837
  fallback_applied: typeof data.fallback_applied === "boolean" ? data.fallback_applied : null
737
838
  };
738
839
  }
@@ -893,7 +994,7 @@ function sourceLoopFor(loopId, loopDescriptions) {
893
994
  if (!description) return null;
894
995
  return { loop_name: loopId, description };
895
996
  }
896
- var MAX_FINAL_ARTIFACTS = 3;
997
+ var MAX_FINAL_ARTIFACTS = 20;
897
998
  function normalizeFinalArtifacts(raw) {
898
999
  if (!Array.isArray(raw)) return [];
899
1000
  const artifacts = [];
@@ -927,12 +1028,16 @@ function normalizePostChatFollowup(data, assistantEntryId) {
927
1028
  }
928
1029
  const recaption = String(data.recaption ?? "");
929
1030
  const finalArtifacts = normalizeFinalArtifacts(data.final_artifacts);
930
- if (suggestions.length === 0 && !recaption && finalArtifacts.length === 0) return null;
1031
+ const feedbackEligible = data.feedback_eligible === true;
1032
+ if (suggestions.length === 0 && !recaption && finalArtifacts.length === 0 && !feedbackEligible)
1033
+ return null;
931
1034
  return {
932
1035
  assistant_entry_id: assistantEntryId,
933
1036
  recaption,
934
1037
  suggestions: suggestions.slice(0, 3),
935
- final_artifacts: finalArtifacts
1038
+ final_artifacts: finalArtifacts,
1039
+ feedback_eligible: feedbackEligible,
1040
+ feedback_scope: "latest_result"
936
1041
  };
937
1042
  }
938
1043
  function normalizeChildPause(pausePayload) {
@@ -983,6 +1088,12 @@ function createTurnState(turnId, loopId, opts = {}) {
983
1088
  toolCalls: [],
984
1089
  usage: null,
985
1090
  responseDone: false,
1091
+ firstTokenMs: null,
1092
+ streamDurationMs: null,
1093
+ firstTokenDeliveryMs: null,
1094
+ modelStartedAt: null,
1095
+ modelDurationMs: null,
1096
+ modelStartedAtReported: false,
986
1097
  memoryRefs: opts.memoryRefs ?? null,
987
1098
  parentForkToolCallId: null
988
1099
  };
@@ -1000,8 +1111,13 @@ function snapshot(state, sequence, status) {
1000
1111
  tool_calls: [...state.toolCalls],
1001
1112
  model: state.model,
1002
1113
  usage: state.usage ? { ...state.usage } : null,
1003
- duration_ms: Math.max(0, Date.now() - state.startedAt),
1004
- started_at: new Date(state.startedAt).toISOString(),
1114
+ duration_ms: state.modelDurationMs,
1115
+ first_token_ms: state.firstTokenMs,
1116
+ stream_duration_ms: state.streamDurationMs,
1117
+ first_token_delivery_ms: state.firstTokenDeliveryMs,
1118
+ started_at: state.modelStartedAt ?? new Date(state.startedAt).toISOString(),
1119
+ // 没有 payload 上报时用的是 loop:turn 的浏览器事件时间,标记出来供瀑布区分。
1120
+ has_reported_timing: state.modelStartedAtReported,
1005
1121
  context_window: state.contextWindow,
1006
1122
  memory_refs: state.memoryRefs,
1007
1123
  parent_fork_tool_call_id: state.parentForkToolCallId
@@ -1015,17 +1131,219 @@ function toolStatusFromResult(result) {
1015
1131
  return inferToolStatus(text);
1016
1132
  }
1017
1133
 
1134
+ // src/schemas/context.ts
1135
+ var BUILTIN_CONTEXT_TITLES = {
1136
+ "agent-instructions": "\u9879\u76EE\u8BF4\u660E",
1137
+ "skill-catalog": "\u53EF\u7528\u80FD\u529B",
1138
+ "runtime-context": "\u5F53\u524D\u5DE5\u4F5C\u73AF\u5883",
1139
+ "workspace-context": "\u5F53\u524D\u5DE5\u4F5C\u533A\u73AF\u5883"
1140
+ };
1141
+ var BUILTIN_CONTEXT_DETAILS = {
1142
+ "agent-instructions": "\u8FD9\u4E9B\u8BF4\u660E\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u7406\u89E3\u5F53\u524D\u9879\u76EE\u548C\u5DE5\u4F5C\u8981\u6C42\u3002",
1143
+ "skill-catalog": "\u8FD9\u4E9B\u80FD\u529B\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u9009\u62E9\u5408\u9002\u7684\u65B9\u5F0F\u5B8C\u6210\u4EFB\u52A1\u3002",
1144
+ "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",
1145
+ "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"
1146
+ };
1147
+ function nonEmptyString(value) {
1148
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1149
+ }
1150
+ function contextProjectionData(fields) {
1151
+ const contextKind = nonEmptyString(fields.context_kind);
1152
+ const contextKey = nonEmptyString(fields.context_key);
1153
+ const contextName = nonEmptyString(fields.context_name);
1154
+ const contextRevision = nonEmptyString(fields.context_revision);
1155
+ if (!contextKind || !contextKey || !contextName || !contextRevision || typeof fields.context_order !== "number" || !Number.isFinite(fields.context_order) || !fields.context_action) {
1156
+ return null;
1157
+ }
1158
+ return {
1159
+ context_kind: contextKind,
1160
+ context_key: contextKey,
1161
+ context_name: contextName,
1162
+ context_order: fields.context_order,
1163
+ context_action: fields.context_action,
1164
+ context_revision: contextRevision,
1165
+ display_title: nonEmptyString(fields.display_title),
1166
+ display_summary: nonEmptyString(fields.display_summary),
1167
+ sources: Array.isArray(fields.sources) ? fields.sources : []
1168
+ };
1169
+ }
1170
+ function getContextDisplayState(context) {
1171
+ const title = nonEmptyString(context.display_title) ?? BUILTIN_CONTEXT_TITLES[context.context_kind] ?? "\u5BF9\u8BDD\u80CC\u666F";
1172
+ const isRemoved = context.context_action === "removed";
1173
+ 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");
1174
+ 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";
1175
+ return { title, summary, detail, isRemoved };
1176
+ }
1177
+ function groupAdjacentContextRuns(kinds) {
1178
+ const runs = [];
1179
+ let current = null;
1180
+ kinds.forEach((kind, index) => {
1181
+ if (kind === null) {
1182
+ current = null;
1183
+ return;
1184
+ }
1185
+ if (current === null) {
1186
+ current = [];
1187
+ runs.push(current);
1188
+ }
1189
+ current.push(index);
1190
+ });
1191
+ return runs;
1192
+ }
1193
+ function getContextGroupDisplayState(contexts) {
1194
+ const count = contexts.length;
1195
+ if (count === 1) {
1196
+ const single = getContextDisplayState(contexts[0]);
1197
+ return { title: single.title, summary: single.summary, count };
1198
+ }
1199
+ const removed = contexts.filter(
1200
+ (context) => context.context_action === "removed"
1201
+ ).length;
1202
+ const updated = count - removed;
1203
+ const parts = [];
1204
+ if (updated > 0) parts.push(`${updated} \u9879\u5DF2\u66F4\u65B0`);
1205
+ if (removed > 0) parts.push(`${removed} \u9879\u5DF2\u505C\u7528`);
1206
+ return {
1207
+ title: "\u5BF9\u8BDD\u80CC\u666F",
1208
+ summary: parts.join(" \xB7 ") || `${count} \u9879`,
1209
+ count
1210
+ };
1211
+ }
1212
+
1213
+ // src/shared/projection/context.ts
1214
+ function buildContextTurn(entry, sequence) {
1215
+ if (entry.kind !== "context" || !isRecord(entry.source) || !isRecord(entry.data)) {
1216
+ return null;
1217
+ }
1218
+ const display = isRecord(entry.data.display) ? entry.data.display : {};
1219
+ const message = isRecord(entry.message) ? entry.message : {};
1220
+ const context = contextProjectionData({
1221
+ context_kind: typeof entry.source.kind === "string" ? entry.source.kind : null,
1222
+ context_key: typeof entry.source.key === "string" ? entry.source.key : null,
1223
+ context_name: typeof entry.data.contribution_name === "string" ? entry.data.contribution_name : null,
1224
+ context_order: typeof entry.data.order === "number" ? entry.data.order : null,
1225
+ context_action: entry.data.action === "published" || entry.data.action === "retained" || entry.data.action === "removed" ? entry.data.action : null,
1226
+ context_revision: typeof entry.data.revision === "string" ? entry.data.revision : null,
1227
+ display_title: typeof display.title === "string" ? display.title : null,
1228
+ display_summary: typeof display.summary === "string" ? display.summary : null,
1229
+ sources: Array.isArray(display.sources) ? display.sources.filter(isRecord) : []
1230
+ });
1231
+ if (!context) return null;
1232
+ const turnId = typeof entry.id === "string" && entry.id ? entry.id : `context-${sequence}`;
1233
+ return {
1234
+ id: turnId,
1235
+ sequence,
1236
+ turn_id: turnId,
1237
+ loop_id: typeof entry.loop_name === "string" && entry.loop_name ? entry.loop_name : "root",
1238
+ kind: "context",
1239
+ role: "system",
1240
+ status: "completed",
1241
+ blocks: [],
1242
+ tool_calls: [],
1243
+ model: null,
1244
+ usage: null,
1245
+ duration_ms: 0,
1246
+ context_content: Object.hasOwn(message, "content") ? message.content : null,
1247
+ ...context
1248
+ };
1249
+ }
1250
+
1018
1251
  // src/shared/projection/builder.ts
1252
+ function backgroundTaskStatus(payload) {
1253
+ const tasks = Array.isArray(payload.tasks) ? payload.tasks : [];
1254
+ return tasks.some((task) => isRecord(task) && (["failed", "cancelled", "lost"].includes(String(task.status)) || typeof task.exit_code === "number" && task.exit_code !== 0)) ? "error" : "done";
1255
+ }
1256
+ function acknowledgeTurnEvents(acknowledge) {
1257
+ acknowledge?.();
1258
+ }
1259
+ function hasChatRunEvent(events) {
1260
+ return events.some(
1261
+ (event) => event.type !== "llm:first_token:delivered" && event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
1262
+ );
1263
+ }
1264
+ function acceptedPostChatFollowupCompletesLatestRun(events, updates) {
1265
+ const acceptedAssistantEntryIds = new Set(
1266
+ updates.flatMap((update) => {
1267
+ if (update.kind !== "upsert") return [];
1268
+ const followup = update.turn.post_chat_followup;
1269
+ return followup?.assistant_entry_id ? [followup.assistant_entry_id] : [];
1270
+ })
1271
+ );
1272
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1273
+ const event = events[index];
1274
+ if (event?.type !== "session:post_chat_followup") continue;
1275
+ const assistantEntryId = String(event.payload.assistant_entry_id ?? "");
1276
+ return acceptedAssistantEntryIds.has(assistantEntryId) && !hasChatRunEvent(events.slice(index + 1));
1277
+ }
1278
+ return false;
1279
+ }
1019
1280
  function isConversationTurn(turn) {
1020
1281
  return (turn.role === "user" || turn.role === "assistant") && (turn.kind ?? "message") === "message" && !turn.blocks.some(
1021
1282
  (block) => block.type === "mode_change" || block.type === "plan_status" || block.type === "ask_user_answer"
1022
1283
  );
1023
1284
  }
1285
+ function completedResponseText(content) {
1286
+ if (typeof content === "string") return content;
1287
+ if (!Array.isArray(content)) return content == null ? "" : String(content);
1288
+ return content.filter(
1289
+ (part) => typeof part === "object" && part !== null && !Array.isArray(part)
1290
+ ).filter((part) => part.type === "text").map((part) => String(part.text ?? "")).join("");
1291
+ }
1292
+ function replaceTextContent(state, content) {
1293
+ const currentContent = state.blocks.filter((block) => block.type === "text").map((block) => String(block.content ?? "")).join("");
1294
+ if (currentContent === content) return;
1295
+ const replacement = { type: "text", content };
1296
+ const blocks = [];
1297
+ let inserted = false;
1298
+ for (const block of state.blocks) {
1299
+ if (block.type === "text") {
1300
+ if (content && !inserted) {
1301
+ blocks.push(replacement);
1302
+ inserted = true;
1303
+ }
1304
+ continue;
1305
+ }
1306
+ blocks.push(block);
1307
+ }
1308
+ if (content && !inserted) {
1309
+ const toolIndex = blocks.findIndex((block) => block.type === "tool_use");
1310
+ blocks.splice(toolIndex < 0 ? blocks.length : toolIndex, 0, replacement);
1311
+ }
1312
+ state.blocks = blocks;
1313
+ }
1314
+ function applyResponseBlockOrder(state, value) {
1315
+ if (!Array.isArray(value)) return false;
1316
+ const ordered = [];
1317
+ for (const item of value) {
1318
+ if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
1319
+ const block = item;
1320
+ if (block.type === "text" || block.type === "thinking") {
1321
+ const content = String(block[block.type] ?? "");
1322
+ if (content) ordered.push({ type: block.type, content });
1323
+ continue;
1324
+ }
1325
+ if (block.type !== "tool_call") continue;
1326
+ const toolCallId = String(block.id ?? "");
1327
+ const toolBlock = state.blocks.find(
1328
+ (candidate) => candidate.type === "tool_use" && candidate.tool_call_id === toolCallId
1329
+ );
1330
+ if (toolBlock) ordered.push(toolBlock);
1331
+ }
1332
+ if (ordered.length === 0 && value.length > 0) return false;
1333
+ const unrelatedBlocks = state.blocks.filter(
1334
+ (block) => block.type !== "text" && block.type !== "thinking" && block.type !== "tool_use"
1335
+ );
1336
+ state.blocks = [...ordered, ...unrelatedBlocks];
1337
+ return true;
1338
+ }
1024
1339
  var ClientProjectionBuilder = class {
1025
1340
  activeTurns = /* @__PURE__ */ new Map();
1026
1341
  lastTurnIds = /* @__PURE__ */ new Map();
1027
1342
  latestTurns = /* @__PURE__ */ new Map();
1343
+ turnsById = /* @__PURE__ */ new Map();
1028
1344
  loopDescriptions = /* @__PURE__ */ new Map();
1345
+ /** loop_name → 正在运行的子智能体起点,agent:end 结算并挂到父工具调用上。 */
1346
+ activeSubagentRuns = /* @__PURE__ */ new Map();
1029
1347
  pendingChildPauses = /* @__PURE__ */ new Map();
1030
1348
  pendingMemoryRefs = /* @__PURE__ */ new Map();
1031
1349
  activeCompactions = /* @__PURE__ */ new Map();
@@ -1060,6 +1378,8 @@ var ClientProjectionBuilder = class {
1060
1378
  return this.onMemoryInject(loopId, payload);
1061
1379
  case "memory:inject:none":
1062
1380
  return null;
1381
+ case "context:entry":
1382
+ return this.onContextEntry(payload);
1063
1383
  case "loop:turn":
1064
1384
  return this.onLoopTurn(loopId, payload);
1065
1385
  case "mode:change":
@@ -1083,6 +1403,25 @@ var ClientProjectionBuilder = class {
1083
1403
  }
1084
1404
  if (event.type.startsWith("compaction:"))
1085
1405
  return this.onCompaction(event.type, loopId, payload);
1406
+ if (event.type === "llm:first_token:delivered") {
1407
+ const targetTurnId = String(payload.turn_id ?? "");
1408
+ const active = this.activeTurns.get(loopId);
1409
+ if (active && (!targetTurnId || active.turnId === targetTurnId)) {
1410
+ this.applyFirstTokenDelivery(active, payload);
1411
+ return this.syncPatch(active);
1412
+ }
1413
+ const latest = targetTurnId ? this.turnsById.get(targetTurnId) : this.latestTurns.get(loopId);
1414
+ const duration = payload.first_token_delivery_ms;
1415
+ if (!latest || latest.role !== "assistant" || typeof duration !== "number" || duration < 0) {
1416
+ return null;
1417
+ }
1418
+ const turn = { ...latest, first_token_delivery_ms: duration };
1419
+ this.turnsById.set(turn.turn_id, turn);
1420
+ if (this.latestTurns.get(loopId)?.turn_id === turn.turn_id) {
1421
+ this.latestTurns.set(loopId, turn);
1422
+ }
1423
+ return [{ kind: "upsert", turn }];
1424
+ }
1086
1425
  const state = this.activeTurns.get(loopId);
1087
1426
  if (!state) {
1088
1427
  if (event.type === "chat:end") {
@@ -1098,13 +1437,28 @@ var ClientProjectionBuilder = class {
1098
1437
  state.toolCalls = [];
1099
1438
  state.usage = null;
1100
1439
  state.responseDone = false;
1440
+ state.firstTokenMs = null;
1441
+ state.streamDurationMs = null;
1442
+ state.firstTokenDeliveryMs = null;
1101
1443
  return this.syncPatch(state);
1102
1444
  case "llm:thinking:delta":
1445
+ this.applyFirstTokenDelivery(state, payload);
1103
1446
  return this.onDelta(state, "thinking", "", String(payload.content ?? ""));
1104
1447
  case "llm:content:delta":
1448
+ this.applyFirstTokenDelivery(state, payload);
1105
1449
  return this.onDelta(state, "text", "", String(payload.content ?? ""));
1450
+ case "llm:content:complete":
1451
+ replaceTextContent(state, String(payload.content ?? ""));
1452
+ return this.syncPatch(state);
1453
+ case "llm:content:done":
1454
+ this.applyStreamTiming(state, payload);
1455
+ return this.syncPatch(state);
1106
1456
  case "llm:tool_call:created":
1457
+ this.applyFirstTokenDelivery(state, payload);
1107
1458
  return this.onToolCallCreated(state, payload);
1459
+ case "llm:tool_call:arguments:delta":
1460
+ this.applyFirstTokenDelivery(state, payload);
1461
+ return this.onToolCallArgumentsDelta(state, payload);
1108
1462
  case "llm:response:done":
1109
1463
  return this.onResponseDone(state, loopId, payload);
1110
1464
  case "tool:result:delta":
@@ -1131,7 +1485,9 @@ var ClientProjectionBuilder = class {
1131
1485
  this.activeTurns.clear();
1132
1486
  this.lastTurnIds.clear();
1133
1487
  this.latestTurns.clear();
1488
+ this.turnsById.clear();
1134
1489
  this.loopDescriptions.clear();
1490
+ this.activeSubagentRuns.clear();
1135
1491
  this.pendingChildPauses.clear();
1136
1492
  this.pendingMemoryRefs.clear();
1137
1493
  this.activeCompactions.clear();
@@ -1146,15 +1502,18 @@ var ClientProjectionBuilder = class {
1146
1502
  }
1147
1503
  _seedTurns(turns) {
1148
1504
  const latestTurns = /* @__PURE__ */ new Map();
1505
+ const turnsById = /* @__PURE__ */ new Map();
1149
1506
  for (const turn of turns) {
1150
1507
  this.seedSequence(turn.sequence);
1151
1508
  if (!isConversationTurn(turn)) continue;
1509
+ turnsById.set(turn.turn_id, turn);
1152
1510
  const current = latestTurns.get(turn.loop_id);
1153
1511
  if (!current || turn.sequence >= current.sequence) {
1154
1512
  latestTurns.set(turn.loop_id, turn);
1155
1513
  }
1156
1514
  }
1157
1515
  this.latestTurns = latestTurns;
1516
+ this.turnsById = turnsById;
1158
1517
  }
1159
1518
  // --- Event handlers ---
1160
1519
  onMemoryInject(loopId, payload) {
@@ -1172,11 +1531,16 @@ var ClientProjectionBuilder = class {
1172
1531
  }
1173
1532
  return null;
1174
1533
  }
1534
+ onContextEntry(payload) {
1535
+ if (!isRecord(payload.entry)) return null;
1536
+ const turn = buildContextTurn(payload.entry, this.nextSeq());
1537
+ return turn ? [{ kind: "upsert", turn }] : null;
1538
+ }
1175
1539
  onLoopTurn(loopId, payload) {
1176
1540
  let turnId = String(payload.turn_id ?? this.nextTurnId(`turn:${loopId}`));
1177
1541
  let memoryRefs = null;
1178
1542
  const pending = this.pendingMemoryRefs.get(loopId);
1179
- if (pending) {
1543
+ if (pending && (!pending.turnId || pending.turnId === turnId)) {
1180
1544
  this.pendingMemoryRefs.delete(loopId);
1181
1545
  if (pending.turnId) turnId = pending.turnId;
1182
1546
  memoryRefs = pending.refs;
@@ -1192,6 +1556,7 @@ var ClientProjectionBuilder = class {
1192
1556
  this.lastTurnIds.set(loopId, turnId);
1193
1557
  const turn = snapshot(state, this.nextSeq(), "streaming");
1194
1558
  this.latestTurns.set(loopId, turn);
1559
+ this.turnsById.set(turn.turn_id, turn);
1195
1560
  return [{ kind: "upsert", turn }];
1196
1561
  }
1197
1562
  onModeChange(loopId, payload) {
@@ -1228,11 +1593,11 @@ var ClientProjectionBuilder = class {
1228
1593
  loopId,
1229
1594
  toolCallId: optStr(payload.tool_call_id) ?? void 0,
1230
1595
  toolName: toolNameFromPayload(payload) ?? void 0,
1231
- workspacePath: isRecord2(payload.workspace_change) ? optStr(payload.workspace_change.to) ?? void 0 : void 0
1596
+ workspacePath: isRecord(payload.workspace_change) ? optStr(payload.workspace_change.to) ?? void 0 : void 0
1232
1597
  }
1233
1598
  ];
1234
1599
  const change = payload.workspace_change;
1235
- if (isRecord2(change)) {
1600
+ if (isRecord(change)) {
1236
1601
  const source = optStr(change.source) ?? "";
1237
1602
  const projectName = optStr(change.project_name) ?? "";
1238
1603
  const fromPath = optStr(change.from) ?? "";
@@ -1264,6 +1629,13 @@ var ClientProjectionBuilder = class {
1264
1629
  onAgentStart(loopId, payload) {
1265
1630
  const description = optStr(payload.description);
1266
1631
  if (description) this.loopDescriptions.set(loopId, description);
1632
+ const subagentStartedAt = optStr(payload.started_at);
1633
+ if (subagentStartedAt) {
1634
+ this.activeSubagentRuns.set(loopId, {
1635
+ runId: optStr(payload.run_id) ?? loopId,
1636
+ startedAt: subagentStartedAt
1637
+ });
1638
+ }
1267
1639
  const turn = buildSystemNotificationTurn(
1268
1640
  this.nextSeq(),
1269
1641
  this.nextTurnId("agent:start"),
@@ -1305,7 +1677,7 @@ var ClientProjectionBuilder = class {
1305
1677
  this.nextTurnId("bg:tasks_completed"),
1306
1678
  loopId,
1307
1679
  "bg:tasks_completed",
1308
- "done",
1680
+ backgroundTaskStatus(payload),
1309
1681
  `Background tasks completed (${count})`,
1310
1682
  detail || null,
1311
1683
  { task_ids: taskIds, count }
@@ -1325,7 +1697,32 @@ var ClientProjectionBuilder = class {
1325
1697
  }
1326
1698
  const directTurn = this.activeTurns.get(loopId) ?? this.latestTurns.get(loopId);
1327
1699
  setToolCallStatus(directTurn, toolCallId, "done", null, null);
1328
- return this.collectLoopUpdates(loopId, parentLoopId);
1700
+ const updates = this.collectLoopUpdates(loopId, parentLoopId);
1701
+ const entryId = typeof payload.entry_id === "string" ? payload.entry_id : "";
1702
+ if (!entryId) return updates;
1703
+ const answerData = isRecord(payload.answer_data) ? payload.answer_data : {};
1704
+ const answerTurn = {
1705
+ id: entryId,
1706
+ sequence: this.nextSeq(),
1707
+ turn_id: entryId,
1708
+ loop_id: loopId,
1709
+ kind: "message",
1710
+ role: "assistant",
1711
+ status: "completed",
1712
+ blocks: [
1713
+ {
1714
+ type: "ask_user_answer",
1715
+ content: answerData,
1716
+ tool_call_id: toolCallId || null
1717
+ }
1718
+ ],
1719
+ tool_calls: [],
1720
+ model: null,
1721
+ usage: null,
1722
+ duration_ms: 0,
1723
+ client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
1724
+ };
1725
+ return [...updates, { kind: "upsert", turn: answerTurn }];
1329
1726
  }
1330
1727
  onUserMessage(_loopId, payload) {
1331
1728
  const entryId = String(payload.entry_id ?? this.nextTurnId("user"));
@@ -1342,8 +1739,12 @@ var ClientProjectionBuilder = class {
1342
1739
  tool_calls: [],
1343
1740
  model: null,
1344
1741
  usage: null,
1345
- duration_ms: 0
1742
+ duration_ms: 0,
1743
+ client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
1346
1744
  };
1745
+ if (payload.start_reason === "background-completion") {
1746
+ turn.blocks.push({ type: "system_notification", content: { notification_type: "bg:wakeup" } });
1747
+ }
1347
1748
  this.latestTurns.set("root", turn);
1348
1749
  return [{ kind: "upsert", turn }];
1349
1750
  }
@@ -1370,13 +1771,7 @@ var ClientProjectionBuilder = class {
1370
1771
  }
1371
1772
  onToolCallCreated(state, payload) {
1372
1773
  if ("arguments_delta" in payload) {
1373
- const delta = String(payload.arguments_delta ?? "");
1374
- const toolCallId2 = String(payload.id ?? "");
1375
- if (delta && toolCallId2) {
1376
- appendToolCallArguments(state, toolCallId2, delta);
1377
- return this.syncPatch(state);
1378
- }
1379
- return null;
1774
+ return this.onToolCallArgumentsDelta(state, payload);
1380
1775
  }
1381
1776
  const toolCallId = String(payload.id ?? "");
1382
1777
  const toolName = toolNameFromPayload(payload);
@@ -1385,8 +1780,16 @@ var ClientProjectionBuilder = class {
1385
1780
  upsertToolCall(state, toolCallId, toolName, displayName, args);
1386
1781
  return this.syncPatch(state);
1387
1782
  }
1783
+ onToolCallArgumentsDelta(state, payload) {
1784
+ const delta = String(payload.arguments_delta ?? "");
1785
+ const toolCallId = String(payload.id ?? "");
1786
+ if (!delta || !toolCallId) return null;
1787
+ appendToolCallArguments(state, toolCallId, delta);
1788
+ return this.syncPatch(state);
1789
+ }
1388
1790
  onResponseDone(state, loopId, payload) {
1389
1791
  state.responseDone = true;
1792
+ this.applyStreamTiming(state, payload);
1390
1793
  const model = optStr(payload.model);
1391
1794
  if (model) state.model = model;
1392
1795
  if (typeof payload.usage === "object" && payload.usage !== null) {
@@ -1409,11 +1812,42 @@ var ClientProjectionBuilder = class {
1409
1812
  );
1410
1813
  }
1411
1814
  }
1412
- if (state.toolCalls.length === 0) {
1815
+ if (!applyResponseBlockOrder(state, payload.response_blocks) && "content" in payload) {
1816
+ replaceTextContent(state, completedResponseText(payload.content));
1817
+ }
1818
+ const isNativeProviderResponse = typeof payload.provider === "string" && payload.provider !== "";
1819
+ if (state.toolCalls.length === 0 || isNativeProviderResponse && state.toolCalls.every(
1820
+ (toolCall) => toolCall.status !== "pending" && toolCall.status !== "awaiting_answer"
1821
+ )) {
1413
1822
  return this.finalize(loopId, "completed");
1414
1823
  }
1415
1824
  return this.syncPatch(state);
1416
1825
  }
1826
+ applyFirstTokenDelivery(state, payload) {
1827
+ const firstTokenMs = payload.first_token_ms;
1828
+ if (state.firstTokenMs === null && typeof firstTokenMs === "number" && firstTokenMs >= 0) {
1829
+ state.firstTokenMs = Math.round(firstTokenMs);
1830
+ }
1831
+ const duration = payload.first_token_delivery_ms;
1832
+ if (state.firstTokenDeliveryMs === null && typeof duration === "number" && duration >= 0) {
1833
+ state.firstTokenDeliveryMs = duration;
1834
+ }
1835
+ }
1836
+ applyStreamTiming(state, payload) {
1837
+ if (typeof payload.first_token_ms === "number" && payload.first_token_ms >= 0) {
1838
+ state.firstTokenMs = Math.round(payload.first_token_ms);
1839
+ }
1840
+ if (typeof payload.stream_duration_ms === "number" && payload.stream_duration_ms >= 0) {
1841
+ state.streamDurationMs = Math.round(payload.stream_duration_ms);
1842
+ }
1843
+ if (typeof payload.started_at === "string" && payload.started_at) {
1844
+ state.modelStartedAt = payload.started_at;
1845
+ state.modelStartedAtReported = true;
1846
+ }
1847
+ if (typeof payload.duration_ms === "number" && payload.duration_ms >= 0) {
1848
+ state.modelDurationMs = Math.round(payload.duration_ms);
1849
+ }
1850
+ }
1417
1851
  onToolResultDone(state, loopId, payload) {
1418
1852
  const toolCallId = String(payload.tool_call_id ?? "");
1419
1853
  const streamedResult = state.toolCalls.find((toolCall) => toolCall.id === toolCallId)?.result;
@@ -1421,9 +1855,12 @@ var ClientProjectionBuilder = class {
1421
1855
  const status = toolStatusFromResult(content);
1422
1856
  const rawDuration = payload.duration_ms;
1423
1857
  const durationMs = typeof rawDuration === "number" && rawDuration >= 0 ? Math.round(rawDuration) : null;
1858
+ const ranInParallel = typeof payload.ran_in_parallel === "boolean" ? payload.ran_in_parallel : null;
1424
1859
  const sl = sourceLoopFor(state.loopId, this.loopDescriptions);
1425
- applyToolResult(state, toolCallId, content, status, durationMs, sl);
1426
- if (this.shouldFinalizeAfterToolResults(state)) {
1860
+ const startedAt = typeof payload.started_at === "string" && payload.started_at ? payload.started_at : null;
1861
+ applyToolResult(state, toolCallId, content, status, durationMs, ranInParallel, sl, startedAt);
1862
+ const isNativeProviderResult = typeof payload.provider === "string" && payload.provider !== "";
1863
+ if (!isNativeProviderResult && this.shouldFinalizeAfterToolResults(state)) {
1427
1864
  return this.finalize(loopId, "completed");
1428
1865
  }
1429
1866
  return this.syncPatch(state);
@@ -1452,10 +1889,13 @@ var ClientProjectionBuilder = class {
1452
1889
  onToolBridge(state, payload) {
1453
1890
  const bridge = payload.bridge;
1454
1891
  if (typeof bridge !== "object" || bridge === null) return null;
1892
+ const toolCallId = String(payload.tool_call_id ?? "");
1893
+ if (!toolCallId || !state.toolCalls.some((toolCall) => toolCall.id === toolCallId)) return null;
1455
1894
  state.blocks.push({
1456
1895
  type: "tool_bridge",
1457
1896
  content: bridge,
1458
- tool_call_id: String(payload.tool_call_id ?? "") || null
1897
+ delivery_id: String(payload.delivery_id ?? "") || null,
1898
+ tool_call_id: toolCallId
1459
1899
  });
1460
1900
  return this.syncPatch(state);
1461
1901
  }
@@ -1464,6 +1904,11 @@ var ClientProjectionBuilder = class {
1464
1904
  const toolName = String(payload.tool_name ?? "");
1465
1905
  const displayName = this.displayNameResolver(toolName);
1466
1906
  upsertToolCall(state, toolCallId, toolName, displayName, "");
1907
+ const relatedToolCallId = String(payload.related_tool_call_id ?? "");
1908
+ if (relatedToolCallId && relatedToolCallId !== toolCallId) {
1909
+ const related = state.toolCalls.find((toolCall) => toolCall.id === relatedToolCallId);
1910
+ if (related) related.status = "awaiting_answer";
1911
+ }
1467
1912
  return this.syncPatch(state);
1468
1913
  }
1469
1914
  onChildPause(state, loopId, payload) {
@@ -1569,6 +2014,7 @@ var ClientProjectionBuilder = class {
1569
2014
  }
1570
2015
  onAgentEnd(state, loopId, payload) {
1571
2016
  const ok = payload.ok !== false;
2017
+ this.settleSubagentRun(loopId, payload);
1572
2018
  const status = ok ? "done" : "error";
1573
2019
  const title = `Agent: ${payload.description ?? payload.skill_id ?? "agent"}`;
1574
2020
  const detail = optStr(payload.error);
@@ -1576,6 +2022,7 @@ var ClientProjectionBuilder = class {
1576
2022
  skill_id: optStr(payload.skill_id),
1577
2023
  description: optStr(payload.description),
1578
2024
  parent_fork_tool_call_id: optStr(payload.parent_fork_tool_call_id),
2025
+ ...typeof payload.run_status === "string" ? { run_status: payload.run_status } : {},
1579
2026
  ok
1580
2027
  };
1581
2028
  if (state) {
@@ -1595,6 +2042,32 @@ var ClientProjectionBuilder = class {
1595
2042
  );
1596
2043
  return [{ kind: "upsert", turn }];
1597
2044
  }
2045
+ settleSubagentRun(loopId, payload) {
2046
+ const active = this.activeSubagentRuns.get(loopId);
2047
+ this.activeSubagentRuns.delete(loopId);
2048
+ const parentToolCallId = optStr(payload.parent_fork_tool_call_id);
2049
+ if (!parentToolCallId) return;
2050
+ const rawDuration = payload.run_duration_ms;
2051
+ const durationMs = typeof rawDuration === "number" && rawDuration >= 0 ? Math.round(rawDuration) : null;
2052
+ const rawStatus = optStr(payload.run_status);
2053
+ const status = rawStatus === "failed" || rawStatus === "cancelled" || rawStatus === "paused" || rawStatus === "incomplete" ? rawStatus : payload.ok === false ? "failed" : "completed";
2054
+ const run = {
2055
+ run_id: active?.runId ?? optStr(payload.run_id) ?? loopId,
2056
+ scope: "subagent",
2057
+ loop_id: loopId,
2058
+ started_at: active?.startedAt ?? optStr(payload.run_started_at),
2059
+ duration_ms: durationMs,
2060
+ status
2061
+ };
2062
+ const seen = /* @__PURE__ */ new Set();
2063
+ for (const turn of [...this.latestTurns.values(), ...this.turnsById.values()]) {
2064
+ if (seen.has(turn)) continue;
2065
+ seen.add(turn);
2066
+ const toolCall = turn.tool_calls.find((item) => item.id === parentToolCallId);
2067
+ if (!toolCall) continue;
2068
+ toolCall.subagent_runs = [...toolCall.subagent_runs ?? [], run];
2069
+ }
2070
+ }
1598
2071
  dropIncompleteToolCalls(state) {
1599
2072
  if (state.responseDone) return;
1600
2073
  state.toolCalls = [];
@@ -1644,6 +2117,7 @@ var ClientProjectionBuilder = class {
1644
2117
  syncPatch(state) {
1645
2118
  const turn = snapshot(state, this.nextSeq(), "streaming");
1646
2119
  this.latestTurns.set(state.loopId, turn);
2120
+ this.turnsById.set(turn.turn_id, turn);
1647
2121
  this.lastTurnIds.set(state.loopId, turn.turn_id);
1648
2122
  return [{ kind: "upsert", turn }];
1649
2123
  }
@@ -1654,6 +2128,7 @@ var ClientProjectionBuilder = class {
1654
2128
  const turn = snapshot(state, this.nextSeq(), status);
1655
2129
  this.lastTurnIds.set(loopId, turn.turn_id);
1656
2130
  this.latestTurns.set(loopId, turn);
2131
+ this.turnsById.set(turn.turn_id, turn);
1657
2132
  return [{ kind: "upsert", turn }];
1658
2133
  }
1659
2134
  shouldFinalizeAfterToolResults(state) {
@@ -1691,9 +2166,6 @@ var ClientProjectionBuilder = class {
1691
2166
  return `${prefix}:${this.syntheticCounter}:${hex}`;
1692
2167
  }
1693
2168
  };
1694
- function isRecord2(value) {
1695
- return typeof value === "object" && value !== null && !Array.isArray(value);
1696
- }
1697
2169
  function optStr(value) {
1698
2170
  if (value == null || value === "") return null;
1699
2171
  return String(value);
@@ -1713,6 +2185,101 @@ function toolArgsFromPayload(payload) {
1713
2185
  return "";
1714
2186
  }
1715
2187
 
2188
+ // src/schemas/mcp-app.ts
2189
+ function isMcpAppData(value) {
2190
+ 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);
2191
+ }
2192
+
2193
+ // src/shared/tool-ui-card.ts
2194
+ function isNonEmptyString(value) {
2195
+ return typeof value === "string" && value.trim().length > 0;
2196
+ }
2197
+ function isPositiveNumber(value) {
2198
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
2199
+ }
2200
+ function isToolUiCard(value) {
2201
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2202
+ return false;
2203
+ }
2204
+ const raw = value;
2205
+ if (raw.target !== "inline" && raw.target !== "preview") {
2206
+ return false;
2207
+ }
2208
+ if (!isPositiveNumber(raw.height)) {
2209
+ return false;
2210
+ }
2211
+ if (raw.archived === true && !isNonEmptyString(raw.resourceHTML)) {
2212
+ return false;
2213
+ }
2214
+ if (!isNonEmptyString(raw.resourceHTML) && !isNonEmptyString(raw.resourceUri) && !isNonEmptyString(raw.resourceURI) && !(raw.target === "preview" && isNonEmptyString(raw.sourcePath))) {
2215
+ return false;
2216
+ }
2217
+ if (raw.title != null && !isNonEmptyString(raw.title)) {
2218
+ return false;
2219
+ }
2220
+ if (raw.sourcePath != null && !isNonEmptyString(raw.sourcePath)) return false;
2221
+ if (raw.mcpApp !== void 0 && !isMcpAppData(raw.mcpApp)) {
2222
+ return false;
2223
+ }
2224
+ return true;
2225
+ }
2226
+ function resolveToolUiCardContent(card) {
2227
+ if (isNonEmptyString(card.resourceHTML)) {
2228
+ return { type: "resource-html", content: card.resourceHTML };
2229
+ }
2230
+ if (card.archived) return null;
2231
+ const uri = isNonEmptyString(card.resourceUri) ? card.resourceUri : card.resourceURI;
2232
+ if (isNonEmptyString(uri)) return { type: "resource-uri", content: uri };
2233
+ return card.target === "preview" && isNonEmptyString(card.sourcePath) ? { type: "resource-file", content: card.sourcePath } : null;
2234
+ }
2235
+ function buildToolUiCardKey(toolCallId, type3, content, sourcePath) {
2236
+ if (type3 === "resource-uri") {
2237
+ return `tool-preview-uri:${content}`;
2238
+ }
2239
+ if (sourcePath?.trim()) return sourcePath.trim();
2240
+ if (type3 === "resource-file") return content;
2241
+ return `tool-preview:${toolCallId}`;
2242
+ }
2243
+ function isInternalStatusToolUiCard(card) {
2244
+ return card?.title?.trim() === "\u9636\u6BB5\u8FDB\u5EA6";
2245
+ }
2246
+ function isAppDevToolUiCard(card, isAppDev) {
2247
+ return isAppDev && card.target === "preview" && !card.resourceHTML && Boolean(card.resourceUri || card.resourceURI);
2248
+ }
2249
+ function collectInlineToolUiCards(messages) {
2250
+ return messages.flatMap(
2251
+ (message) => (message.blocks ?? []).flatMap(
2252
+ (block, index) => block.type === "tool_ui" && block.tool_call_id && isToolUiCard(block.content) && block.content.target === "inline" ? [
2253
+ {
2254
+ key: `${message.entry_id ?? message.timestamp ?? "message"}-${block.tool_call_id}-${index}`,
2255
+ toolCallId: block.tool_call_id,
2256
+ card: block.content
2257
+ }
2258
+ ] : []
2259
+ )
2260
+ );
2261
+ }
2262
+ function collectPreviewToolUiCards(messages) {
2263
+ return messages.flatMap(
2264
+ (message) => (message.blocks ?? []).flatMap((block, index) => {
2265
+ if (block.type !== "tool_ui" || !block.tool_call_id || !isToolUiCard(block.content) || block.content.target !== "preview") {
2266
+ return [];
2267
+ }
2268
+ const toolCall = (message.tool_calls ?? []).find(
2269
+ (candidate) => candidate.id === block.tool_call_id
2270
+ );
2271
+ return toolCall ? [
2272
+ {
2273
+ key: `${message.entry_id ?? message.timestamp ?? "message"}-${block.tool_call_id}-${index}`,
2274
+ toolCall,
2275
+ card: block.content,
2276
+ blocks: message.blocks ?? []
2277
+ }
2278
+ ] : [];
2279
+ })
2280
+ );
2281
+ }
2282
+
1716
2283
  // src/shared/projection/history.ts
1717
2284
  function projectHistory(entries) {
1718
2285
  const turns = [];
@@ -1720,6 +2287,8 @@ function projectHistory(entries) {
1720
2287
  const latestAssistantByLoop = /* @__PURE__ */ new Map();
1721
2288
  const pendingChildPauses = /* @__PURE__ */ new Map();
1722
2289
  const answeredToolCallIds = /* @__PURE__ */ new Set();
2290
+ const nativeToolTurns = /* @__PURE__ */ new Set();
2291
+ const turnEntryAliases = /* @__PURE__ */ new Map();
1723
2292
  let latestRootMessage = null;
1724
2293
  let latestRootAssistant = null;
1725
2294
  let sequence = 0;
@@ -1727,7 +2296,40 @@ function projectHistory(entries) {
1727
2296
  const kind = String(entry.kind ?? "");
1728
2297
  const loopId = String(entry.loop_name ?? "root");
1729
2298
  const id = String(entry.id ?? `history-${sequence + 1}`);
1730
- if (kind === "post_chat_followup" && isRecord3(entry.data)) {
2299
+ if (kind === "turn_timing" && isRecord(entry.data)) {
2300
+ const turnId = typeof entry.data.turn_id === "string" ? entry.data.turn_id : "";
2301
+ const duration = numberOrNull(entry.data.first_token_delivery_ms);
2302
+ const target = turnEntryAliases.get(turnId);
2303
+ if (target && duration !== null) target.first_token_delivery_ms = duration;
2304
+ continue;
2305
+ }
2306
+ if (kind === "run_timing" && isRecord(entry.data)) {
2307
+ const data = entry.data;
2308
+ if (data.scope === "subagent") {
2309
+ const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
2310
+ const duration = numberOrNull(data.duration_ms);
2311
+ const rawStatus = String(data.status ?? "completed");
2312
+ const status = rawStatus === "failed" || rawStatus === "cancelled" || rawStatus === "paused" || rawStatus === "incomplete" ? rawStatus : "completed";
2313
+ for (const turn of turns) {
2314
+ const toolCall = turn.tool_calls.find((item) => item.id === parentToolCallId);
2315
+ if (!toolCall) continue;
2316
+ toolCall.subagent_runs = [
2317
+ ...toolCall.subagent_runs ?? [],
2318
+ {
2319
+ run_id: String(data.run_id ?? ""),
2320
+ scope: "subagent",
2321
+ loop_id: loopId,
2322
+ started_at: typeof data.started_at === "string" ? data.started_at : null,
2323
+ duration_ms: duration,
2324
+ status
2325
+ }
2326
+ ];
2327
+ break;
2328
+ }
2329
+ }
2330
+ continue;
2331
+ }
2332
+ if (kind === "post_chat_followup" && isRecord(entry.data)) {
1731
2333
  const assistantEntryId = String(entry.data.assistant_entry_id ?? "");
1732
2334
  if (latestRootMessage?.role === "assistant" && latestRootMessage.turn_id === assistantEntryId) {
1733
2335
  const followup = normalizePostChatFollowup(entry.data, assistantEntryId);
@@ -1735,7 +2337,15 @@ function projectHistory(entries) {
1735
2337
  }
1736
2338
  continue;
1737
2339
  }
1738
- if (kind === "message" && isRecord3(entry.message)) {
2340
+ if (kind === "context") {
2341
+ const turn = buildContextTurn(entry, sequence + 1);
2342
+ if (turn) {
2343
+ sequence += 1;
2344
+ turns.push(turn);
2345
+ }
2346
+ continue;
2347
+ }
2348
+ if (kind === "message" && isRecord(entry.message)) {
1739
2349
  const message = entry.message;
1740
2350
  const role = String(message.role ?? "");
1741
2351
  if (role === "tool") {
@@ -1745,6 +2355,12 @@ function projectHistory(entries) {
1745
2355
  if (owner && toolCall) {
1746
2356
  toolCall.status = message._error ? "error" : inferToolStatus(String(message.content ?? ""));
1747
2357
  toolCall.result = message.content ?? "";
2358
+ const duration = numberOrNull(message._duration_ms);
2359
+ if (duration !== null) toolCall.duration_ms = duration;
2360
+ if (typeof message._started_at === "string") {
2361
+ toolCall.started_at = message._started_at;
2362
+ }
2363
+ if (message._ran_in_parallel === true) toolCall.ran_in_parallel = true;
1748
2364
  owner.blocks.push({
1749
2365
  type: "tool_result",
1750
2366
  content: message.content ?? "",
@@ -1754,8 +2370,11 @@ function projectHistory(entries) {
1754
2370
  continue;
1755
2371
  }
1756
2372
  if (role !== "user" && role !== "assistant" && role !== "system") continue;
1757
- const toolCalls = role === "assistant" ? buildToolCalls(message.tool_calls) : [];
2373
+ const toolCalls = role === "assistant" && !message._error ? buildToolCalls(message.tool_calls) : [];
1758
2374
  const blocks = buildBlocks(message, toolCalls);
2375
+ if (role === "user" && message._start_reason === "background-completion") {
2376
+ blocks.push({ type: "system_notification", content: { notification_type: "bg:wakeup" } });
2377
+ }
1759
2378
  const turn = {
1760
2379
  id,
1761
2380
  sequence: ++sequence,
@@ -1767,14 +2386,52 @@ function projectHistory(entries) {
1767
2386
  blocks,
1768
2387
  tool_calls: toolCalls,
1769
2388
  model: stringOrNull(message.model),
1770
- usage: isRecord3(message._usage) ? { ...message._usage } : null,
1771
- duration_ms: numberOrZero(message._duration_ms),
1772
- started_at: typeof entry.timestamp === "string" ? entry.timestamp : void 0
2389
+ usage: isRecord(message._usage) ? { ...message._usage } : null,
2390
+ // 模型请求区间来自持久化字段;旧会话缺失时为 null,不用落盘时刻冒充(#2581 A1)。
2391
+ duration_ms: numberOrNull(message._duration_ms),
2392
+ first_token_ms: numberOrNull(message._first_token_ms),
2393
+ stream_duration_ms: numberOrNull(message._stream_duration_ms),
2394
+ started_at: typeof message._started_at === "string" ? message._started_at : typeof entry.timestamp === "string" ? entry.timestamp : void 0,
2395
+ has_reported_timing: typeof message._started_at === "string",
2396
+ // fork 子会话的父锚点:历史里落在消息级 _parent_fork_tool_call_id。丢掉它,
2397
+ // 刷新后工作流步骤 fork 就配不回 workflow:run 工具调用(普通 Agent fork 有
2398
+ // root Agent 工具调用兜底配对,工作流 fork 没有,子智能体卡会整体消失)。
2399
+ parent_fork_tool_call_id: stringOrNull(message._parent_fork_tool_call_id)
1773
2400
  };
2401
+ const isNativeToolEntry = role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.some(
2402
+ (toolCall) => isRecord(toolCall) && typeof toolCall.provider === "string"
2403
+ );
2404
+ const previousTurn = turns[turns.length - 1];
2405
+ if (role === "assistant" && previousTurn?.role === "assistant" && previousTurn.loop_id === loopId && nativeToolTurns.has(previousTurn)) {
2406
+ nativeToolTurns.delete(previousTurn);
2407
+ previousTurn.blocks.push(...turn.blocks);
2408
+ previousTurn.tool_calls.push(...turn.tool_calls);
2409
+ previousTurn.status = turn.status;
2410
+ previousTurn.model = turn.model ?? previousTurn.model;
2411
+ previousTurn.usage = turn.usage ?? previousTurn.usage;
2412
+ if (turn.duration_ms) previousTurn.duration_ms = turn.duration_ms;
2413
+ previousTurn.first_token_ms = turn.first_token_ms ?? previousTurn.first_token_ms;
2414
+ previousTurn.stream_duration_ms = turn.stream_duration_ms ?? previousTurn.stream_duration_ms;
2415
+ if (turn.has_reported_timing) {
2416
+ previousTurn.started_at = turn.started_at;
2417
+ previousTurn.has_reported_timing = true;
2418
+ } else if (!previousTurn.has_reported_timing) {
2419
+ previousTurn.started_at = previousTurn.started_at ?? turn.started_at;
2420
+ }
2421
+ previousTurn.id = turn.id;
2422
+ previousTurn.turn_id = turn.turn_id;
2423
+ if (isNativeToolEntry) nativeToolTurns.add(previousTurn);
2424
+ turnEntryAliases.set(id, previousTurn);
2425
+ if (loopId === "root") latestRootMessage = previousTurn;
2426
+ latestAssistantByLoop.set(loopId, previousTurn);
2427
+ continue;
2428
+ }
1774
2429
  if (loopId === "root" && (role === "user" || role === "assistant")) {
1775
2430
  latestRootMessage = turn;
1776
2431
  }
1777
2432
  turns.push(turn);
2433
+ turnEntryAliases.set(id, turn);
2434
+ if (isNativeToolEntry) nativeToolTurns.add(turn);
1778
2435
  if (role === "assistant") latestAssistantByLoop.set(loopId, turn);
1779
2436
  if (loopId === "root") {
1780
2437
  if (role === "assistant") latestRootAssistant = turn;
@@ -1783,10 +2440,25 @@ function projectHistory(entries) {
1783
2440
  for (const toolCall of toolCalls) toolOwners.set(toolCall.id, turn);
1784
2441
  continue;
1785
2442
  }
2443
+ if (kind === "tool_result") {
2444
+ const data = entry.data ?? {};
2445
+ const toolCallId = String(data.tool_call_id ?? "");
2446
+ const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
2447
+ if (owner && toolCallId) {
2448
+ const rawContent = data.content;
2449
+ const content = typeof rawContent === "string" ? rawContent : Array.isArray(rawContent) ? rawContent : String(rawContent ?? "");
2450
+ const status = data.is_error === true ? "error" : inferToolStatus(typeof content === "string" ? content : JSON.stringify(content));
2451
+ setToolCallStatus(owner, toolCallId, status, content, null);
2452
+ if (!owner.blocks.some((block) => block.type === "tool_result" && block.tool_call_id === toolCallId)) {
2453
+ owner.blocks.push({ type: "tool_result", content, tool_call_id: toolCallId });
2454
+ }
2455
+ }
2456
+ continue;
2457
+ }
1786
2458
  if (kind === "mode_change") {
1787
2459
  turns.push(markerTurn(id, ++sequence, loopId, "mode_change", entry.data ?? {}));
1788
2460
  } else if (kind === "workspace_change") {
1789
- const change = isRecord3(entry.data) ? entry.data : {};
2461
+ const change = isRecord(entry.data) ? entry.data : {};
1790
2462
  const source = stringOrNull(change.source) ?? "";
1791
2463
  const projectName = stringOrNull(change.project_name) ?? "";
1792
2464
  const fromPath = stringOrNull(change.from) ?? "";
@@ -1815,19 +2487,22 @@ function projectHistory(entries) {
1815
2487
  } else if (kind === "tool_ui" || kind === "tool_bridge") {
1816
2488
  const data = entry.data ?? {};
1817
2489
  const toolCallId = String(data.tool_call_id ?? "");
1818
- const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
1819
- const content = kind === "tool_ui" ? data.ui : data.bridge;
2490
+ const owner = kind === "tool_bridge" ? toolCallId ? toolOwners.get(toolCallId) : void 0 : toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
2491
+ const rawContent = kind === "tool_ui" ? data.ui : data.bridge;
2492
+ const fileReference = isToolUiCard(rawContent) && resolveToolUiCardContent(rawContent)?.type === "resource-file";
2493
+ const content = kind === "tool_ui" && isRecord(rawContent) ? { ...rawContent, ...!fileReference ? { archived: true } : {} } : rawContent;
1820
2494
  if (owner && content !== void 0) {
1821
2495
  owner.blocks.push({
1822
2496
  type: kind,
1823
2497
  content,
2498
+ ...kind === "tool_bridge" ? { delivery_id: id } : {},
1824
2499
  tool_call_id: toolCallId || null
1825
2500
  });
1826
2501
  }
1827
2502
  } else if (kind === "compaction" || kind === "tool_result_archive") {
1828
2503
  const data = entry.data ?? {};
1829
2504
  const compactionId = String(data.compaction_id ?? entry.id ?? `history-${sequence + 1}`);
1830
- let content = isRecord3(entry.message) ? entry.message.content : void 0;
2505
+ let content = isRecord(entry.message) ? entry.message.content : void 0;
1831
2506
  if (kind === "tool_result_archive") {
1832
2507
  const archivedCount = Array.isArray(data.archived_files) ? data.archived_files.length : 0;
1833
2508
  content = `<compaction-summary>
@@ -1835,18 +2510,24 @@ function projectHistory(entries) {
1835
2510
  </compaction-summary>`;
1836
2511
  }
1837
2512
  turns.push(
1838
- buildCompactionTurn(++sequence, compactionId, loopId, "completed", {
1839
- ...data,
1840
- content
1841
- })
2513
+ buildCompactionTurn(
2514
+ ++sequence,
2515
+ compactionId,
2516
+ loopId,
2517
+ data.status === "interrupted" ? "interrupted" : data.status === "failed" ? "failed" : "completed",
2518
+ {
2519
+ ...data,
2520
+ content
2521
+ }
2522
+ )
1842
2523
  );
1843
2524
  } else if (kind === "child_pause") {
1844
2525
  const data = entry.data ?? {};
1845
- const sourceLoop = isRecord3(data.source_loop) ? data.source_loop : {};
2526
+ const sourceLoop = isRecord(data.source_loop) ? data.source_loop : {};
1846
2527
  const childLoopId = String(data.child_loop_name ?? sourceLoop.name ?? "");
1847
2528
  const childToolCallId = String(data.child_pause_tool_call_id ?? "");
1848
2529
  const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
1849
- const pauseToolData = isRecord3(data.pause_tool_data) ? data.pause_tool_data : {};
2530
+ const pauseToolData = isRecord(data.pause_tool_data) ? data.pause_tool_data : {};
1850
2531
  const description = String(sourceLoop.description ?? "");
1851
2532
  applyAskUserPauseToTurn(
1852
2533
  latestAssistantByLoop.get(childLoopId),
@@ -1934,15 +2615,15 @@ function parseToolArguments(value) {
1934
2615
  if (!value) return {};
1935
2616
  try {
1936
2617
  const parsed = JSON.parse(value);
1937
- return isRecord3(parsed) ? parsed : {};
2618
+ return isRecord(parsed) ? parsed : {};
1938
2619
  } catch {
1939
2620
  return {};
1940
2621
  }
1941
2622
  }
1942
2623
  function buildToolCalls(value) {
1943
2624
  if (!Array.isArray(value)) return [];
1944
- return value.filter(isRecord3).map((raw) => {
1945
- const fn = isRecord3(raw.function) ? raw.function : {};
2625
+ return value.filter(isRecord).map((raw) => {
2626
+ const fn = isRecord(raw.function) ? raw.function : {};
1946
2627
  const name = String(fn.name ?? "");
1947
2628
  return {
1948
2629
  id: String(raw.id ?? ""),
@@ -1958,23 +2639,40 @@ function buildBlocks(message, toolCalls) {
1958
2639
  const displayContent = message.role === "user" ? message._display_content : void 0;
1959
2640
  const stored = message._blocks;
1960
2641
  const blocks = [];
2642
+ const storedToolCallIds = /* @__PURE__ */ new Set();
1961
2643
  if (displayContent !== void 0) {
1962
2644
  blocks.push({ type: "text", content: displayContent });
1963
2645
  } else if (Array.isArray(stored)) {
1964
2646
  for (const raw of stored) {
1965
- if (!isRecord3(raw)) continue;
2647
+ if (!isRecord(raw)) continue;
1966
2648
  const type3 = String(raw.type ?? "");
1967
2649
  if (type3 === "thinking") blocks.push({ type: type3, content: raw.thinking ?? raw.content ?? "" });
1968
2650
  if (type3 === "text") blocks.push({ type: type3, content: raw.text ?? raw.content ?? "" });
2651
+ if (type3 === "tool_call") {
2652
+ const toolCallId = String(raw.id ?? "");
2653
+ const toolCall = toolCalls.find((candidate) => candidate.id === toolCallId);
2654
+ if (!toolCall) continue;
2655
+ storedToolCallIds.add(toolCallId);
2656
+ blocks.push({
2657
+ type: "tool_use",
2658
+ content: toolCall.arguments,
2659
+ tool_call_id: toolCall.id,
2660
+ tool_name: toolCall.tool_name,
2661
+ display_name: toolCall.display_name
2662
+ });
2663
+ }
1969
2664
  }
1970
2665
  } else if (message.content !== void 0 && message.content !== "") {
1971
2666
  blocks.push({ type: "text", content: message.content });
1972
2667
  }
1973
2668
  for (const toolCall of toolCalls) {
2669
+ if (storedToolCallIds.has(toolCall.id)) continue;
1974
2670
  blocks.push({
1975
2671
  type: "tool_use",
1976
- content: { name: toolCall.tool_name, arguments: toolCall.arguments },
1977
- tool_call_id: toolCall.id
2672
+ content: toolCall.arguments,
2673
+ tool_call_id: toolCall.id,
2674
+ tool_name: toolCall.tool_name,
2675
+ display_name: toolCall.display_name
1978
2676
  });
1979
2677
  }
1980
2678
  return blocks;
@@ -1995,14 +2693,11 @@ function markerTurn(id, sequence, loopId, type3, content, toolCallId = null) {
1995
2693
  duration_ms: 0
1996
2694
  };
1997
2695
  }
1998
- function isRecord3(value) {
1999
- return typeof value === "object" && value !== null && !Array.isArray(value);
2000
- }
2001
2696
  function stringOrNull(value) {
2002
2697
  return typeof value === "string" ? value : null;
2003
2698
  }
2004
- function numberOrZero(value) {
2005
- return typeof value === "number" ? value : 0;
2699
+ function numberOrNull(value) {
2700
+ return typeof value === "number" && value >= 0 ? value : null;
2006
2701
  }
2007
2702
  function findLatestAssistant(turns, loopId) {
2008
2703
  for (let index = turns.length - 1; index >= 0; index -= 1) {
@@ -2013,6 +2708,10 @@ function findLatestAssistant(turns, loopId) {
2013
2708
  }
2014
2709
 
2015
2710
  // src/resources/sessions.ts
2711
+ function isEntryGoneError(error) {
2712
+ const status = error?.status;
2713
+ return status === 404 || status === 409;
2714
+ }
2016
2715
  function toCreateSessionPayload(request) {
2017
2716
  const solutionRef = request.solution_ref ?? externalSolutionRef(request.solution_id);
2018
2717
  return {
@@ -2031,13 +2730,14 @@ function toCreateSessionPayload(request) {
2031
2730
  solution_snapshot_session_id: request.solution_snapshot_session_id ?? null,
2032
2731
  compaction_ratio: request.compaction_ratio ?? null,
2033
2732
  ...request.runtime_type ? { runtime_type: request.runtime_type } : {},
2034
- ...request.daemon_id ? { daemon_id: request.daemon_id } : {}
2733
+ ...request.daemon_id ? { daemon_id: request.daemon_id } : {},
2734
+ ...request.agent_runtime_id ? { agent_runtime_id: request.agent_runtime_id } : {},
2735
+ ...request.chat_group_id ? { chat_group_id: request.chat_group_id } : {}
2035
2736
  };
2036
2737
  }
2037
2738
  var builtinSolutionIds = /* @__PURE__ */ new Set([
2038
2739
  "app-dev",
2039
2740
  "general_chat",
2040
- "multica",
2041
2741
  "night_build",
2042
2742
  "skill_editor",
2043
2743
  "smart_bid",
@@ -2060,6 +2760,54 @@ var SessionsResource = class {
2060
2760
  this.client = client;
2061
2761
  }
2062
2762
  client;
2763
+ pendingImages = /* @__PURE__ */ new Map();
2764
+ /** Fetch an immutable message image using this client's authenticated identity. */
2765
+ fetchImage(sessionId, assetId, preview = true) {
2766
+ return this.fetchImagePath(`/api/sessions/${encodeURIComponent(sessionId)}/media/${encodeURIComponent(assetId)}`, preview);
2767
+ }
2768
+ /** Fetch a public-share image; the server validates the share token on every request. */
2769
+ fetchSharedImage(token, assetId, preview = true) {
2770
+ return this.fetchImagePath(`/api/share/${encodeURIComponent(token)}/media/${encodeURIComponent(assetId)}`, preview);
2771
+ }
2772
+ fetchImagePath(resource, preview) {
2773
+ const path = `${resource}${preview ? "?variant=preview" : ""}`;
2774
+ const identity = this.client.buildAuthedUrl(path);
2775
+ const existing = this.pendingImages.get(identity);
2776
+ if (existing) return existing;
2777
+ const pending = this.client.blob("GET", path).finally(() => this.pendingImages.delete(identity));
2778
+ this.pendingImages.set(identity, pending);
2779
+ return pending;
2780
+ }
2781
+ getMcpApp(sessionId, sourceId, init) {
2782
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}`, init);
2783
+ }
2784
+ onMcpAppChanged(sessionId, sourceId, listener) {
2785
+ let socket = this.client.socket();
2786
+ const changed = (payload) => {
2787
+ if (payload.session_id === sessionId && payload.source_id === sourceId) listener();
2788
+ };
2789
+ socket.on("mcp-app:changed", changed);
2790
+ socket.on("connect", listener);
2791
+ const off = this.client.onSocketReplaced(() => {
2792
+ socket.off("mcp-app:changed", changed);
2793
+ socket.off("connect", listener);
2794
+ socket = this.client.socket();
2795
+ socket.on("mcp-app:changed", changed);
2796
+ socket.on("connect", listener);
2797
+ });
2798
+ return () => {
2799
+ off();
2800
+ socket.off("mcp-app:changed", changed);
2801
+ socket.off("connect", listener);
2802
+ };
2803
+ }
2804
+ callMcpApp(sessionId, sourceId, request, init) {
2805
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/calls`, {
2806
+ ...init,
2807
+ method: "POST",
2808
+ body: JSON.stringify(request)
2809
+ });
2810
+ }
2063
2811
  /**
2064
2812
  * 连接一个已存在的会话,返回实时会话对象 AgentSession
2065
2813
  * (自动完成历史加载、Socket.IO 订阅、断线重连补数)。
@@ -2067,6 +2815,56 @@ var SessionsResource = class {
2067
2815
  connect(sessionId) {
2068
2816
  return this.client.hub.connect(sessionId);
2069
2817
  }
2818
+ listSessionPlugins(sessionId, init) {
2819
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins`, init);
2820
+ }
2821
+ setSessionPluginActivation(sessionId, name, active, init) {
2822
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}:activation`, { method: "POST", body: JSON.stringify({ active }), ...init });
2823
+ }
2824
+ /**
2825
+ * Fetch a plugin's package-local icon as bytes.
2826
+ *
2827
+ * The request carries the client's own bearer credentials, so callers turn
2828
+ * the blob into an object URL instead of pointing `<img>` at an API path and
2829
+ * hoping the request is authenticated.
2830
+ */
2831
+ fetchSessionPluginIcon(sessionId, name) {
2832
+ return this.client.blob("GET", `/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/icon`);
2833
+ }
2834
+ /** Authenticated URL for the icon; useful where a blob URL cannot be used. */
2835
+ sessionPluginIconUrl(sessionId, name) {
2836
+ return this.client.buildAuthedUrl(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/icon`);
2837
+ }
2838
+ /**
2839
+ * 准备一个插件的配置:把包准备好并读出用户的配置契约,**不启用**它。
2840
+ *
2841
+ * 开启插件前必须先走这一步:缺配置时用户只会看到配置表单,服务端不会先把
2842
+ * active 持久化成 true 再靠补偿关掉。它不修改会话的插件选择、不连接 MCP。
2843
+ */
2844
+ prepareSessionPluginConfig(sessionId, name, init) {
2845
+ return this.client.jsonFromInit(
2846
+ `/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}:prepare-config`,
2847
+ { method: "POST", ...init }
2848
+ );
2849
+ }
2850
+ /**
2851
+ * Read a plugin's configuration contract and the user's stored values.
2852
+ *
2853
+ * Secrets are never returned: `write_only` lists the pointers that hold one.
2854
+ */
2855
+ getSessionPluginConfig(sessionId, name, init) {
2856
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/config`, init);
2857
+ }
2858
+ /**
2859
+ * Publish explicit set/remove operations to the user's keychain file.
2860
+ *
2861
+ * Fields absent from both lists keep their stored value, so a form never has
2862
+ * to round-trip a secret. Rejections leave the previous file untouched; the
2863
+ * call never restarts MCP servers.
2864
+ */
2865
+ updateSessionPluginConfig(sessionId, name, update, init) {
2866
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/config`, { method: "PUT", body: JSON.stringify(update), ...init });
2867
+ }
2070
2868
  /** 创建新会话并直接连接,返回可收发消息的 AgentSession。 */
2071
2869
  async create(request = {}) {
2072
2870
  if (isSessionDefinition(request)) {
@@ -2163,7 +2961,8 @@ var SessionsResource = class {
2163
2961
  template_id_prefix,
2164
2962
  solution_id,
2165
2963
  solution_registry_id,
2166
- q
2964
+ q,
2965
+ chat_group_id
2167
2966
  }) {
2168
2967
  const searchParams = new URLSearchParams();
2169
2968
  searchParams.set("limit", String(limit));
@@ -2176,6 +2975,7 @@ var SessionsResource = class {
2176
2975
  }
2177
2976
  const trimmedQuery = q?.trim();
2178
2977
  if (trimmedQuery) searchParams.set("q", trimmedQuery);
2978
+ if (chat_group_id) searchParams.set("chat_group_id", chat_group_id);
2179
2979
  return this.client.json("GET", `/api/sessions?${searchParams.toString()}`);
2180
2980
  }
2181
2981
  async listSessions(template_id_prefix, solution_id) {
@@ -2195,6 +2995,11 @@ var SessionsResource = class {
2195
2995
  listSessionsWithSkillData() {
2196
2996
  return this.client.json("GET", "/api/sessions/with-skill-data");
2197
2997
  }
2998
+ searchOwnedContent(params) {
2999
+ const searchParams = new URLSearchParams({ q: params.q.trim() });
3000
+ if (params.limit !== void 0) searchParams.set("limit", String(params.limit));
3001
+ return this.client.json("GET", `/api/search?${searchParams.toString()}`);
3002
+ }
2198
3003
  createSession(intent, template_id, primary_skill_id) {
2199
3004
  return this.createSessionWithRequest({ intent, template_id, primary_skill_id });
2200
3005
  }
@@ -2261,9 +3066,68 @@ var SessionsResource = class {
2261
3066
  );
2262
3067
  return projectHistory(entries);
2263
3068
  }
2264
- getSessionContextStats(sessionId, init) {
2265
- return this.client.jsonFromInit(`/api/sessions/${sessionId}/context-stats`, init);
2266
- }
3069
+ async getSessionTurnsPage(sessionId, options = {}, init = {}) {
3070
+ const query = new URLSearchParams();
3071
+ if (options.limit !== void 0) query.set("limit", String(options.limit));
3072
+ if (options.before !== void 0) query.set("before", options.before);
3073
+ if (options.fromEntryId !== void 0) query.set("from_entry_id", options.fromEntryId);
3074
+ const suffix = query.size ? `?${query}` : "";
3075
+ const page = await this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/turns${suffix}`, init);
3076
+ return {
3077
+ turns: page.turns,
3078
+ nextBefore: page.next_before,
3079
+ currentMode: page.current_mode
3080
+ };
3081
+ }
3082
+ /**
3083
+ * 当前分支上可导航的用户发言目录(消息导航侧栏)。
3084
+ *
3085
+ * 只回 id、标题和时间:侧栏要列的是**整条会话**的发言,而窗口外的发言 id 只能
3086
+ * 从服务端拿;消息体仍然走 `/turns`。
3087
+ */
3088
+ async getSessionTurnIndex(sessionId) {
3089
+ const index = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turn-index`);
3090
+ return {
3091
+ sessionId: index.session_id,
3092
+ items: index.items.map((item) => ({
3093
+ entryId: item.entry_id,
3094
+ preview: item.preview,
3095
+ createdAt: item.created_at
3096
+ }))
3097
+ };
3098
+ }
3099
+ getMcpAppContext(sessionId, sourceId, init) {
3100
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/context`, init);
3101
+ }
3102
+ updateMcpAppContext(sessionId, sourceId, body, init) {
3103
+ return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/context`, {
3104
+ ...init,
3105
+ method: "POST",
3106
+ body: JSON.stringify(body)
3107
+ });
3108
+ }
3109
+ async getSessionTurnCommands(sessionId) {
3110
+ const commands = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turn-commands`);
3111
+ return commands.map((command) => ({
3112
+ deliveryId: command.delivery_id,
3113
+ toolCallId: command.tool_call_id ?? void 0,
3114
+ action: command.action,
3115
+ payload: command.payload
3116
+ }));
3117
+ }
3118
+ listResultFeedback(sessionId) {
3119
+ return this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/feedback`);
3120
+ }
3121
+ putResultFeedback(sessionId, assistantEntryId, feedback) {
3122
+ return this.client.json(
3123
+ "PUT",
3124
+ `/api/sessions/${encodeURIComponent(sessionId)}/turns/${encodeURIComponent(assistantEntryId)}/feedback`,
3125
+ feedback
3126
+ );
3127
+ }
3128
+ getSessionContextStats(sessionId, init) {
3129
+ return this.client.jsonFromInit(`/api/sessions/${sessionId}/context-stats`, init);
3130
+ }
2267
3131
  getSessionHistory(sessionId, init) {
2268
3132
  return this.client.jsonFromInit(`/api/sessions/${sessionId}/history`, init);
2269
3133
  }
@@ -2318,8 +3182,9 @@ var SessionsResource = class {
2318
3182
  `/api/sessions/${sessionId}/probe-url?url=${encodeURIComponent(url)}`
2319
3183
  );
2320
3184
  }
2321
- listDir(sessionId, dirPath) {
2322
- return this.client.json("GET", `/api/sessions/${sessionId}/ls/${encodeURIComponent(dirPath)}`);
3185
+ listDir(sessionId, dirPath, showHidden = false) {
3186
+ const query = showHidden ? "?show_hidden=true" : "";
3187
+ return this.client.json("GET", `/api/sessions/${sessionId}/ls/${encodeURIComponent(dirPath)}${query}`);
2323
3188
  }
2324
3189
  async countFiles(sessionId, dirPath = "") {
2325
3190
  const entries = await this.listDir(sessionId, dirPath);
@@ -2514,6 +3379,130 @@ function profileToCreateSessionRequest(profile) {
2514
3379
  };
2515
3380
  }
2516
3381
 
3382
+ // src/resources/plugins.ts
3383
+ var PluginsResource = class {
3384
+ constructor(client) {
3385
+ this.client = client;
3386
+ }
3387
+ client;
3388
+ /**
3389
+ * 列出当前用户安装的插件及其展示信息。
3390
+ *
3391
+ * 服务端可能为了补齐名称/图标惰性获取包内容,但绝不激活插件、不连 MCP;
3392
+ * 单个插件的展示信息读不出来只会降级成占位,不会让整份目录失败。
3393
+ */
3394
+ catalog(init) {
3395
+ return this.client.jsonFromInit("/api/plugins/catalog", init);
3396
+ }
3397
+ /** 取一个插件的包内图标字节(用户级,不需要会话)。 */
3398
+ fetchIcon(name) {
3399
+ return this.client.blob("GET", `/api/plugins/${encodeURIComponent(name)}/icon`);
3400
+ }
3401
+ /** 带凭据的图标地址;能直接用 blob URL 的地方优先用 `fetchIcon`。 */
3402
+ iconUrl(name) {
3403
+ return this.client.buildAuthedUrl(`/api/plugins/${encodeURIComponent(name)}/icon`);
3404
+ }
3405
+ };
3406
+
3407
+ // src/resources/chat-groups.ts
3408
+ var ChatGroupsResource = class {
3409
+ constructor(client) {
3410
+ this.client = client;
3411
+ }
3412
+ client;
3413
+ list() {
3414
+ return this.client.json("GET", "/api/chat-groups");
3415
+ }
3416
+ create(payload) {
3417
+ return this.client.json("POST", "/api/chat-groups", payload);
3418
+ }
3419
+ update(groupId, payload) {
3420
+ return this.client.json("PATCH", `/api/chat-groups/${encodeURIComponent(groupId)}`, payload);
3421
+ }
3422
+ delete(groupId, options) {
3423
+ const query = options?.deleteWorkspace ? "?delete_workspace=true" : "";
3424
+ return this.client.json("DELETE", `/api/chat-groups/${encodeURIComponent(groupId)}${query}`);
3425
+ }
3426
+ };
3427
+
3428
+ // src/resources/site-visits.ts
3429
+ var SITE_VISITS_PATH = "/api/site-visits";
3430
+ var REQUEST_TIMEOUT_MS = 5e3;
3431
+ var SiteVisitsResource = class {
3432
+ constructor(client) {
3433
+ this.client = client;
3434
+ }
3435
+ client;
3436
+ /** 上报一次页面访问,返回当前快照;不承诺本次已计入或已落盘。 */
3437
+ record() {
3438
+ return this.request("POST");
3439
+ }
3440
+ /** 只读快照:不设置访客 Cookie,也不增加任何计数。 */
3441
+ summary() {
3442
+ return this.request("GET");
3443
+ }
3444
+ async request(method) {
3445
+ const url = buildBackendUrl(this.client.options.baseUrl, SITE_VISITS_PATH);
3446
+ const controller = new AbortController();
3447
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
3448
+ try {
3449
+ const response = await (this.client.options.fetchImpl ?? fetch)(url.toString(), {
3450
+ method,
3451
+ // 访客 Cookie 是唯一的去重依据,必须随请求往返。
3452
+ credentials: "include",
3453
+ signal: controller.signal
3454
+ });
3455
+ if (!response.ok) {
3456
+ throw new BladeApiError(response, await extractErrorDetail(response));
3457
+ }
3458
+ return await response.json();
3459
+ } finally {
3460
+ clearTimeout(timeout);
3461
+ }
3462
+ }
3463
+ };
3464
+
3465
+ // src/shared/auth-busy.ts
3466
+ var DEFAULT_RETRY_AFTER_SECONDS = 1;
3467
+ var MAX_TIMER_DELAY_MS = 2147483647;
3468
+ function errorData(error) {
3469
+ if (typeof error !== "object" || error === null || !("data" in error)) return null;
3470
+ const data = error.data;
3471
+ return typeof data === "object" && data !== null ? data : null;
3472
+ }
3473
+ function authBusyRetryDelayMs(error) {
3474
+ const data = errorData(error);
3475
+ if (data?.code !== "auth_busy" || Number(data.status) !== 429) return null;
3476
+ const rawRetryAfter = data.retry_after;
3477
+ const seconds = rawRetryAfter == null || String(rawRetryAfter).trim() === "" ? Number.NaN : Number(rawRetryAfter);
3478
+ const delaySeconds = Number.isFinite(seconds) && seconds >= 0 ? seconds : DEFAULT_RETRY_AFTER_SECONDS;
3479
+ return Math.min(delaySeconds * 1e3, MAX_TIMER_DELAY_MS);
3480
+ }
3481
+ var AuthBusyReconnect = class {
3482
+ timer = null;
3483
+ get pending() {
3484
+ return this.timer !== null;
3485
+ }
3486
+ schedule(error, reconnect) {
3487
+ const delay = authBusyRetryDelayMs(error);
3488
+ if (delay === null) {
3489
+ this.cancel();
3490
+ return false;
3491
+ }
3492
+ this.cancel();
3493
+ this.timer = globalThis.setTimeout(() => {
3494
+ this.timer = null;
3495
+ reconnect();
3496
+ }, delay);
3497
+ return true;
3498
+ }
3499
+ cancel() {
3500
+ if (this.timer === null) return;
3501
+ globalThis.clearTimeout(this.timer);
3502
+ this.timer = null;
3503
+ }
3504
+ };
3505
+
2517
3506
  // src/commands/registry.ts
2518
3507
  var BUFFER_LIMIT_PER_ACTION = 20;
2519
3508
  var CommandRegistry = class {
@@ -2622,6 +3611,172 @@ var Emitter = class {
2622
3611
  }
2623
3612
  };
2624
3613
 
3614
+ // src/session/history-page.ts
3615
+ function sortedUnique(turns) {
3616
+ const byId = /* @__PURE__ */ new Map();
3617
+ for (const turn of turns) byId.set(turn.turn_id, turn);
3618
+ return [...byId.values()].sort((left, right) => left.sequence - right.sequence);
3619
+ }
3620
+ function prependOlder(current, page) {
3621
+ const currentById = new Map(current.map((turn) => [turn.turn_id, turn]));
3622
+ return sortedUnique([
3623
+ ...current,
3624
+ ...page.map((turn) => {
3625
+ const live = currentById.get(turn.turn_id);
3626
+ return live ? { ...live, sequence: turn.sequence } : turn;
3627
+ })
3628
+ ]);
3629
+ }
3630
+ function replaceWindow(current, page, liveRevisionAtStart, liveRevisions) {
3631
+ const pageById = new Map(page.map((turn) => [turn.turn_id, turn]));
3632
+ const newerLive = current.filter(
3633
+ (turn) => turn.turn_id.startsWith("local-user-") || (liveRevisions.byTurnId.get(turn.turn_id) ?? 0) > liveRevisionAtStart
3634
+ );
3635
+ return sortedUnique([
3636
+ ...page,
3637
+ ...newerLive.map((turn) => {
3638
+ const snapshot2 = pageById.get(turn.turn_id);
3639
+ return snapshot2 ? { ...turn, sequence: snapshot2.sequence } : turn;
3640
+ })
3641
+ ]);
3642
+ }
3643
+
3644
+ // src/session/queue.ts
3645
+ var EMPTY_SESSION_QUEUE = {
3646
+ session_id: "",
3647
+ revision: 0,
3648
+ paused: false,
3649
+ pause_reason: null,
3650
+ items: []
3651
+ };
3652
+ var QUEUE_FAILURE_MESSAGE = "\u961F\u5217\u64CD\u4F5C\u5931\u8D25";
3653
+ var QUEUED_MESSAGE_STATUSES = [
3654
+ "pending",
3655
+ "delivering",
3656
+ "claimed",
3657
+ "consumed",
3658
+ "cancelled"
3659
+ ];
3660
+ function toQueuedMessage(value) {
3661
+ if (!isRecord(value)) return null;
3662
+ const status = typeof value.status === "string" && QUEUED_MESSAGE_STATUSES.includes(value.status) ? value.status : null;
3663
+ 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) {
3664
+ return null;
3665
+ }
3666
+ const item = {
3667
+ id: value.id,
3668
+ content: value.content,
3669
+ position: value.position,
3670
+ version: value.version,
3671
+ status,
3672
+ created_at: value.created_at,
3673
+ updated_at: value.updated_at
3674
+ };
3675
+ if (typeof value.target_run_id === "string") item.target_run_id = value.target_run_id;
3676
+ if (typeof value.error === "string") item.error = value.error;
3677
+ return item;
3678
+ }
3679
+ function toQueueSnapshot(value, fallbackSessionId) {
3680
+ if (!isRecord(value)) return null;
3681
+ if (typeof value.revision !== "number" || !Number.isInteger(value.revision) || value.revision < 0) {
3682
+ return null;
3683
+ }
3684
+ if (typeof value.paused !== "boolean") return null;
3685
+ if (!Array.isArray(value.items)) return null;
3686
+ const pauseReason = value.pause_reason;
3687
+ if (pauseReason !== null && pauseReason !== void 0 && typeof pauseReason !== "string") {
3688
+ return null;
3689
+ }
3690
+ const items = [];
3691
+ for (const raw of value.items) {
3692
+ const item = toQueuedMessage(raw);
3693
+ if (!item) return null;
3694
+ items.push(item);
3695
+ }
3696
+ return {
3697
+ session_id: typeof value.session_id === "string" && value.session_id ? value.session_id : fallbackSessionId,
3698
+ revision: value.revision,
3699
+ paused: value.paused,
3700
+ pause_reason: typeof pauseReason === "string" ? pauseReason : null,
3701
+ items
3702
+ };
3703
+ }
3704
+ function isSessionQueueSnapshot(value) {
3705
+ if (!isRecord(value) || typeof value.session_id !== "string") return false;
3706
+ return toQueueSnapshot(value, value.session_id) !== null;
3707
+ }
3708
+ function shouldApplySnapshot(current, next) {
3709
+ if (!isSessionQueueSnapshot(next)) return false;
3710
+ if (current?.session_id && next.session_id !== current.session_id) return false;
3711
+ return next.revision > (current?.revision ?? -1);
3712
+ }
3713
+ function parseQueueAck(response, fallbackSessionId) {
3714
+ if (!isRecord(response)) {
3715
+ return { ok: false, conflict: false, snapshot: null, message: QUEUE_FAILURE_MESSAGE };
3716
+ }
3717
+ const message = typeof response.message === "string" && response.message ? response.message : null;
3718
+ const code = typeof response.code === "string" && response.code ? response.code : void 0;
3719
+ const status = response.status;
3720
+ if (status !== "ok" && status !== "conflict" && status !== "queued") {
3721
+ return {
3722
+ ok: false,
3723
+ conflict: false,
3724
+ snapshot: null,
3725
+ message: message ?? QUEUE_FAILURE_MESSAGE,
3726
+ ...code ? { code } : {}
3727
+ };
3728
+ }
3729
+ const snapshot2 = toQueueSnapshot(response, fallbackSessionId);
3730
+ if (status === "conflict") {
3731
+ return {
3732
+ ok: false,
3733
+ conflict: true,
3734
+ snapshot: snapshot2,
3735
+ message: message ?? QUEUE_FAILURE_MESSAGE,
3736
+ ...code ? { code } : {}
3737
+ };
3738
+ }
3739
+ if (!snapshot2) {
3740
+ return {
3741
+ ok: false,
3742
+ conflict: false,
3743
+ snapshot: null,
3744
+ message: message ?? QUEUE_FAILURE_MESSAGE,
3745
+ ...code ? { code } : {}
3746
+ };
3747
+ }
3748
+ return { ok: true, conflict: false, snapshot: snapshot2, message, ...code ? { code } : {} };
3749
+ }
3750
+ function canEditQueuedMessage(item) {
3751
+ return item.status === "pending";
3752
+ }
3753
+ function canCancelQueuedMessage(item) {
3754
+ return item.status === "pending";
3755
+ }
3756
+ function canDeliverQueuedMessage(item) {
3757
+ return item.status === "pending";
3758
+ }
3759
+ var QUEUED_MESSAGE_STATUS_LABELS = {
3760
+ pending: "\u5F85\u6267\u884C",
3761
+ delivering: "\u7B49\u5F85\u63A5\u6536",
3762
+ claimed: "",
3763
+ consumed: "",
3764
+ cancelled: ""
3765
+ };
3766
+ function queuedMessageStatusLabel(status) {
3767
+ return QUEUED_MESSAGE_STATUS_LABELS[status] ?? "";
3768
+ }
3769
+ var QUEUE_PAUSE_REASON_LABELS = {
3770
+ user_stop: "\u5DF2\u6682\u505C",
3771
+ run_failed: "\u4EFB\u52A1\u5F02\u5E38\u5DF2\u6682\u505C",
3772
+ run_unknown: "\u4EFB\u52A1\u7ED3\u675F\u72B6\u6001\u672A\u77E5\uFF0C\u5DF2\u6682\u505C",
3773
+ server_restart: "\u670D\u52A1\u91CD\u542F\u5DF2\u6682\u505C"
3774
+ };
3775
+ function queuePauseReasonLabel(reason) {
3776
+ if (!reason) return null;
3777
+ return QUEUE_PAUSE_REASON_LABELS[reason] ?? reason;
3778
+ }
3779
+
2625
3780
  // src/schemas/message-utils.ts
2626
3781
  var SYSTEM_REMINDER_PATTERN = /^<system-reminder>\s*[\s\S]*?\s*<\/system-reminder>$/i;
2627
3782
  var SYSTEM_NOTIFICATION_PATTERN = /^<system-notification>\s*[\s\S]*?\s*<\/system-notification>$/i;
@@ -2630,6 +3785,36 @@ var LEGACY_FORK_CONTEXT_MARKERS = [
2630
3785
  "\u53EF\u901A\u8FC7 `read_file` \u8BFB\u53D6\u3002",
2631
3786
  "# \u4F60\u7684\u4EFB\u52A1"
2632
3787
  ];
3788
+ var INTERNAL_CHAT_ERROR_PATTERNS = [
3789
+ /\b(?:ENOENT|EACCES|EPERM)\b/i,
3790
+ /\[(?:Errno -?\d+|WinError \d+)\]/i,
3791
+ /\b(?:ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|ENOTFOUND|EAI_AGAIN)\b/i,
3792
+ /\b(?:Connection refused|Connection reset by peer|All connection attempts failed|getaddrinfo failed)\b/i,
3793
+ /(?:^|[\s("'`])\/(?:root|home|Users|tmp|var|opt|etc)\//,
3794
+ /[A-Za-z]:\\(?:Users|Windows|Program Files|AppData)\\/i,
3795
+ /(?:^|\n)Traceback \(most recent call last\):/,
3796
+ /(?:^|\n)\s*at\s+\S+(?:\s+\(|:\d+:\d+)/,
3797
+ /(?:^|\n)File "[^"]+", line \d+/
3798
+ ];
3799
+ var SAFE_CHAT_ERROR_PREFIXES = [
3800
+ "\u6C99\u76D2\u8FD0\u884C\u73AF\u5883\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
3801
+ "\u6A21\u578B\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u68C0\u67E5\u6A21\u578B\u914D\u7F6E\u540E\u91CD\u8BD5\u3002"
3802
+ ];
3803
+ var GENERIC_CHAT_ERROR = "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002";
3804
+ function chatErrorForDisplay(message, developer = false) {
3805
+ const normalized = message.trim();
3806
+ if (!developer) {
3807
+ const safePrefix = SAFE_CHAT_ERROR_PREFIXES.find((prefix) => normalized.startsWith(prefix));
3808
+ if (safePrefix) return safePrefix;
3809
+ }
3810
+ if (!developer && /no source matches this model/i.test(normalized)) {
3811
+ return "\u5F53\u524D\u6A21\u578B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u6216\u91CD\u65B0\u9009\u62E9\u6A21\u578B\u3002";
3812
+ }
3813
+ if (!developer && INTERNAL_CHAT_ERROR_PATTERNS.some((pattern) => pattern.test(normalized))) {
3814
+ return GENERIC_CHAT_ERROR;
3815
+ }
3816
+ return normalized || GENERIC_CHAT_ERROR;
3817
+ }
2633
3818
  function normalizeMessageContent(content) {
2634
3819
  if (typeof content === "string") return content;
2635
3820
  if (Array.isArray(content)) return content;
@@ -2649,10 +3834,10 @@ function isLegacyForkContextText(text) {
2649
3834
  const normalized = text.trim();
2650
3835
  return LEGACY_FORK_CONTEXT_MARKERS.every((marker) => normalized.includes(marker));
2651
3836
  }
2652
- function isHiddenInternalMessage(message) {
3837
+ function isHiddenInternalMessage(message, options = {}) {
2653
3838
  if (message.role !== "user") return false;
2654
3839
  const text = getTextContent(normalizeMessageContent(message.content));
2655
- return isSystemReminderText(text) || isSystemNotificationText(text) || isLegacyForkContextText(text);
3840
+ return isSystemReminderText(text) || isSystemNotificationText(text) || (options.includeLegacyForkContext ?? true) && isLegacyForkContextText(text);
2656
3841
  }
2657
3842
  function buildMessageContent(text, attachments) {
2658
3843
  if (attachments.length === 0) return text;
@@ -2775,6 +3960,12 @@ function groupMessagesByLoop(messages) {
2775
3960
  }
2776
3961
  return groups;
2777
3962
  }
3963
+ function isBackgroundWakeMessage(message) {
3964
+ return message.blocks?.some((block) => {
3965
+ const content = block.content;
3966
+ return block.type === "system_notification" && content !== null && typeof content === "object" && "notification_type" in content && content.notification_type === "bg:wakeup";
3967
+ }) ?? false;
3968
+ }
2778
3969
 
2779
3970
  // src/session/state.ts
2780
3971
  function toReplaySnapshot(raw) {
@@ -2799,7 +3990,10 @@ function createInitialSessionState(sessionId) {
2799
3990
  agentLoops: {},
2800
3991
  activeCompaction: null,
2801
3992
  replay: null,
2802
- viewerRole: null
3993
+ queue: EMPTY_SESSION_QUEUE,
3994
+ viewerRole: null,
3995
+ nextBefore: null,
3996
+ loadingOlder: false
2803
3997
  };
2804
3998
  }
2805
3999
  function parseAgentDescription(argumentsJson) {
@@ -2825,10 +4019,10 @@ function inferLoopStatusFromMessages(messages) {
2825
4019
  }
2826
4020
  function inferLoopStatusFromTurns(turns, messages) {
2827
4021
  const latestAgentNotification = [...turns].reverse().flatMap((turn) => turn.blocks).find((block) => {
2828
- if (block.type !== "system_notification" || !isRecord4(block.content)) return false;
4022
+ if (block.type !== "system_notification" || !isRecord(block.content)) return false;
2829
4023
  return block.content.notification_type === "agent:start" || block.content.notification_type === "agent:end";
2830
4024
  });
2831
- if (latestAgentNotification?.type === "system_notification" && isRecord4(latestAgentNotification.content)) {
4025
+ if (latestAgentNotification?.type === "system_notification" && isRecord(latestAgentNotification.content)) {
2832
4026
  const notificationType = latestAgentNotification.content.notification_type;
2833
4027
  const status = latestAgentNotification.content.status;
2834
4028
  if (notificationType === "agent:start" || status === "running") return "running";
@@ -2837,17 +4031,44 @@ function inferLoopStatusFromTurns(turns, messages) {
2837
4031
  }
2838
4032
  return inferLoopStatusFromMessages(messages);
2839
4033
  }
2840
- function isRecord4(value) {
2841
- return typeof value === "object" && value !== null && !Array.isArray(value);
4034
+ function toSelectionMap(value) {
4035
+ if (!isRecord(value)) return {};
4036
+ const entries = Object.entries(value).map(([questionKey, optionIndexes]) => {
4037
+ if (!Array.isArray(optionIndexes)) return null;
4038
+ const parsedIndexes = optionIndexes.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item));
4039
+ return [Number(questionKey), parsedIndexes];
4040
+ }).filter((entry) => entry !== null);
4041
+ return Object.fromEntries(entries);
4042
+ }
4043
+ function toCustomMap(value) {
4044
+ if (!isRecord(value)) return {};
4045
+ const entries = Object.entries(value).filter(([, text]) => typeof text === "string").map(([questionKey, text]) => [Number(questionKey), text]);
4046
+ return Object.fromEntries(entries);
4047
+ }
4048
+ function extractAskAnswers(turns) {
4049
+ const answers = {};
4050
+ for (const turn of turns) {
4051
+ for (const block of turn.blocks) {
4052
+ if (block.type !== "ask_user_answer" || typeof block.tool_call_id !== "string") continue;
4053
+ if (!isRecord(block.content)) continue;
4054
+ const note = typeof block.content.note === "string" ? block.content.note.trim() : "";
4055
+ answers[block.tool_call_id] = {
4056
+ selections: toSelectionMap(block.content.selections),
4057
+ custom: toCustomMap(block.content.custom),
4058
+ ...note ? { note } : {}
4059
+ };
4060
+ }
4061
+ }
4062
+ return answers;
2842
4063
  }
2843
4064
  function parentForkToolCallIdFromTurn(turn) {
2844
4065
  if (typeof turn.parent_fork_tool_call_id === "string" && turn.parent_fork_tool_call_id.length > 0) {
2845
4066
  return turn.parent_fork_tool_call_id;
2846
4067
  }
2847
4068
  for (const block of turn.blocks) {
2848
- if (block.type !== "system_notification" || !isRecord4(block.content)) continue;
4069
+ if (block.type !== "system_notification" || !isRecord(block.content)) continue;
2849
4070
  const metadata = block.content.metadata;
2850
- if (!isRecord4(metadata)) continue;
4071
+ if (!isRecord(metadata)) continue;
2851
4072
  const parentId = metadata.parent_fork_tool_call_id;
2852
4073
  if (typeof parentId === "string" && parentId.length > 0) return parentId;
2853
4074
  }
@@ -2864,16 +4085,16 @@ function buildMessageContent2(turn) {
2864
4085
  }
2865
4086
  function workspaceNotificationContent(turn) {
2866
4087
  const block = turn.blocks.find(
2867
- (candidate) => candidate.type === "system_notification" && isRecord4(candidate.content) && candidate.content.notification_type === "workspace_change"
4088
+ (candidate) => candidate.type === "system_notification" && isRecord(candidate.content) && candidate.content.notification_type === "workspace_change"
2868
4089
  );
2869
- if (!block || !isRecord4(block.content)) return "";
4090
+ if (!block || !isRecord(block.content)) return "";
2870
4091
  const title = typeof block.content.title === "string" ? block.content.title.trim() : "";
2871
4092
  const detail = typeof block.content.detail === "string" ? block.content.detail.trim() : "";
2872
4093
  return [title ? `**${title}**` : "", detail].filter(Boolean).join("\n\n");
2873
4094
  }
2874
4095
  function askUserAnswerContent(turn) {
2875
4096
  const answerBlock = turn.blocks.find((block) => block.type === "ask_user_answer");
2876
- if (!answerBlock || !isRecord4(answerBlock.content)) return null;
4097
+ if (!answerBlock || !isRecord(answerBlock.content)) return null;
2877
4098
  const answer = answerBlock.content.answer;
2878
4099
  return typeof answer === "string" && answer.trim().length > 0 ? answer : null;
2879
4100
  }
@@ -2893,6 +4114,19 @@ function extractModeFromBlocks(blocks) {
2893
4114
  return null;
2894
4115
  }
2895
4116
  function projectionToMessage(turn) {
4117
+ if (turn.kind === "context") {
4118
+ const context = contextProjectionData(turn);
4119
+ if (!context) return null;
4120
+ return {
4121
+ role: "assistant",
4122
+ content: "",
4123
+ kind: "context",
4124
+ context,
4125
+ loop_name: turn.loop_id,
4126
+ entry_id: turn.turn_id,
4127
+ status: turn.status
4128
+ };
4129
+ }
2896
4130
  if (turn.kind === "compaction" && turn.compaction_id) {
2897
4131
  return {
2898
4132
  role: "assistant",
@@ -3062,6 +4296,62 @@ function orderTurns(turns) {
3062
4296
  }
3063
4297
  return turns;
3064
4298
  }
4299
+ function reconcileOptimisticUserTurns(currentTurns, incomingTurns) {
4300
+ return reconcileUserTurns(currentTurns, incomingTurns, true, false);
4301
+ }
4302
+ function reconcileHistoricalUserTurns(currentTurns, incomingTurns) {
4303
+ return reconcileUserTurns(currentTurns, incomingTurns, false, true);
4304
+ }
4305
+ function reconcileUserTurns(currentTurns, incomingTurns, allowLiveFallback, keepUnclaimedOptimistic) {
4306
+ const preservedTurns = preserveClientRenderIds(currentTurns, incomingTurns);
4307
+ const optimisticUserTurns = currentTurns.filter(
4308
+ (turn) => turn.role === "user" && (turn.kind ?? "message") === "message" && turn.turn_id.startsWith("local-user-")
4309
+ );
4310
+ const optimisticByRequestId = new Map(
4311
+ optimisticUserTurns.map((turn) => [
4312
+ turn.turn_id.slice("local-user-".length),
4313
+ turn
4314
+ ])
4315
+ );
4316
+ const currentTurnIds = new Set(currentTurns.map((turn) => turn.turn_id));
4317
+ const claimedOptimisticIds = /* @__PURE__ */ new Set();
4318
+ const reconciled = preservedTurns.map((turn) => {
4319
+ const exactOptimistic = typeof turn.client_request_id === "string" ? optimisticByRequestId.get(turn.client_request_id) : void 0;
4320
+ if (exactOptimistic && !claimedOptimisticIds.has(exactOptimistic.turn_id) && isUserConfirmation(turn)) {
4321
+ claimedOptimisticIds.add(exactOptimistic.turn_id);
4322
+ return { ...turn, client_render_id: exactOptimistic.turn_id };
4323
+ }
4324
+ const fallbackOptimistic = optimisticUserTurns.find(
4325
+ (candidate) => !claimedOptimisticIds.has(candidate.turn_id)
4326
+ );
4327
+ if (allowLiveFallback && typeof turn.client_request_id !== "string" && fallbackOptimistic && isUserConfirmation(turn) && !turn.turn_id.startsWith("local-user-") && !currentTurnIds.has(turn.turn_id)) {
4328
+ claimedOptimisticIds.add(fallbackOptimistic.turn_id);
4329
+ return { ...turn, client_render_id: fallbackOptimistic.turn_id };
4330
+ }
4331
+ return turn;
4332
+ });
4333
+ const result = claimedOptimisticIds.size === 0 ? reconciled : reconciled.filter((turn) => !claimedOptimisticIds.has(turn.turn_id));
4334
+ if (!keepUnclaimedOptimistic) return result;
4335
+ const resultIds = new Set(result.map((turn) => turn.turn_id));
4336
+ const unclaimedOptimisticTurns = optimisticUserTurns.filter(
4337
+ (turn) => !claimedOptimisticIds.has(turn.turn_id) && !resultIds.has(turn.turn_id)
4338
+ );
4339
+ return unclaimedOptimisticTurns.length === 0 ? result : orderTurns([...result, ...unclaimedOptimisticTurns]);
4340
+ }
4341
+ function isUserConfirmation(turn) {
4342
+ return turn.role === "user" && (turn.kind ?? "message") === "message" || turn.blocks.some((block) => block.type === "ask_user_answer");
4343
+ }
4344
+ function preserveClientRenderIds(currentTurns, incomingTurns) {
4345
+ const currentById = new Map(currentTurns.map((turn) => [turn.turn_id, turn]));
4346
+ return incomingTurns.map((turn) => {
4347
+ const current = currentById.get(turn.turn_id);
4348
+ if (current) {
4349
+ const currentRenderId = current.client_render_id;
4350
+ return turn.client_render_id || !currentRenderId ? turn : { ...turn, client_render_id: currentRenderId };
4351
+ }
4352
+ return turn;
4353
+ });
4354
+ }
3065
4355
  function areAgentLoopsEqual(left, right) {
3066
4356
  if (!left) return Object.keys(right).length === 0;
3067
4357
  const leftEntries = Object.entries(left);
@@ -3073,7 +4363,7 @@ function areAgentLoopsEqual(left, right) {
3073
4363
  });
3074
4364
  }
3075
4365
  function withTurns(state, turns) {
3076
- const orderedTurns = orderTurns(turns);
4366
+ const orderedTurns = orderTurns(reconcileHistoricalUserTurns(state.turns, turns));
3077
4367
  const { messages, agentLoops, activeCompaction } = materialize(orderedTurns);
3078
4368
  const isWaitingForInput = orderedTurns.some(
3079
4369
  (turn) => turn.tool_calls.some((toolCall) => toolCall.status === "awaiting_answer")
@@ -3083,28 +4373,53 @@ function withTurns(state, turns) {
3083
4373
  const preservedErrors = lastTurnId ? state.messages.filter(
3084
4374
  (m) => m.role === "error" && typeof m.entry_id === "string" && m.entry_id.startsWith(`${ERROR_ANCHOR_PREFIX}${lastTurnId}:`)
3085
4375
  ) : [];
3086
- const mergedMessages = preservedErrors.length > 0 ? [...messages, ...preservedErrors] : messages;
4376
+ const turnById = new Map(orderedTurns.map((turn) => [turn.turn_id, turn]));
4377
+ const messagesWithRenderIds = messages.map((message) => {
4378
+ if (!message.entry_id) return message;
4379
+ const renderId = turnById.get(message.entry_id)?.client_render_id;
4380
+ return renderId ? { ...message, render_id: renderId } : message;
4381
+ });
4382
+ const mergedMessages = preservedErrors.length > 0 ? [...messagesWithRenderIds, ...preservedErrors] : messagesWithRenderIds;
3087
4383
  const latestMode = [...orderedTurns].reverse().map((turn) => extractModeFromBlocks(turn.blocks)).find((mode) => mode !== null);
4384
+ const askAnswersFromHistory = extractAskAnswers(orderedTurns);
4385
+ const presentToolCallIds = new Set(
4386
+ orderedTurns.flatMap((turn) => turn.tool_calls.map((toolCall) => toolCall.id))
4387
+ );
4388
+ const askAnswers = { ...askAnswersFromHistory };
4389
+ for (const [toolCallId, answer] of Object.entries(state.askAnswers)) {
4390
+ if (!(toolCallId in askAnswers) && presentToolCallIds.has(toolCallId)) {
4391
+ askAnswers[toolCallId] = answer;
4392
+ }
4393
+ }
3088
4394
  return {
3089
4395
  ...state,
3090
4396
  turns: orderedTurns,
3091
4397
  messages: mergedMessages,
3092
4398
  agentLoops: stableAgentLoops,
3093
4399
  activeCompaction,
4400
+ askAnswers,
3094
4401
  mode: latestMode ?? state.mode,
3095
4402
  // 历史恢复时 chat:end(paused) 可能已不在 socket replay 缓存中;
3096
4403
  // awaiting_answer 是投影里的持久权威状态,必须据此恢复问答交互。
3097
4404
  status: isWaitingForInput ? "waiting_for_input" : state.status
3098
4405
  };
3099
4406
  }
4407
+ function withQueue(state, snapshot2) {
4408
+ if (!shouldApplySnapshot(state.queue, snapshot2)) return state;
4409
+ return { ...state, queue: snapshot2 };
4410
+ }
3100
4411
  var localTurnCounter = 0;
3101
- function addUserMessage(state, content) {
4412
+ function addUserMessage(state, content, clientRequestId) {
3102
4413
  localTurnCounter += 1;
3103
- const turnId = `local-user-${Date.now()}-${localTurnCounter}`;
4414
+ const turnId = `local-user-${clientRequestId ?? `${Date.now()}-${localTurnCounter}`}`;
3104
4415
  const optimisticTurn = {
3105
4416
  id: turnId,
3106
4417
  sequence: Math.max(0, ...state.turns.map((turn) => turn.sequence)) + 1,
3107
4418
  turn_id: turnId,
4419
+ // 乐观态就自赋,跟服务端确认后 reconcileUserTurns 补上的
4420
+ // client_render_id(= 乐观 turn 自己的 turn_id)保持同一个值,
4421
+ // 消费方才能拿它当"确认前后不变"的稳定标识使用。
4422
+ client_render_id: turnId,
3108
4423
  loop_id: "root",
3109
4424
  role: "user",
3110
4425
  status: "streaming",
@@ -3118,16 +4433,18 @@ function addUserMessage(state, content) {
3118
4433
  }
3119
4434
  function upsertTurn(state, turn) {
3120
4435
  let existing = [...state.turns];
3121
- if (turn.role === "user" && !turn.turn_id.startsWith("local-user-")) {
3122
- existing = existing.filter((t) => !t.turn_id.startsWith("local-user-"));
4436
+ const [nextTurn] = reconcileOptimisticUserTurns(existing, [turn]);
4437
+ const claimedOptimisticId = nextTurn.client_render_id;
4438
+ if (claimedOptimisticId?.startsWith("local-user-")) {
4439
+ existing = existing.filter((item) => item.turn_id !== claimedOptimisticId);
3123
4440
  }
3124
- const index = existing.findIndex((item) => item.turn_id === turn.turn_id);
4441
+ const index = existing.findIndex((item) => item.turn_id === nextTurn.turn_id);
3125
4442
  if (index >= 0) {
3126
- existing[index] = turn;
4443
+ existing[index] = nextTurn;
3127
4444
  } else {
3128
- existing.push(turn);
4445
+ existing.push(nextTurn);
3129
4446
  }
3130
- return withTurns(state, existing);
4447
+ return withTurns({ ...state, turns: existing }, existing);
3131
4448
  }
3132
4449
  function applyTurnPatch(state, patch) {
3133
4450
  const turn = patch.data.turn;
@@ -3138,13 +4455,7 @@ function applyTurnPatch(state, patch) {
3138
4455
  if (lastSequence !== null && patch.sequence <= lastSequence) {
3139
4456
  return state;
3140
4457
  }
3141
- const nextTurn = { ...turn, sequence: patch.sequence };
3142
- if (index >= 0) {
3143
- existing[index] = nextTurn;
3144
- } else {
3145
- existing.push(nextTurn);
3146
- }
3147
- return withTurns(state, existing);
4458
+ return upsertTurn(state, { ...turn, sequence: patch.sequence });
3148
4459
  }
3149
4460
  function addErrorMessage(state, content) {
3150
4461
  const anchorTurnId = state.turns[state.turns.length - 1]?.turn_id ?? null;
@@ -3174,21 +4485,49 @@ function markStreamingTurns(state, turnStatus, toolCallStatus) {
3174
4485
  return withTurns(state, turns);
3175
4486
  }
3176
4487
 
4488
+ // src/session/errors.ts
4489
+ var AgentSessionError = class extends Error {
4490
+ name = "AgentSessionError";
4491
+ code;
4492
+ constructor(message, code) {
4493
+ super(message);
4494
+ this.code = code;
4495
+ }
4496
+ };
4497
+
3177
4498
  // src/session/agent-session.ts
3178
4499
  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";
3179
- var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
3180
- function isRecord5(value) {
3181
- return typeof value === "object" && value !== null && !Array.isArray(value);
4500
+ var COMMAND_RESTORE_RETRY_BASE_MS = 1e3;
4501
+ var COMMAND_RESTORE_RETRY_MAX_MS = 3e4;
4502
+ var QUEUE_ACK_TIMEOUT_MS = 1e4;
4503
+ var fallbackRequestCounter = 0;
4504
+ function createClientRequestId() {
4505
+ if (typeof globalThis.crypto?.randomUUID === "function") {
4506
+ return globalThis.crypto.randomUUID();
4507
+ }
4508
+ fallbackRequestCounter += 1;
4509
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}-${fallbackRequestCounter}`;
3182
4510
  }
4511
+ var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
3183
4512
  function isOomText(value) {
3184
4513
  return typeof value === "string" && OOM_KEYWORDS_RE.test(value);
3185
4514
  }
4515
+ function describeSessionError(error, fallback) {
4516
+ if (error instanceof AgentSessionError) {
4517
+ return {
4518
+ message: error.message || fallback,
4519
+ ...error.code ? { code: error.code } : {}
4520
+ };
4521
+ }
4522
+ if (error instanceof Error && error.message) return { message: error.message };
4523
+ return { message: fallback };
4524
+ }
3186
4525
  function parseJsonRecord(value) {
3187
- if (isRecord5(value)) return value;
4526
+ if (isRecord(value)) return value;
3188
4527
  if (typeof value !== "string") return null;
3189
4528
  try {
3190
4529
  const parsed = JSON.parse(value);
3191
- return isRecord5(parsed) ? parsed : null;
4530
+ return isRecord(parsed) ? parsed : null;
3192
4531
  } catch {
3193
4532
  return null;
3194
4533
  }
@@ -3214,6 +4553,21 @@ function isOomResult(value) {
3214
4553
  function isToolBridgeContent(value) {
3215
4554
  return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.action === "string";
3216
4555
  }
4556
+ function isCursorInvalidError(error) {
4557
+ return typeof error === "object" && error !== null && "status" in error && error.status === 409;
4558
+ }
4559
+ function queueRequestFailure(error) {
4560
+ const failure = error instanceof AgentSessionError ? { message: error.message, ...error.code ? { code: error.code } : {} } : { message: error instanceof Error && error.message ? error.message : "\u961F\u5217\u64CD\u4F5C\u5931\u8D25" };
4561
+ return {
4562
+ ok: false,
4563
+ conflict: false,
4564
+ snapshot: null,
4565
+ ...failure
4566
+ };
4567
+ }
4568
+ function commandDeliveryKey(deliveryId) {
4569
+ return `tool-command:${deliveryId}`;
4570
+ }
3217
4571
  function sessionStatusFromChatEnd(status) {
3218
4572
  if (status === "paused") return "waiting_for_input";
3219
4573
  if (status === "interrupted") return "interrupted";
@@ -3229,14 +4583,6 @@ function extractTerminalChatEndStatus(events) {
3229
4583
  }
3230
4584
  return null;
3231
4585
  }
3232
- function hasChatRunEvent(events) {
3233
- return events.some(
3234
- (event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
3235
- );
3236
- }
3237
- function isUiMetaLike(value) {
3238
- return isRecord5(value) && ("resourceHTML" in value || "resourceUri" in value || "resourceURI" in value);
3239
- }
3240
4586
  var AgentSession = class _AgentSession {
3241
4587
  sessionId;
3242
4588
  state;
@@ -3250,14 +4596,55 @@ var AgentSession = class _AgentSession {
3250
4596
  patchFlushCancel = null;
3251
4597
  joined = false;
3252
4598
  joinPending = null;
4599
+ // bootstrap 与每次 reconnect 共用一条订阅生命周期队列。它们都会重置 join
4600
+ // 状态并临时设置 replay source,禁止并发,否则较早操作的 finally 会清掉
4601
+ // 较晚操作仍在使用的来源上下文。
4602
+ subscriptionQueue = Promise.resolve();
4603
+ replaceQueue = Promise.resolve();
4604
+ timelineEpoch = 0;
4605
+ /** 定位请求的流水号:被后一次选择取代的那次定位不再提交窗口(见 locateEntry)。 */
4606
+ locateToken = 0;
4607
+ /** 在途那次定位的请求句柄。**只**服务定位:权威刷新不共用它(见 cancelPendingLocate)。 */
4608
+ locateAbort = null;
4609
+ pagingGeneration = 0;
4610
+ liveRevision = 0;
4611
+ modeRevision = 0;
4612
+ liveRevisions = /* @__PURE__ */ new Map();
4613
+ pendingOptimisticTurnIds = /* @__PURE__ */ new Set();
4614
+ olderPending = null;
4615
+ replaceInFlight = 0;
3253
4616
  // 回放控制请求排成一条队列。连着点"5x"再点"退出回放"时,并发发出去的两个
3254
4617
  // 请求响应可能乱序回来,晚到的旧值会把已经退出的会话又显示成回放中;用
3255
4618
  // 序号丢弃过期响应又会在失败时留下中间态。串行执行就没有这些情况:每个
3256
4619
  // 响应都是当时最新的,失败也只影响自己。
3257
4620
  replayQueue = Promise.resolve();
4621
+ // Historical snapshots and live commands share one delivery barrier. A
4622
+ // repair closes the barrier before joining, snapshots the raw command log
4623
+ // after the join, then releases buffered commands in socket arrival order.
4624
+ commandRestoreQueue = Promise.resolve();
4625
+ commandRecoveryGeneration = 0;
4626
+ commandRestoreRetryAttempt = 0;
4627
+ commandRestoreRetryHandle = null;
4628
+ bufferedCommandDeliveries = null;
3258
4629
  pendingReplayMessage = null;
3259
4630
  pendingReplayMode;
4631
+ pendingReplayKbIds;
3260
4632
  recentChatEndAt = 0;
4633
+ /**
4634
+ * 最近一次已经通过 `error` 事件投递过的服务端失败(`message` + `code`)。
4635
+ *
4636
+ * `chat:send` 被代理时服务端会先发 `system:error`、再用同一条失败回 ack,两条
4637
+ * 通道都会到达这里:前者走 `_handleSystemError`,后者走发送失败的回滚。不去重
4638
+ * 的话调用方会收到两次同一个 `error`(重复提示、重试逻辑跑两遍)。
4639
+ *
4640
+ * 带代际边界,只认「本次 `emitWithAck` 等待期间」投递的那一条:否则队列/停止
4641
+ * 失败留下的标记会一直挂着,之后一次无关的订阅失败只是文案码相同就被当成
4642
+ * 重复而静音。用单调递增的代际号而不是时间戳,同一毫秒内的两条也不会混淆。
4643
+ */
4644
+ lastDeliveredSystemError = null;
4645
+ /** 每次投递 `system:error` 递增,用于判定某条失败是否属于本次等待。 */
4646
+ systemErrorGeneration = 0;
4647
+ subscriptionReplaySource = null;
3261
4648
  disposed = false;
3262
4649
  getAppContext = null;
3263
4650
  sendPending = false;
@@ -3268,11 +4655,19 @@ var AgentSession = class _AgentSession {
3268
4655
  seenToolCallIds = /* @__PURE__ */ new Set();
3269
4656
  finishedToolCallIds = /* @__PURE__ */ new Set();
3270
4657
  emittedMessageTurnIds = /* @__PURE__ */ new Set();
3271
- constructor(sessionId, runtime, connection) {
4658
+ constructor(sessionId, runtime, connection, options = {}) {
3272
4659
  this.sessionId = sessionId;
3273
4660
  this.runtime = runtime;
3274
4661
  this.state = { ...createInitialSessionState(sessionId), connection };
4662
+ this.isStatic = options.staticHistory === true;
3275
4663
  }
4664
+ /**
4665
+ * 静态历史(分享页 / `fromTurns`):没有目录接口,也**没有**"目录还没到位"这个中间态。
4666
+ *
4667
+ * 宿主据此决定要不要拿已加载窗口当兜底条目——真实会话在 timeline 被重写后,窗口里还是
4668
+ * 旧分支的发言,拿它兜底会把已经失效的 entry id 重新摆到轨道上,点了必然找不到。
4669
+ */
4670
+ isStatic;
3276
4671
  /** 从一份静态 turn 快照构造只展示历史的会话(例如公开分享页)。 */
3277
4672
  static fromTurns(sessionId, turns) {
3278
4673
  const readonlyError = () => {
@@ -3284,7 +4679,10 @@ var AgentSession = class _AgentSession {
3284
4679
  emitEvent: readonlyError,
3285
4680
  emitWithAck: async () => readonlyError(),
3286
4681
  ensureConnected: readonlyError,
3287
- fetchTurns: async () => turns,
4682
+ fetchTurnsPage: async () => ({ turns, nextBefore: null, currentMode: "executing" }),
4683
+ // 静态会话(分享页)没有会话目录接口,宿主用已加载的历史兜底条目。
4684
+ fetchTurnIndex: async () => ({ sessionId, items: [] }),
4685
+ fetchCommands: async () => [],
3288
4686
  // 静态历史没有回放身份可言,给个空详情即可,不要抛错——_bootstrap
3289
4687
  // 不会跑到这里,但保持只读语义一致。
3290
4688
  fetchSessionInfo: async () => ({ replay_state: null }),
@@ -3295,7 +4693,8 @@ var AgentSession = class _AgentSession {
3295
4693
  attachAppCli: async () => readonlyError(),
3296
4694
  onDispose: () => true
3297
4695
  },
3298
- "connected"
4696
+ "connected",
4697
+ { staticHistory: true }
3299
4698
  );
3300
4699
  session.state = {
3301
4700
  ...withTurns(session.state, turns),
@@ -3308,6 +4707,101 @@ var AgentSession = class _AgentSession {
3308
4707
  getState() {
3309
4708
  return this.state;
3310
4709
  }
4710
+ get hasOlderHistory() {
4711
+ return this.state.nextBefore !== null;
4712
+ }
4713
+ /** Load the page immediately preceding the current display window. */
4714
+ loadOlderHistory(options = {}) {
4715
+ if (this.disposed) return Promise.resolve();
4716
+ if (this.olderPending) return this.olderPending;
4717
+ if (this.replaceInFlight > 0) {
4718
+ return this.replaceQueue.then(() => this.loadOlderHistory(options));
4719
+ }
4720
+ const before = this.state.nextBefore;
4721
+ if (!before) return Promise.resolve();
4722
+ const epoch = this.timelineEpoch;
4723
+ const generation = this.pagingGeneration;
4724
+ this.update((state) => ({ ...state, loadingOlder: true }));
4725
+ const request = this.runtime.fetchTurnsPage(this.sessionId, { before }).then((page) => {
4726
+ if (epoch !== this.timelineEpoch || generation !== this.pagingGeneration || this.state.nextBefore !== before) return;
4727
+ options.beforeCommit?.();
4728
+ this.update((state) => ({
4729
+ ...withTurns(state, prependOlder(state.turns, page.turns)),
4730
+ nextBefore: page.nextBefore,
4731
+ loadingOlder: false
4732
+ }));
4733
+ }).catch(async (error) => {
4734
+ if (isCursorInvalidError(error) && epoch === this.timelineEpoch && generation === this.pagingGeneration && this.state.nextBefore === before) {
4735
+ this.invalidateTimeline();
4736
+ await this.replaceFromHistory();
4737
+ return;
4738
+ }
4739
+ }).finally(() => {
4740
+ if (this.olderPending !== request) return;
4741
+ this.olderPending = null;
4742
+ this.update((state) => ({ ...state, loadingOlder: false }));
4743
+ });
4744
+ this.olderPending = request;
4745
+ return request;
4746
+ }
4747
+ /**
4748
+ * 定位到一条窗口外的历史发言:请服务端返回"从这条发言到最新"的窗口并整窗替换。
4749
+ *
4750
+ * 返回 false 表示这次定位没有落地(请求失败,或期间 timeline 被重写、会话已释放),
4751
+ * 当前窗口保持不变,调用方可以原样重试。已加载的发言不要走这里——宿主直接滚动,
4752
+ * 一次网络请求都不该发。
4753
+ */
4754
+ async locateEntry(entryId) {
4755
+ const token = ++this.locateToken;
4756
+ const controller = new AbortController();
4757
+ this.locateAbort?.abort();
4758
+ this.locateAbort = controller;
4759
+ try {
4760
+ return await this.replaceFromHistory({
4761
+ suppressErrors: false,
4762
+ fromEntryId: entryId,
4763
+ signal: controller.signal,
4764
+ isStale: () => this.locateToken !== token
4765
+ });
4766
+ } catch {
4767
+ return false;
4768
+ } finally {
4769
+ if (this.locateAbort === controller) this.locateAbort = null;
4770
+ }
4771
+ }
4772
+ /**
4773
+ * 取消在途定位:用户改选了另一条发言(哪怕那条已经加载、不需要请求),
4774
+ * 先前那次定位既不该再落地,也不该继续占着替换队列——排队中的下一次定位
4775
+ * 不然要等它跑完(用户看到的是"点了新目标一直转圈")。
4776
+ *
4777
+ * **只**中止定位自己的请求:权威刷新(运行结束 / 重连补数)不共用这个 controller,
4778
+ * 否则"运行刚结束时点一下侧栏"就会把终态对账一起中止,流式与历史对不上。
4779
+ */
4780
+ cancelPendingLocate() {
4781
+ this.locateToken += 1;
4782
+ this.locateAbort?.abort();
4783
+ this.locateAbort = null;
4784
+ }
4785
+ /**
4786
+ * timeline 被整体改写(rewind / checkout / 释放):推进纪元,并把在途定位请求一并中止
4787
+ * ——它已经属于旧 timeline,却还占着替换队列,会让改写后的权威窗口排在后面等它跑完。
4788
+ * 作废号仍然保留:中止只是让它更早结束。
4789
+ */
4790
+ invalidateTimeline() {
4791
+ this.timelineEpoch += 1;
4792
+ this.locateAbort?.abort();
4793
+ this.locateAbort = null;
4794
+ }
4795
+ /**
4796
+ * 拉取当前分支的用户发言目录(消息导航侧栏用)。
4797
+ *
4798
+ * 不在这里缓存:什么时候该刷新只有调用方知道(会话切换、历史被替换、新发言),
4799
+ * 缓存只会让旧条目在 rewind 之后继续可点。
4800
+ */
4801
+ async loadTurnIndex() {
4802
+ const index = await this.runtime.fetchTurnIndex(this.sessionId);
4803
+ return index.items;
4804
+ }
3311
4805
  /** 订阅状态变化。返回取消订阅函数。 */
3312
4806
  subscribe(listener) {
3313
4807
  this.listeners.add(listener);
@@ -3360,7 +4854,7 @@ var AgentSession = class _AgentSession {
3360
4854
  /** 发送一条消息。字符串或多模态内容均可。 */
3361
4855
  async send(content, options = {}) {
3362
4856
  this.runtime.ensureConnected();
3363
- if (this.state.isStreaming || this.state.activeCompaction || this.sendPending || this.manualCompactionPending) {
4857
+ if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.sendPending || this.manualCompactionPending) {
3364
4858
  this.update(
3365
4859
  (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")
3366
4860
  );
@@ -3373,9 +4867,125 @@ var AgentSession = class _AgentSession {
3373
4867
  this.sendPending = false;
3374
4868
  }
3375
4869
  }
4870
+ /**
4871
+ * 入队一条等待消息(运行中排队 / 暂停期间照常入队)。
4872
+ *
4873
+ * ack 超时或传输失败时结果未知:用**同一个** `client_request_id` 重试一次,
4874
+ * 服务端按 `(session_id, client_request_id)` 幂等,不会制造第二条。
4875
+ */
4876
+ async queueMessage(content) {
4877
+ const text = content.trim();
4878
+ if (!text) {
4879
+ return { ok: false, conflict: false, snapshot: null, message: "\u6D88\u606F\u4E0D\u80FD\u4E3A\u7A7A" };
4880
+ }
4881
+ const payload = {
4882
+ session_id: this.sessionId,
4883
+ message: text,
4884
+ client_request_id: createClientRequestId()
4885
+ };
4886
+ try {
4887
+ return await this.requestQueueAck("chat:append", payload);
4888
+ } catch {
4889
+ try {
4890
+ return await this.requestQueueAck("chat:append", payload);
4891
+ } catch (error) {
4892
+ return queueRequestFailure(error);
4893
+ }
4894
+ }
4895
+ }
4896
+ /** 修改一条等待消息的正文;`expectedVersion` 是客户端看到的条目版本。 */
4897
+ async updateQueuedMessage(itemId, expectedVersion, content) {
4898
+ return this.runQueueCommand("chat:queue:update", {
4899
+ session_id: this.sessionId,
4900
+ item_id: itemId,
4901
+ expected_version: expectedVersion,
4902
+ message: content
4903
+ });
4904
+ }
4905
+ /**
4906
+ * 重排等待消息。`orderedIds` 必须是当前 pending 条目 id 的**完整集合**(顺序任意),
4907
+ * 服务端按 `expectedRevision` 校验后整体替换顺序。
4908
+ *
4909
+ * 拖动排序就该像本地操作一样落地:版本冲突(运行中的任务刚领走一条 / 另一个端也在
4910
+ * 动队列)不甩给用户「刷新后重试」,而是拿冲突响应里那份**最新快照**重算一次再提交
4911
+ * ——掉队的条目(已经被领取执行的)从顺序里剔除,剩下的保持用户拖出来的相对次序。
4912
+ * 最多补两次;仍旧冲突才把结果交回调用方。
4913
+ */
4914
+ async reorderQueuedMessages(expectedRevision, orderedIds) {
4915
+ let revision = expectedRevision;
4916
+ let ids = orderedIds;
4917
+ let result = await this.runQueueCommand("chat:queue:reorder", {
4918
+ session_id: this.sessionId,
4919
+ expected_revision: revision,
4920
+ ordered_ids: ids
4921
+ });
4922
+ for (let attempt = 0; attempt < 2 && !result.ok && result.conflict; attempt++) {
4923
+ const snapshot2 = result.snapshot;
4924
+ if (!snapshot2) break;
4925
+ const pending = snapshot2.items.filter(canEditQueuedMessage);
4926
+ const live = new Set(pending.map((item) => item.id));
4927
+ const next = ids.filter((id) => live.has(id));
4928
+ for (const item of pending) if (!next.includes(item.id)) next.push(item.id);
4929
+ if (next.length === 0) {
4930
+ return { ok: true, conflict: false, snapshot: snapshot2, message: null };
4931
+ }
4932
+ revision = snapshot2.revision;
4933
+ ids = next;
4934
+ result = await this.runQueueCommand("chat:queue:reorder", {
4935
+ session_id: this.sessionId,
4936
+ expected_revision: revision,
4937
+ ordered_ids: ids
4938
+ });
4939
+ }
4940
+ return result;
4941
+ }
4942
+ /** 删除一条等待消息;不传版本时由服务端自行判定。 */
4943
+ async cancelQueuedMessage(itemId, expectedVersion) {
4944
+ return this.runQueueCommand("chat:queue:cancel", {
4945
+ session_id: this.sessionId,
4946
+ item_id: itemId,
4947
+ expected_version: expectedVersion
4948
+ });
4949
+ }
4950
+ /** 立即补充:把等待消息交给当前运行,在下一个接收点消费,不停止也不另起一轮。 */
4951
+ async deliverQueuedMessage(itemId, expectedVersion) {
4952
+ return this.runQueueCommand("chat:queue:deliver", {
4953
+ session_id: this.sessionId,
4954
+ item_id: itemId,
4955
+ expected_version: expectedVersion
4956
+ });
4957
+ }
4958
+ /** 继续执行:清除暂停,并按当前顺序恢复逐条接续。 */
4959
+ async resumeQueue() {
4960
+ return this.runQueueCommand("chat:queue:resume", { session_id: this.sessionId });
4961
+ }
4962
+ /** 拉取一份权威队列快照。首次连接与每次重连后由实例自动调用,也可以手动刷新。 */
4963
+ async refreshQueue() {
4964
+ return this.runQueueCommand("chat:queue:list", { session_id: this.sessionId });
4965
+ }
4966
+ /**
4967
+ * 发出一条队列命令并安装 ack 里的快照。
4968
+ * 传输层失败(超时 / 断线)会 reject,由调用方决定是否重试。
4969
+ */
4970
+ async requestQueueAck(event, payload) {
4971
+ this.runtime.ensureConnected();
4972
+ const response = await this.runtime.emitWithAck(event, payload, QUEUE_ACK_TIMEOUT_MS);
4973
+ const result = parseQueueAck(response, this.sessionId);
4974
+ const snapshot2 = result.snapshot;
4975
+ if (snapshot2) this.update((state) => withQueue(state, snapshot2));
4976
+ return result;
4977
+ }
4978
+ /** 队列命令的对外入口:传输失败只体现为失败结果,不向调用方抛异常。 */
4979
+ async runQueueCommand(event, payload) {
4980
+ try {
4981
+ return await this.requestQueueAck(event, payload);
4982
+ } catch (error) {
4983
+ return queueRequestFailure(error);
4984
+ }
4985
+ }
3376
4986
  async sendUnlocked(content, options) {
3377
4987
  await this.ensureIdleBeforeSend();
3378
- if (this.state.isStreaming || this.state.activeCompaction || this.manualCompactionPending) {
4988
+ if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.manualCompactionPending) {
3379
4989
  this.update(
3380
4990
  (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")
3381
4991
  );
@@ -3385,7 +4995,7 @@ var AgentSession = class _AgentSession {
3385
4995
  if (this.getAppContext) {
3386
4996
  try {
3387
4997
  const rawContext = await this.getAppContext();
3388
- if (!isRecord5(rawContext)) {
4998
+ if (!isRecord(rawContext)) {
3389
4999
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
3390
5000
  }
3391
5001
  const serialized = JSON.stringify(rawContext);
@@ -3393,7 +5003,7 @@ var AgentSession = class _AgentSession {
3393
5003
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE\u53EF\u5E8F\u5217\u5316\u7684 JSON \u5BF9\u8C61");
3394
5004
  }
3395
5005
  const normalized = JSON.parse(serialized);
3396
- if (!isRecord5(normalized)) {
5006
+ if (!isRecord(normalized)) {
3397
5007
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
3398
5008
  }
3399
5009
  appContext = normalized;
@@ -3403,17 +5013,18 @@ var AgentSession = class _AgentSession {
3403
5013
  return false;
3404
5014
  }
3405
5015
  }
5016
+ const wasStreaming = this.state.isStreaming;
3406
5017
  const askUserAnswer = options.askUserAnswer;
3407
5018
  let optimisticTurnId = null;
5019
+ const clientRequestId = createClientRequestId();
3408
5020
  let optimisticAskAnswer;
3409
5021
  let previousAskAnswer;
3410
- if (!(askUserAnswer && typeof askUserAnswer.tool_call_id === "string")) {
3411
- this.update((state) => {
3412
- const next = addUserMessage(state, content);
3413
- optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
3414
- return next;
3415
- });
3416
- }
5022
+ this.update((state) => {
5023
+ const next = addUserMessage(state, content, clientRequestId);
5024
+ optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
5025
+ return next;
5026
+ });
5027
+ if (optimisticTurnId) this.markOptimisticTurnPending(optimisticTurnId);
3417
5028
  this.update((s) => ({ ...s, isStreaming: true }));
3418
5029
  if (askUserAnswer && typeof askUserAnswer.tool_call_id === "string") {
3419
5030
  const { tool_call_id, ...rest } = askUserAnswer;
@@ -3427,57 +5038,103 @@ var AgentSession = class _AgentSession {
3427
5038
  if (!options.replayDecision) {
3428
5039
  this.pendingReplayMessage = content;
3429
5040
  this.pendingReplayMode = options.mode;
5041
+ this.pendingReplayKbIds = options.kbIds;
3430
5042
  }
3431
5043
  const payload = {
3432
5044
  session_id: this.sessionId,
3433
5045
  message: content,
5046
+ client_request_id: clientRequestId,
3434
5047
  mode: options.mode,
3435
5048
  askuser_answer: askUserAnswer,
3436
5049
  model: options.model,
3437
5050
  thinking_override: options.thinkingOverride,
3438
5051
  whatif: options.whatif,
3439
5052
  replay_decision: options.replayDecision,
3440
- app_context: appContext
5053
+ app_context: appContext,
5054
+ kb_ids: options.kbIds,
5055
+ // 运行中提交不必由客户端挑命令:服务端按会话运行状态决定直接执行还是入队。
5056
+ queue_when_running: options.queueWhenRunning === true
3441
5057
  };
3442
5058
  try {
3443
5059
  await this.ensureJoined();
3444
5060
  } catch (error) {
5061
+ const failure = describeSessionError(error, "\u4F1A\u8BDD\u8BA2\u9605\u5931\u8D25\uFF0C\u6D88\u606F\u672A\u53D1\u9001");
3445
5062
  this.rollbackOptimisticSend(
3446
5063
  optimisticTurnId,
3447
5064
  askUserAnswer,
3448
5065
  optimisticAskAnswer,
3449
5066
  previousAskAnswer,
3450
- error instanceof Error && error.message ? error.message : "\u4F1A\u8BDD\u8BA2\u9605\u5931\u8D25\uFF0C\u6D88\u606F\u672A\u53D1\u9001"
5067
+ wasStreaming,
5068
+ failure.message,
5069
+ failure.code
3451
5070
  );
3452
5071
  return false;
3453
5072
  }
5073
+ const waitStartedAt = this.systemErrorGeneration;
3454
5074
  try {
3455
5075
  const response = await this.runtime.emitWithAck("chat:send", payload, 3e4);
5076
+ if (response?.status === "queued") {
5077
+ this.discardOptimisticSend(
5078
+ optimisticTurnId,
5079
+ askUserAnswer,
5080
+ optimisticAskAnswer,
5081
+ previousAskAnswer
5082
+ );
5083
+ const snapshot2 = parseQueueAck(response, this.sessionId).snapshot;
5084
+ const running = response.running === true;
5085
+ this.update((current) => {
5086
+ const next = running ? current : { ...current, isStreaming: false };
5087
+ return snapshot2 ? withQueue(next, snapshot2) : next;
5088
+ });
5089
+ return true;
5090
+ }
3456
5091
  if (response?.status !== "accepted") {
3457
- throw new Error(
3458
- typeof response?.message === "string" ? response.message : "\u6D88\u606F\u672A\u88AB\u670D\u52A1\u7AEF\u63A5\u53D7"
5092
+ throw new AgentSessionError(
5093
+ typeof response?.message === "string" ? response.message : "\u6D88\u606F\u672A\u88AB\u670D\u52A1\u7AEF\u63A5\u53D7",
5094
+ typeof response?.code === "string" ? response.code : void 0
3459
5095
  );
3460
5096
  }
3461
5097
  return true;
3462
5098
  } catch (error) {
5099
+ const failure = describeSessionError(error, "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
3463
5100
  this.rollbackOptimisticSend(
3464
5101
  optimisticTurnId,
3465
5102
  askUserAnswer,
3466
5103
  optimisticAskAnswer,
3467
5104
  previousAskAnswer,
3468
- error instanceof Error && error.message ? error.message : "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5"
5105
+ wasStreaming,
5106
+ failure.message,
5107
+ failure.code,
5108
+ waitStartedAt
3469
5109
  );
3470
5110
  return false;
3471
5111
  }
3472
5112
  }
3473
- rollbackOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer, message) {
5113
+ rollbackOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer, wasStreaming, message, code, waitStartedAt) {
5114
+ this.discardOptimisticSend(
5115
+ optimisticTurnId,
5116
+ askUserAnswer,
5117
+ optimisticAskAnswer,
5118
+ previousAskAnswer
5119
+ );
5120
+ this.update(
5121
+ (current) => addErrorMessage(wasStreaming ? current : { ...current, isStreaming: false }, message)
5122
+ );
5123
+ const delivered = this.lastDeliveredSystemError;
5124
+ if (waitStartedAt !== void 0 && delivered && delivered.generation > waitStartedAt && delivered.message === message && delivered.code === code) {
5125
+ this.lastDeliveredSystemError = null;
5126
+ return;
5127
+ }
5128
+ this.emitter.emit("error", { message, ...code ? { code } : {} });
5129
+ }
5130
+ /** 撤回一次乐观提交:移除乐观消息、恢复提问作答、清掉回放草稿。 */
5131
+ discardOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer) {
3474
5132
  this.pendingReplayMessage = null;
3475
5133
  this.pendingReplayMode = void 0;
5134
+ this.pendingReplayKbIds = void 0;
3476
5135
  this.update((current) => {
3477
- let next = optimisticTurnId ? withTurns(
3478
- current,
3479
- current.turns.filter((turn) => turn.turn_id !== optimisticTurnId)
3480
- ) : current;
5136
+ const filtered = optimisticTurnId ? current.turns.filter((turn) => turn.turn_id !== optimisticTurnId) : current.turns;
5137
+ let next = optimisticTurnId ? withTurns({ ...current, turns: filtered }, filtered) : current;
3481
5138
  const toolCallId = askUserAnswer && typeof askUserAnswer.tool_call_id === "string" ? askUserAnswer.tool_call_id : null;
3482
5139
  if (toolCallId && next.askAnswers[toolCallId] === optimisticAskAnswer) {
3483
5140
  const askAnswers = { ...next.askAnswers };
@@ -3488,18 +5145,20 @@ var AgentSession = class _AgentSession {
3488
5145
  }
3489
5146
  next = { ...next, askAnswers };
3490
5147
  }
3491
- return addErrorMessage({ ...next, isStreaming: false }, message);
5148
+ return next;
3492
5149
  });
3493
- }
3494
- /** 智能体运行期间追加补充说明(不打断当前回复)。 */
3495
- append(text) {
3496
- this.runtime.ensureConnected();
3497
- this.runtime.emitEvent("chat:append", { session_id: this.sessionId, message: text });
3498
- this.update((s) => addUserMessage(s, text));
5150
+ if (optimisticTurnId) {
5151
+ this.pendingOptimisticTurnIds.delete(optimisticTurnId);
5152
+ this.liveRevisions.delete(optimisticTurnId);
5153
+ }
3499
5154
  }
3500
5155
  /** 停止当前回复。 */
3501
5156
  stop() {
3502
- return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) => response ?? {});
5157
+ return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) => {
5158
+ const result = response ?? {};
5159
+ if (result.status !== "accepted") this.manualCompactionPending = false;
5160
+ return result;
5161
+ });
3503
5162
  }
3504
5163
  /** 手动触发上下文压缩。 */
3505
5164
  compact() {
@@ -3519,8 +5178,14 @@ var AgentSession = class _AgentSession {
3519
5178
  trigger: "manual"
3520
5179
  }
3521
5180
  }));
3522
- return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {}).finally(() => {
5181
+ return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => {
5182
+ const result = response ?? {};
5183
+ if (result.status !== "accepted") this.manualCompactionPending = false;
5184
+ return result;
5185
+ }).catch((error) => {
3523
5186
  this.manualCompactionPending = false;
5187
+ throw error;
5188
+ }).finally(() => {
3524
5189
  if (this.state.activeCompaction?.compaction_id === "pending-manual-compaction") {
3525
5190
  this.update((state) => ({ ...state, activeCompaction: null }));
3526
5191
  }
@@ -3559,10 +5224,20 @@ var AgentSession = class _AgentSession {
3559
5224
  if (this.disposed) return;
3560
5225
  if (!this.runtime.onDispose(this)) return;
3561
5226
  this.disposed = true;
5227
+ this.invalidateTimeline();
5228
+ this.invalidateOlderPaging();
5229
+ this.state = { ...this.state, nextBefore: null, loadingOlder: false };
3562
5230
  this.cancelPatchFlush();
3563
5231
  this.listeners.clear();
3564
5232
  this.emitter.clear();
3565
5233
  this.commands.clear();
5234
+ this.commandRecoveryGeneration += 1;
5235
+ this.commandRestoreRetryAttempt = 0;
5236
+ this.bufferedCommandDeliveries = null;
5237
+ if (this.commandRestoreRetryHandle !== null) {
5238
+ globalThis.clearTimeout(this.commandRestoreRetryHandle);
5239
+ this.commandRestoreRetryHandle = null;
5240
+ }
3566
5241
  }
3567
5242
  get isDisposed() {
3568
5243
  return this.disposed;
@@ -3573,28 +5248,64 @@ var AgentSession = class _AgentSession {
3573
5248
  if (this.state.connection === connection) return;
3574
5249
  this.update((s) => ({ ...s, connection }));
3575
5250
  }
5251
+ /**
5252
+ * @internal 服务端广播的权威队列快照。
5253
+ * 结构不合法或 revision 不新的一律丢弃,绝不用旧快照覆盖新状态。
5254
+ */
5255
+ _handleQueueUpdated(payload) {
5256
+ if (!isSessionQueueSnapshot(payload)) return;
5257
+ const snapshot2 = payload;
5258
+ this.update((state) => withQueue(state, snapshot2));
5259
+ }
3576
5260
  /** @internal socket 重连后由 Hub 调用:重新加入房间并补数。 */
3577
- async _rejoinAfterReconnect() {
3578
- const turnsBeforeRejoin = this.state.turns;
3579
- this.joined = false;
3580
- this.joinPending = null;
3581
- try {
3582
- await this.ensureJoined();
3583
- await Promise.all([
3584
- this.syncTurnsFromHistory({
3585
- clearOnEmpty: true,
3586
- preserveChangesSince: turnsBeforeRejoin
3587
- }),
3588
- this.syncReplayFromSession()
3589
- ]);
3590
- } catch {
3591
- }
5261
+ _rejoinAfterReconnect() {
5262
+ this.invalidateOlderPaging();
5263
+ this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
5264
+ const reconnectLiveRevision = this.liveRevision;
5265
+ const reconnectModeRevision = this.modeRevision;
5266
+ const commandRecoveryGeneration = this.beginCommandRecovery();
5267
+ return this.enqueueSubscription(async () => {
5268
+ this.joined = false;
5269
+ this.joinPending = null;
5270
+ try {
5271
+ this.subscriptionReplaySource = "reconnect_replay";
5272
+ try {
5273
+ await this.ensureJoined(reconnectLiveRevision);
5274
+ } finally {
5275
+ this.subscriptionReplaySource = null;
5276
+ this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
5277
+ }
5278
+ await Promise.all([
5279
+ this.refreshAuthoritativeHistory({
5280
+ liveRevisionAtStart: reconnectLiveRevision,
5281
+ modeRevisionAtStart: reconnectModeRevision
5282
+ }),
5283
+ this.syncReplayFromSession()
5284
+ ]);
5285
+ } catch {
5286
+ }
5287
+ });
3592
5288
  }
3593
5289
  /** @internal 首次连接:加载历史 + 加入房间。 */
3594
- async _bootstrap() {
3595
- await this.syncTurnsFromHistory({ suppressErrors: false });
3596
- await this.ensureJoined();
3597
- await this.syncReplayFromSession();
5290
+ _bootstrap() {
5291
+ const commandRecoveryGeneration = this.beginCommandRecovery();
5292
+ return this.enqueueSubscription(async () => {
5293
+ await this.replaceFromHistory({ suppressErrors: false });
5294
+ this.subscriptionReplaySource = "bootstrap_replay";
5295
+ try {
5296
+ await this.ensureJoined();
5297
+ } finally {
5298
+ this.subscriptionReplaySource = null;
5299
+ }
5300
+ this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
5301
+ await Promise.all([this.syncReplayFromSession(), this.refreshQueue()]);
5302
+ });
5303
+ }
5304
+ /** 订阅生命周期串行执行;一次失败只由自己的调用方观察,不截断后续重连。 */
5305
+ enqueueSubscription(operation) {
5306
+ const run = this.subscriptionQueue.then(operation);
5307
+ this.subscriptionQueue = run.catch(() => void 0);
5308
+ return run;
3598
5309
  }
3599
5310
  /** 改回放是 owner 专属(服务端 PATCH 走 _get_owned_session)。 */
3600
5311
  get canControlReplay() {
@@ -3651,10 +5362,16 @@ var AgentSession = class _AgentSession {
3651
5362
  this.applyTurn(turn);
3652
5363
  }
3653
5364
  /** @internal 历史回放通道(event_format: raw)。 */
3654
- _handleTurnEvents(events) {
5365
+ _handleTurnEvents(events, replayed = false) {
3655
5366
  if (!events.length) return;
5367
+ if (events.some(
5368
+ (event) => event.type === "compaction:end" || event.type === "compaction:error" || event.type === "compaction:abort"
5369
+ )) {
5370
+ this.manualCompactionPending = false;
5371
+ }
5372
+ this.dispatchRawCommands(events);
3656
5373
  const terminalChatEndStatus = extractTerminalChatEndStatus(events);
3657
- if (!terminalChatEndStatus && hasChatRunEvent(events)) {
5374
+ if (!terminalChatEndStatus && hasChatRunEvent(events) && this.state.status !== "waiting_for_input") {
3658
5375
  this.markRunningIfIdle();
3659
5376
  }
3660
5377
  if (this.state.turns.length) {
@@ -3663,36 +5380,58 @@ var AgentSession = class _AgentSession {
3663
5380
  const updates = this.projectionBuilder.processBatch(events);
3664
5381
  for (const update of updates) {
3665
5382
  if (update.kind === "upsert") {
3666
- this.applyTurn(update.turn);
5383
+ this.applyTurn(
5384
+ update.turn,
5385
+ replayed ? this.subscriptionReplaySource ?? "bootstrap_replay" : "live"
5386
+ );
3667
5387
  } else if (update.kind === "workspace_changed") {
3668
5388
  this.emitter.emit("workspaceChanged", {});
3669
5389
  }
3670
5390
  }
3671
- if (terminalChatEndStatus) {
3672
- this.finishStreaming(terminalChatEndStatus);
5391
+ if (terminalChatEndStatus || acceptedPostChatFollowupCompletesLatestRun(events, updates)) {
5392
+ this.completeChat(terminalChatEndStatus ?? "completed");
3673
5393
  }
3674
5394
  }
3675
5395
  /** @internal */
3676
5396
  _handleChatStart() {
3677
- this.update((s) => ({ ...s, status: "running", errorMessage: null, isStreaming: true }));
5397
+ this.update((s) => ({
5398
+ ...s,
5399
+ status: "running",
5400
+ errorMessage: null,
5401
+ isStreaming: true
5402
+ }));
3678
5403
  }
3679
5404
  /** @internal */
3680
5405
  _handleChatEnd(status) {
3681
5406
  this.flushPatches();
3682
- this.finishStreaming(status);
3683
- this.recentChatEndAt = Date.now();
3684
- void this.syncTurnsFromHistory();
3685
- this.emitter.emit("chatEnd", { status });
5407
+ this.completeChat(status);
3686
5408
  }
3687
5409
  /** @internal */
3688
- _handleSystemError(message) {
5410
+ _handleSystemError(message, code) {
3689
5411
  this.flushPatches();
3690
5412
  const finalMessage = isOomText(message) ? OOM_MESSAGE : message;
3691
5413
  this.update(
3692
- (s) => addErrorMessage(markFailed({ ...s, status: "failed", errorMessage: finalMessage, isStreaming: false }), finalMessage)
5414
+ (s) => addErrorMessage(
5415
+ markFailed({
5416
+ ...s,
5417
+ status: "failed",
5418
+ errorMessage: finalMessage,
5419
+ isStreaming: false
5420
+ }),
5421
+ finalMessage
5422
+ )
3693
5423
  );
3694
5424
  this.recentChatEndAt = Date.now();
3695
- this.emitter.emit("error", { message: finalMessage });
5425
+ this.systemErrorGeneration += 1;
5426
+ this.lastDeliveredSystemError = {
5427
+ message: finalMessage,
5428
+ ...code ? { code } : {},
5429
+ generation: this.systemErrorGeneration
5430
+ };
5431
+ this.emitter.emit("error", {
5432
+ message: finalMessage,
5433
+ ...code ? { code } : {}
5434
+ });
3696
5435
  }
3697
5436
  /** @internal */
3698
5437
  _handleTaskUpdated(tasks) {
@@ -3712,6 +5451,8 @@ var AgentSession = class _AgentSession {
3712
5451
  this.update((s) => ({ ...s, replay }));
3713
5452
  }
3714
5453
  this.emitter.emit("sessionUpdated", {
5454
+ workspacePath: data.workspace_path,
5455
+ chatGroupId: data.chat_group_id,
3715
5456
  intent: data.intent,
3716
5457
  status: data.status,
3717
5458
  model: data.model,
@@ -3721,16 +5462,26 @@ var AgentSession = class _AgentSession {
3721
5462
  }
3722
5463
  /** @internal */
3723
5464
  _handleRewind(checkpointId) {
3724
- this.update((s) => ({ ...s, mode: null, errorMessage: null }));
3725
- void this.syncTurnsFromHistory({ clearOnEmpty: true });
5465
+ this.invalidateTimeline();
5466
+ this.invalidateOlderPaging();
5467
+ this.update((s) => ({
5468
+ ...s,
5469
+ mode: null,
5470
+ errorMessage: null,
5471
+ nextBefore: null,
5472
+ loadingOlder: false
5473
+ }));
5474
+ void this.replaceFromHistory();
3726
5475
  this.emitter.emit("rewind", { checkpointId });
3727
5476
  }
3728
5477
  /** @internal */
3729
5478
  _handleReplayMismatch(data) {
3730
5479
  const message = this.pendingReplayMessage ?? data.actual_message ?? "";
3731
5480
  const mode = this.pendingReplayMode;
5481
+ const kbIds = this.pendingReplayKbIds;
3732
5482
  this.pendingReplayMessage = null;
3733
5483
  this.pendingReplayMode = void 0;
5484
+ this.pendingReplayKbIds = void 0;
3734
5485
  const respond = (decision) => {
3735
5486
  void this.runtime.emitWithAck(
3736
5487
  "chat:send",
@@ -3738,7 +5489,8 @@ var AgentSession = class _AgentSession {
3738
5489
  session_id: this.sessionId,
3739
5490
  message,
3740
5491
  mode,
3741
- replay_decision: decision
5492
+ replay_decision: decision,
5493
+ kb_ids: kbIds
3742
5494
  },
3743
5495
  3e4
3744
5496
  ).then((response) => {
@@ -3779,9 +5531,9 @@ var AgentSession = class _AgentSession {
3779
5531
  detail,
3780
5532
  status,
3781
5533
  loopId,
3782
- metadata: isRecord5(notification.metadata) ? notification.metadata : void 0
5534
+ metadata: isRecord(notification.metadata) ? notification.metadata : void 0
3783
5535
  });
3784
- if (notificationType === "bg:started" && isRecord5(notification.metadata)) {
5536
+ if (notificationType === "bg:started" && isRecord(notification.metadata)) {
3785
5537
  const taskId = typeof notification.metadata.task_id === "string" ? notification.metadata.task_id : "";
3786
5538
  if (taskId) {
3787
5539
  this.emitter.emit("backgroundTask", {
@@ -3803,12 +5555,12 @@ var AgentSession = class _AgentSession {
3803
5555
  this.emitter.emit("artifact", event);
3804
5556
  }
3805
5557
  /** @internal 加入房间的应答里带 is_running,据此校准流式状态。 */
3806
- _applyRunningState(isRunning) {
5558
+ _applyRunningState(isRunning, liveRevisionAtJoin = this.liveRevision) {
3807
5559
  if (isRunning) {
3808
5560
  this.update((s) => ({ ...s, status: "running", isStreaming: true }));
3809
5561
  } else if (this.state.isStreaming) {
3810
5562
  this.update((s) => ({ ...s, isStreaming: false }));
3811
- void this.syncTurnsFromHistory();
5563
+ void this.refreshAuthoritativeHistory({ liveRevisionAtStart: liveRevisionAtJoin });
3812
5564
  }
3813
5565
  }
3814
5566
  // ---------------------------------------------------------------- 内部实现
@@ -3826,11 +5578,39 @@ var AgentSession = class _AgentSession {
3826
5578
  return next;
3827
5579
  });
3828
5580
  }
3829
- applyTurn(turn) {
5581
+ completeChat(status) {
5582
+ this.finishStreaming(status);
5583
+ this.recentChatEndAt = Date.now();
5584
+ void this.refreshAuthoritativeHistory();
5585
+ this.emitter.emit("chatEnd", { status });
5586
+ }
5587
+ markTurnFresh(turnId) {
5588
+ this.liveRevision += 1;
5589
+ this.liveRevisions.set(turnId, this.liveRevision);
5590
+ }
5591
+ markOptimisticTurnPending(turnId) {
5592
+ this.pendingOptimisticTurnIds.add(turnId);
5593
+ this.markTurnFresh(turnId);
5594
+ }
5595
+ pruneOptimisticLiveRevisions() {
5596
+ const retained = new Set(
5597
+ this.state.turns.map((turn) => turn.turn_id).filter((turnId) => turnId.startsWith("local-user-"))
5598
+ );
5599
+ for (const turnId of this.liveRevisions.keys()) {
5600
+ if (turnId.startsWith("local-user-") && !retained.has(turnId)) {
5601
+ this.pendingOptimisticTurnIds.delete(turnId);
5602
+ this.liveRevisions.delete(turnId);
5603
+ }
5604
+ }
5605
+ }
5606
+ applyTurn(turn, source = "live") {
3830
5607
  this.flushPatches();
5608
+ this.markTurnFresh(turn.turn_id);
5609
+ if (extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
3831
5610
  const previous = this.state;
3832
5611
  this.update((s) => upsertTurn(s, turn));
3833
- this.emitTurnDerivedEvents(previous, turn);
5612
+ this.pruneOptimisticLiveRevisions();
5613
+ this.emitTurnDerivedEvents(previous, turn, source);
3834
5614
  }
3835
5615
  schedulePatchFlush() {
3836
5616
  if (this.patchFlushHandle != null) return;
@@ -3863,11 +5643,15 @@ var AgentSession = class _AgentSession {
3863
5643
  this.pendingTurnPatches.clear();
3864
5644
  for (const patch of patches) {
3865
5645
  const previous = this.state;
3866
- this.update((s) => applyTurnPatch(s, patch));
5646
+ const next = applyTurnPatch(previous, patch);
5647
+ if (next === previous) continue;
5648
+ this.liveRevision += 1;
5649
+ this.liveRevisions.set(patch.turn_id, this.liveRevision);
3867
5650
  const turn = patch.data.turn;
3868
- if (turn) {
3869
- this.emitTurnDerivedEvents(previous, turn);
3870
- }
5651
+ if (turn && extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
5652
+ this.setState(next);
5653
+ this.pruneOptimisticLiveRevisions();
5654
+ if (turn) this.emitTurnDerivedEvents(previous, turn);
3871
5655
  }
3872
5656
  if (this.patchFlushCancel) {
3873
5657
  this.patchFlushCancel();
@@ -3876,7 +5660,7 @@ var AgentSession = class _AgentSession {
3876
5660
  }
3877
5661
  }
3878
5662
  /** turn 数据落地后统一派生语义事件(指令 / 工具调用 / 消息 / 模式 / 预览 / OOM)。 */
3879
- emitTurnDerivedEvents(previous, turn) {
5663
+ emitTurnDerivedEvents(previous, turn, source = "live") {
3880
5664
  const mode = extractModeFromBlocks(turn.blocks);
3881
5665
  if (mode && previous.mode !== mode) {
3882
5666
  this.emitter.emit("modeChange", { mode });
@@ -3889,23 +5673,29 @@ var AgentSession = class _AgentSession {
3889
5673
  this.dispatchCommand(turn, block);
3890
5674
  continue;
3891
5675
  }
3892
- if (block.type === "tool_ui" && block.tool_call_id && isUiMetaLike(block.content)) {
5676
+ if (block.type === "tool_ui" && block.tool_call_id && isToolUiCard(block.content)) {
3893
5677
  const ui = block.content;
3894
5678
  if (ui.target !== "preview") continue;
3895
- const previewType = ui.resourceHTML ? "resource-html" : "resource-uri";
3896
- const previewContent = ui.resourceHTML ?? ui.resourceUri ?? ui.resourceURI;
3897
- if (!previewContent) continue;
3898
- const key = `${block.tool_call_id}:${previewType}`;
3899
- if (this.deliveredToolPreviews.get(key) === previewContent) continue;
3900
- this.deliveredToolPreviews.set(key, previewContent);
5679
+ const resolved = resolveToolUiCardContent(ui);
5680
+ if (!resolved) continue;
5681
+ const key = buildToolUiCardKey(block.tool_call_id, resolved.type, resolved.content, ui.sourcePath);
5682
+ const fingerprint = JSON.stringify([
5683
+ resolved.content,
5684
+ ui.mcpApp?.sourceId,
5685
+ resolved.type === "resource-file" ? block.tool_call_id : void 0
5686
+ ]);
5687
+ if (this.deliveredToolPreviews.get(key) === fingerprint) continue;
5688
+ this.deliveredToolPreviews.set(key, fingerprint);
3901
5689
  this.emitter.emit("toolPreview", {
3902
5690
  toolCallId: block.tool_call_id,
3903
- type: previewType,
3904
- content: previewContent,
3905
- title: ui.title ?? "\u5DE5\u5177\u9884\u89C8"
5691
+ type: resolved.type,
5692
+ content: resolved.content,
5693
+ title: ui.title ?? "\u5DE5\u5177\u9884\u89C8",
5694
+ ...ui.sourcePath ? { sourcePath: ui.sourcePath } : {},
5695
+ ...ui.mcpApp ? { mcpApp: ui.mcpApp } : {}
3906
5696
  });
3907
5697
  }
3908
- if (block.type === "system_notification" && isRecord5(block.content)) {
5698
+ if (block.type === "system_notification" && isRecord(block.content)) {
3909
5699
  this._handleSystemNotification(turn.loop_id, block.content);
3910
5700
  }
3911
5701
  }
@@ -3925,7 +5715,7 @@ var AgentSession = class _AgentSession {
3925
5715
  const finished = toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled";
3926
5716
  if (finished && !this.finishedToolCallIds.has(toolCall.id)) {
3927
5717
  this.finishedToolCallIds.add(toolCall.id);
3928
- this.emitter.emit("toolResult", { toolCall: info, turn });
5718
+ this.emitter.emit("toolResult", { toolCall: info, turn, source });
3929
5719
  if (isOomResult(toolCall.result)) {
3930
5720
  this.notifySandboxOom(toolCall.id);
3931
5721
  }
@@ -3942,17 +5732,36 @@ var AgentSession = class _AgentSession {
3942
5732
  dispatchCommand(turn, block) {
3943
5733
  if (!isToolBridgeContent(block.content)) return;
3944
5734
  const { action, payload } = block.content;
3945
- let payloadKey = "";
3946
- try {
3947
- payloadKey = JSON.stringify(payload ?? null);
3948
- } catch {
3949
- payloadKey = String(payload ?? "");
5735
+ const deliveryKey = block.delivery_id ? commandDeliveryKey(block.delivery_id) : `legacy-tool-command:${turn.turn_id}:${block.tool_call_id ?? ""}:${action}:${JSON.stringify(payload)}`;
5736
+ this.deliverCommand(deliveryKey, action, payload, block.tool_call_id ?? void 0);
5737
+ }
5738
+ dispatchRawCommands(events) {
5739
+ for (const event of events) {
5740
+ if (event.type !== "loop:tool_bridge") continue;
5741
+ const deliveryId = String(event.payload.delivery_id ?? "");
5742
+ const bridge = event.payload.bridge;
5743
+ if (!deliveryId || !isToolBridgeContent(bridge)) continue;
5744
+ this.deliverCommand(
5745
+ commandDeliveryKey(deliveryId),
5746
+ bridge.action,
5747
+ bridge.payload,
5748
+ String(event.payload.tool_call_id ?? "") || void 0
5749
+ );
5750
+ }
5751
+ }
5752
+ deliverCommand(deliveryKey, action, payload, toolCallId) {
5753
+ if (this.bufferedCommandDeliveries) {
5754
+ this.bufferedCommandDeliveries.push({ deliveryKey, action, payload, toolCallId });
5755
+ return;
3950
5756
  }
3951
- const deliveryKey = [block.tool_call_id ?? turn.id, action, payloadKey].join("\0");
5757
+ this.deliverCommandNow({ deliveryKey, action, payload, toolCallId });
5758
+ }
5759
+ deliverCommandNow(command) {
5760
+ const { deliveryKey, action, payload, toolCallId } = command;
3952
5761
  if (this.deliveredCommandKeys.has(deliveryKey)) return;
3953
5762
  this.deliveredCommandKeys.add(deliveryKey);
3954
- const meta = { toolCallId: block.tool_call_id ?? void 0 };
3955
- this.emitter.emit("command", { action, payload, toolCallId: meta.toolCallId });
5763
+ const meta = { toolCallId };
5764
+ this.emitter.emit("command", { action, payload, toolCallId });
3956
5765
  this.commands.dispatch(action, payload, meta);
3957
5766
  }
3958
5767
  notifySandboxOom(key) {
@@ -3961,63 +5770,217 @@ var AgentSession = class _AgentSession {
3961
5770
  this.update((s) => addErrorMessage(s, OOM_MESSAGE));
3962
5771
  this.emitter.emit("sandboxOom", { toolCallId: key });
3963
5772
  }
3964
- async syncTurnsFromHistory(options = {}) {
3965
- const { clearOnEmpty = false, suppressErrors = true, preserveChangesSince } = options;
3966
- const turnsAtRequestStart = this.state.turns;
3967
- try {
3968
- const turns = await this.runtime.fetchTurns(this.sessionId);
3969
- if (turns.length === 0 && !clearOnEmpty) return;
3970
- this.update((state) => {
3971
- const mergeBaseline = preserveChangesSince ?? turnsAtRequestStart;
3972
- if (state.turns === mergeBaseline) return withTurns(state, turns);
3973
- const historyById = new Map(turns.map((turn) => [turn.turn_id, turn]));
3974
- let merged = withTurns(state, turns);
3975
- for (const current of state.turns) {
3976
- const fromHistory = historyById.get(current.turn_id);
3977
- if (!fromHistory || current.sequence > fromHistory.sequence) {
3978
- merged = upsertTurn(merged, current);
3979
- }
3980
- }
3981
- return merged;
3982
- });
3983
- this.replayCommandsFromTurns(turns);
3984
- } catch (error) {
3985
- if (!suppressErrors) throw error;
3986
- }
3987
- }
3988
5773
  /**
3989
- * 把历史里的指令补投给宿主页面。
5774
+ * 权威刷新(运行结束 / 重连补数):**按当前窗口第一条发言取窗口**,不再另记"用户想看哪段"。
3990
5775
  *
3991
- * 重开会话、断线补数时,地图高亮这类"页面当前状态"必须能恢复——
3992
- * 否则刷新一次页面,之前智能体让页面做的事就全丢了。
3993
- * 只回放指令:toolCall / message 这些事件如果按历史全量重放,
3994
- * 监听器会在挂载瞬间收到几百条陈年事件。
3995
- * dispatch 内部按 deliveredCommandKeys 去重,重复调用安全。
5776
+ * 窗口本身就是"用户在看哪段":首屏就是最新一页,向上翻页 / 定位之后首条就是用户滚到的位置。
5777
+ * 直接拿 `from_entry_id = 当前窗口第一条 turn_id` 去取,运行结束、重连补数、翻页之后的对账
5778
+ * 是同一条规则,不需要意图、锚点、已落地锚点这些状态;窗口首条已经不在分支上(404 / 409)时
5779
+ * 回落到最新一页。
3996
5780
  */
3997
- replayCommandsFromTurns(turns) {
5781
+ refreshAuthoritativeHistory(options = {}) {
5782
+ let entryGone = false;
5783
+ const locateTokenAtStart = this.locateToken;
5784
+ return this.replaceFromHistory({
5785
+ ...options,
5786
+ // 锚点在**这次操作真正开始**时采样:排队期间用户可能刚定位过,那时窗口首条才是他要的。
5787
+ fromEntryId: () => {
5788
+ const anchor = this.state.turns[0]?.turn_id;
5789
+ return anchor?.startsWith("local-user-") ? void 0 : anchor;
5790
+ },
5791
+ onError: (error) => {
5792
+ entryGone = isEntryGoneError(error);
5793
+ }
5794
+ }).then((located) => {
5795
+ if (located || !entryGone) return located;
5796
+ if (this.locateToken !== locateTokenAtStart) return false;
5797
+ return this.replaceFromHistory(options);
5798
+ });
5799
+ }
5800
+ replaceFromHistory(options = {}) {
5801
+ if (this.disposed) return Promise.resolve(false);
5802
+ const operationEpoch = this.timelineEpoch;
5803
+ const liveRevisionAtStart = options.liveRevisionAtStart ?? this.liveRevision;
5804
+ const modeRevisionAtStart = options.modeRevisionAtStart ?? this.modeRevision;
5805
+ this.invalidateOlderPaging();
5806
+ this.replaceInFlight += 1;
5807
+ const operation = async () => {
5808
+ let previousNextBefore = null;
5809
+ const restoreCursor = () => {
5810
+ if (this.disposed || previousNextBefore === null) return;
5811
+ this.update((state) => ({
5812
+ ...state,
5813
+ nextBefore: previousNextBefore,
5814
+ loadingOlder: false
5815
+ }));
5816
+ };
5817
+ try {
5818
+ if (this.disposed || operationEpoch !== this.timelineEpoch) return false;
5819
+ if (options.isStale?.()) return false;
5820
+ this.invalidateOlderPaging();
5821
+ previousNextBefore = this.state.nextBefore;
5822
+ this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
5823
+ const signal = options.signal;
5824
+ const fromEntryId = typeof options.fromEntryId === "function" ? options.fromEntryId() : options.fromEntryId;
5825
+ const page = await this.runtime.fetchTurnsPage(
5826
+ this.sessionId,
5827
+ fromEntryId ? { fromEntryId } : void 0,
5828
+ signal ? { signal } : void 0
5829
+ );
5830
+ if (this.disposed || operationEpoch !== this.timelineEpoch) {
5831
+ restoreCursor();
5832
+ return false;
5833
+ }
5834
+ if (options.isStale?.()) {
5835
+ restoreCursor();
5836
+ return false;
5837
+ }
5838
+ if (!this.joined) this.seedToolCallDeliveryBaseline(page.turns);
5839
+ this.update((state) => {
5840
+ const next = withTurns(
5841
+ state,
5842
+ replaceWindow(state.turns, page.turns, liveRevisionAtStart, {
5843
+ current: this.liveRevision,
5844
+ byTurnId: this.liveRevisions
5845
+ })
5846
+ );
5847
+ return {
5848
+ ...next,
5849
+ mode: this.modeRevision === modeRevisionAtStart ? page.currentMode : state.mode,
5850
+ nextBefore: page.nextBefore,
5851
+ loadingOlder: false
5852
+ };
5853
+ });
5854
+ this.pruneOptimisticLiveRevisions();
5855
+ this.projectionBuilder._seedTurns(this.state.turns);
5856
+ return true;
5857
+ } catch (error) {
5858
+ options.onError?.(error);
5859
+ if (!this.disposed && operationEpoch === this.timelineEpoch) {
5860
+ this.update((state) => ({
5861
+ ...state,
5862
+ nextBefore: previousNextBefore,
5863
+ loadingOlder: false
5864
+ }));
5865
+ }
5866
+ if (options.suppressErrors === false) throw error;
5867
+ return false;
5868
+ } finally {
5869
+ this.replaceInFlight -= 1;
5870
+ }
5871
+ };
5872
+ const run = this.replaceQueue.then(operation);
5873
+ this.replaceQueue = run.catch(() => void 0);
5874
+ return run;
5875
+ }
5876
+ invalidateOlderPaging() {
5877
+ this.pagingGeneration += 1;
5878
+ this.olderPending = null;
5879
+ }
5880
+ beginCommandRecovery() {
5881
+ this.commandRecoveryGeneration += 1;
5882
+ this.commandRestoreRetryAttempt = 0;
5883
+ this.bufferedCommandDeliveries ??= [];
5884
+ if (this.commandRestoreRetryHandle !== null) {
5885
+ globalThis.clearTimeout(this.commandRestoreRetryHandle);
5886
+ this.commandRestoreRetryHandle = null;
5887
+ }
5888
+ return this.commandRecoveryGeneration;
5889
+ }
5890
+ scheduleHistoricalCommandRestore(generation) {
5891
+ const restore = async () => {
5892
+ if (this.disposed || generation !== this.commandRecoveryGeneration) return;
5893
+ try {
5894
+ const commands = await this.runtime.fetchCommands(this.sessionId);
5895
+ if (this.disposed) return;
5896
+ for (const command of commands) {
5897
+ if (this.disposed) return;
5898
+ this.deliverCommandNow({
5899
+ deliveryKey: commandDeliveryKey(command.deliveryId),
5900
+ action: command.action,
5901
+ payload: command.payload,
5902
+ toolCallId: command.toolCallId
5903
+ });
5904
+ }
5905
+ if (generation !== this.commandRecoveryGeneration) return;
5906
+ this.commandRestoreRetryAttempt = 0;
5907
+ this.flushBufferedCommands(generation);
5908
+ } catch {
5909
+ if (!this.disposed && generation === this.commandRecoveryGeneration && this.commandRestoreRetryHandle === null) {
5910
+ const retryDelay = Math.min(
5911
+ COMMAND_RESTORE_RETRY_BASE_MS * 2 ** this.commandRestoreRetryAttempt,
5912
+ COMMAND_RESTORE_RETRY_MAX_MS
5913
+ );
5914
+ this.commandRestoreRetryAttempt += 1;
5915
+ this.commandRestoreRetryHandle = globalThis.setTimeout(() => {
5916
+ this.commandRestoreRetryHandle = null;
5917
+ if (!this.disposed && generation === this.commandRecoveryGeneration) {
5918
+ this.scheduleHistoricalCommandRestore(generation);
5919
+ }
5920
+ }, retryDelay);
5921
+ }
5922
+ }
5923
+ };
5924
+ const run = this.commandRestoreQueue.then(restore);
5925
+ this.commandRestoreQueue = run.catch(() => void 0);
5926
+ }
5927
+ flushBufferedCommands(generation) {
5928
+ const pending = [...this.bufferedCommandDeliveries ?? []];
5929
+ this.bufferedCommandDeliveries = [];
5930
+ let index = 0;
5931
+ while (index < pending.length) {
5932
+ if (this.disposed) return;
5933
+ if (generation !== this.commandRecoveryGeneration) {
5934
+ const currentGenerationBuffer = this.bufferedCommandDeliveries ?? [];
5935
+ this.bufferedCommandDeliveries = [
5936
+ ...pending.slice(index),
5937
+ ...currentGenerationBuffer
5938
+ ];
5939
+ return;
5940
+ }
5941
+ this.deliverCommandNow(pending[index]);
5942
+ index += 1;
5943
+ const reentrantBuffer = this.bufferedCommandDeliveries;
5944
+ if (index === pending.length && reentrantBuffer && reentrantBuffer.length > 0) {
5945
+ pending.push(...reentrantBuffer);
5946
+ this.bufferedCommandDeliveries = [];
5947
+ }
5948
+ }
5949
+ if (!this.disposed && generation === this.commandRecoveryGeneration) {
5950
+ this.bufferedCommandDeliveries = null;
5951
+ }
5952
+ }
5953
+ /** 首次订阅前的 REST 历史建立语义事件基线,随后同一工具的订阅回放不再投递。 */
5954
+ seedToolCallDeliveryBaseline(turns) {
3998
5955
  for (const turn of turns) {
3999
- for (const block of turn.blocks) {
4000
- if (block.type === "tool_bridge" && isToolBridgeContent(block.content)) {
4001
- this.dispatchCommand(turn, block);
5956
+ for (const toolCall of turn.tool_calls) {
5957
+ this.seenToolCallIds.add(toolCall.id);
5958
+ if (toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled") {
5959
+ this.finishedToolCallIds.add(toolCall.id);
4002
5960
  }
4003
5961
  }
4004
5962
  }
4005
5963
  }
4006
- ensureJoined() {
5964
+ ensureJoined(liveRevisionBoundary = this.liveRevision) {
4007
5965
  if (this.joined) return Promise.resolve();
4008
5966
  if (this.joinPending) return this.joinPending;
4009
5967
  const join = this.runtime.emitWithAck(
4010
5968
  "session:subscribe",
4011
- { session_id: this.sessionId, event_format: "raw", resume: true },
5969
+ {
5970
+ session_id: this.sessionId,
5971
+ event_format: "raw",
5972
+ resume: true
5973
+ },
4012
5974
  5e3
4013
5975
  ).then((response) => {
4014
5976
  if (response && response.ok === false) {
4015
- throw new Error(
4016
- typeof response.message === "string" && response.message ? response.message : "Session subscription failed"
5977
+ throw new AgentSessionError(
5978
+ typeof response.message === "string" && response.message ? response.message : "Session subscription failed",
5979
+ typeof response.code === "string" ? response.code : void 0
4017
5980
  );
4018
5981
  }
4019
5982
  this.joined = true;
4020
- this._applyRunningState(Boolean(response?.is_running));
5983
+ this._applyRunningState(Boolean(response?.is_running), liveRevisionBoundary);
4021
5984
  }).finally(() => {
4022
5985
  this.joinPending = null;
4023
5986
  });
@@ -4025,6 +5988,7 @@ var AgentSession = class _AgentSession {
4025
5988
  return join;
4026
5989
  }
4027
5990
  async ensureIdleBeforeSend() {
5991
+ if (this.state.isStreaming) return;
4028
5992
  if (Date.now() - this.recentChatEndAt > 3e3) return;
4029
5993
  const deadline = Date.now() + 3e3;
4030
5994
  while (Date.now() < deadline) {
@@ -4059,6 +6023,7 @@ var SessionHub = class {
4059
6023
  connectionListeners = /* @__PURE__ */ new Set();
4060
6024
  bound = false;
4061
6025
  hasConnected = false;
6026
+ authBusyReconnect = new AuthBusyReconnect();
4062
6027
  constructor(client) {
4063
6028
  this.client = client;
4064
6029
  }
@@ -4080,16 +6045,28 @@ var SessionHub = class {
4080
6045
  * 只有最后一份归还才退订房间——否则 StrictMode 双跑、路由抖动这类"连两次
4081
6046
  * 只释放一次"的场景会把仍在用的会话退订掉,之后所有 turn/chat 事件静默丢失。
4082
6047
  */
4083
- async connect(sessionId) {
6048
+ async connect(sessionId, options = {}) {
4084
6049
  const existing = this.sessions.get(sessionId);
4085
6050
  if (existing && !existing.isDisposed) {
4086
6051
  this.hold(sessionId);
6052
+ try {
6053
+ options.setup?.(existing);
6054
+ } catch (error) {
6055
+ existing.dispose();
6056
+ throw error;
6057
+ }
4087
6058
  return this.bootstrapPending.get(sessionId) ?? existing;
4088
6059
  }
4089
6060
  this.ensureBound();
4090
6061
  const session = new AgentSession(sessionId, this.runtimeFor(), this.connection);
4091
6062
  this.sessions.set(sessionId, session);
4092
6063
  this.hold(sessionId);
6064
+ try {
6065
+ options.setup?.(session);
6066
+ } catch (error) {
6067
+ session.dispose();
6068
+ throw error;
6069
+ }
4093
6070
  this.ensureConnected();
4094
6071
  const bootstrap = session._bootstrap().then(() => session);
4095
6072
  this.bootstrapPending.set(sessionId, bootstrap);
@@ -4113,6 +6090,7 @@ var SessionHub = class {
4113
6090
  /** 手动重连(例如登录态变化后)。 */
4114
6091
  reconnect() {
4115
6092
  this.ensureBound();
6093
+ this.authBusyReconnect.cancel();
4116
6094
  const socket = this.client.socket();
4117
6095
  socket.disconnect();
4118
6096
  this.setConnection("connecting");
@@ -4120,11 +6098,13 @@ var SessionHub = class {
4120
6098
  }
4121
6099
  /** 断开连接(不清理 session 实例)。 */
4122
6100
  disconnect() {
6101
+ this.authBusyReconnect.cancel();
4123
6102
  this.client.socket().disconnect();
4124
6103
  this.setConnection("disconnected");
4125
6104
  }
4126
6105
  /** 清理当前身份下缓存的全部会话,避免切换登录身份后复用旧快照。 */
4127
6106
  disposeAll() {
6107
+ this.authBusyReconnect.cancel();
4128
6108
  this.bootstrapPending.clear();
4129
6109
  for (const session of [...this.sessions.values()]) {
4130
6110
  this.forceRelease(session);
@@ -4136,6 +6116,7 @@ var SessionHub = class {
4136
6116
  * turn:* / chat:* 事件都会静默丢弃。
4137
6117
  */
4138
6118
  _handleSocketReplaced() {
6119
+ this.authBusyReconnect.cancel();
4139
6120
  this.bound = false;
4140
6121
  this.setConnection("disconnected");
4141
6122
  if (this.sessions.size === 0) return;
@@ -4148,18 +6129,11 @@ var SessionHub = class {
4148
6129
  ;
4149
6130
  this.client.socket().emit(event, data);
4150
6131
  },
4151
- emitWithAck: (event, data, timeoutMs) => new Promise((resolve, reject) => {
4152
- ;
4153
- this.client.socket().timeout(timeoutMs).emit(event, data, (err, response) => {
4154
- if (err) {
4155
- reject(err);
4156
- return;
4157
- }
4158
- resolve(response);
4159
- });
4160
- }),
6132
+ emitWithAck: (event, data, timeoutMs) => this.client.requestCommand(event, data, timeoutMs),
4161
6133
  ensureConnected: () => this.ensureConnected(),
4162
- fetchTurns: (sessionId) => this.client.sessions.getSessionTurns(sessionId),
6134
+ fetchTurnsPage: (sessionId, query, init) => this.client.sessions.getSessionTurnsPage(sessionId, query, init),
6135
+ fetchTurnIndex: (sessionId) => this.client.sessions.getSessionTurnIndex(sessionId),
6136
+ fetchCommands: (sessionId) => this.client.sessions.getSessionTurnCommands(sessionId),
4163
6137
  fetchSessionInfo: (sessionId) => this.client.sessions.getSession(sessionId),
4164
6138
  updateReplay: (sessionId, payload) => this.client.sessions.updateReplaySession(sessionId, payload).then((result) => result.replay_state ?? null),
4165
6139
  listDir: (sessionId, dirPath) => this.client.sessions.listDir(sessionId, dirPath),
@@ -4187,6 +6161,9 @@ var SessionHub = class {
4187
6161
  if (this.sessions.get(session.sessionId) === session) {
4188
6162
  this.sessions.delete(session.sessionId);
4189
6163
  }
6164
+ if (this.sessions.size === 0) {
6165
+ this.authBusyReconnect.cancel();
6166
+ }
4190
6167
  ;
4191
6168
  this.client.socket().emit("session:unsubscribe", { session_id: session.sessionId });
4192
6169
  return true;
@@ -4195,10 +6172,13 @@ var SessionHub = class {
4195
6172
  forceRelease(session) {
4196
6173
  this.holdCounts.delete(session.sessionId);
4197
6174
  session.dispose();
6175
+ if (this.sessions.size === 0) {
6176
+ this.authBusyReconnect.cancel();
6177
+ }
4198
6178
  }
4199
6179
  ensureConnected() {
4200
6180
  const socket = this.client.socket();
4201
- if (socket.connected || socket.active) return;
6181
+ if (socket.connected || socket.active || this.authBusyReconnect.pending) return;
4202
6182
  this.setConnection("connecting");
4203
6183
  socket.connect();
4204
6184
  }
@@ -4230,17 +6210,35 @@ var SessionHub = class {
4230
6210
  this.bound = true;
4231
6211
  const socket = this.client.socket();
4232
6212
  socket.on("connect", () => {
6213
+ this.authBusyReconnect.cancel();
4233
6214
  const isReconnect = this.hasConnected;
4234
6215
  this.hasConnected = true;
4235
6216
  this.setConnection("connected");
4236
6217
  if (isReconnect) {
4237
6218
  for (const session of this.sessions.values()) {
4238
6219
  void session._rejoinAfterReconnect();
6220
+ void session.refreshQueue();
4239
6221
  }
4240
6222
  }
4241
6223
  });
4242
- socket.on("connect_error", () => this.setConnection("disconnected"));
4243
- socket.on("disconnect", () => this.setConnection("disconnected"));
6224
+ socket.on("connect_error", (error) => {
6225
+ this.setConnection("disconnected");
6226
+ if (this.sessions.size === 0) {
6227
+ this.authBusyReconnect.cancel();
6228
+ return;
6229
+ }
6230
+ if (this.authBusyReconnect.schedule(error, () => {
6231
+ const current = this.client.socket();
6232
+ if (current !== socket || current.connected || current.active) return;
6233
+ this.setConnection("connecting");
6234
+ current.connect();
6235
+ })) return;
6236
+ this.authBusyReconnect.cancel();
6237
+ });
6238
+ socket.on("disconnect", () => {
6239
+ this.authBusyReconnect.cancel();
6240
+ this.setConnection("disconnected");
6241
+ });
4244
6242
  socket.io.on("reconnect_attempt", () => this.setConnection("reconnecting"));
4245
6243
  socket.on("turn:start", (data) => {
4246
6244
  for (const session of this.route(data.session_id)) session._handleTurnStart(data);
@@ -4251,9 +6249,25 @@ var SessionHub = class {
4251
6249
  socket.on("turn:end", (data) => {
4252
6250
  for (const session of this.route(data.session_id)) session._handleTurnEnd(data);
4253
6251
  });
4254
- socket.on("turn:events", (data) => {
4255
- for (const session of this.route(data.session_id)) session._handleTurnEvents(data.events ?? []);
4256
- });
6252
+ socket.on(
6253
+ "turn:timing",
6254
+ (data) => {
6255
+ for (const session of this.route(data.session_id)) {
6256
+ session._handleTurnEvents([
6257
+ { type: "llm:first_token:delivered", payload: data }
6258
+ ]);
6259
+ }
6260
+ }
6261
+ );
6262
+ socket.on(
6263
+ "turn:events",
6264
+ (data, acknowledge) => {
6265
+ acknowledgeTurnEvents(acknowledge);
6266
+ for (const session of this.route(data.session_id)) {
6267
+ session._handleTurnEvents(data.events ?? [], data.replayed === true);
6268
+ }
6269
+ }
6270
+ );
4257
6271
  socket.on("chat:start", (data) => {
4258
6272
  for (const session of this.route(data?.session_id)) session._handleChatStart();
4259
6273
  });
@@ -4262,8 +6276,17 @@ var SessionHub = class {
4262
6276
  session._handleChatEnd(data?.status ?? "completed");
4263
6277
  }
4264
6278
  });
4265
- socket.on("system:error", (data) => {
4266
- for (const session of this.route(data.session_id)) session._handleSystemError(data.message);
6279
+ socket.on(
6280
+ "system:error",
6281
+ (data) => {
6282
+ const code = data.code == null ? void 0 : String(data.code);
6283
+ for (const session of this.route(data.session_id)) {
6284
+ session._handleSystemError(data.message, code);
6285
+ }
6286
+ }
6287
+ );
6288
+ socket.on("chat:queue:updated", (data) => {
6289
+ for (const session of this.route(data?.session_id)) session._handleQueueUpdated(data);
4267
6290
  });
4268
6291
  socket.on(
4269
6292
  "system:notification",
@@ -4381,21 +6404,13 @@ function resolveAuthToken(options) {
4381
6404
  return token ? token : null;
4382
6405
  }
4383
6406
 
4384
- // src/version.ts
4385
- var SDK_NAME = "agent-client";
4386
- var SDK_VERSION = true ? "2610.0.1-alpha.1" : "1.1.1";
4387
-
4388
6407
  // src/socket.ts
4389
- function withSdkIdentity(auth) {
4390
- return { ...auth ?? {}, sdk: SDK_NAME, sdk_version: SDK_VERSION };
4391
- }
4392
6408
  function createSocket(options) {
4393
- const token = resolveAuthToken(options);
4394
6409
  return io(options.baseUrl, {
4395
6410
  path: options.path ?? "/socket.io",
4396
6411
  withCredentials: true,
4397
- query: token ? { token } : void 0,
4398
- auth: typeof options.token === "function" ? (cb) => cb(withSdkIdentity(buildSocketAuth(options))) : withSdkIdentity(buildSocketAuth(options)),
6412
+ ...options.transports ? { transports: options.transports } : {},
6413
+ auth: typeof options.token === "function" ? (cb) => cb(buildSocketAuth(options) ?? {}) : buildSocketAuth(options) ?? {},
4399
6414
  autoConnect: false
4400
6415
  });
4401
6416
  }
@@ -4411,8 +6426,17 @@ var BladeClient = class {
4411
6426
  options;
4412
6427
  auth;
4413
6428
  headless;
6429
+ computers;
4414
6430
  models;
4415
6431
  sessions;
6432
+ /** 按用户读取的插件目录与图标;没有会话时也能用。 */
6433
+ plugins;
6434
+ chatGroups;
6435
+ /**
6436
+ * 全站访问统计:匿名读写,不带 bearer 也不触发登录刷新。
6437
+ * 只有内置 Web 的同源页面使用,第三方宿主页面不会被自动统计。
6438
+ */
6439
+ siteVisits;
4416
6440
  /** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
4417
6441
  hub;
4418
6442
  constructor(options) {
@@ -4422,8 +6446,12 @@ var BladeClient = class {
4422
6446
  };
4423
6447
  this.auth = new AuthResource(this);
4424
6448
  this.headless = new HeadlessResource(this);
6449
+ this.computers = new ComputersResource(this);
4425
6450
  this.models = new ModelsResource(this);
4426
6451
  this.sessions = new SessionsResource(this);
6452
+ this.plugins = new PluginsResource(this);
6453
+ this.chatGroups = new ChatGroupsResource(this);
6454
+ this.siteVisits = new SiteVisitsResource(this);
4427
6455
  this.hub = new SessionHub(this);
4428
6456
  if (this.options.token === void 0) {
4429
6457
  this.runtimeToken = readStoredToken(this.options.baseUrl);
@@ -4518,6 +6546,16 @@ var BladeClient = class {
4518
6546
  }
4519
6547
  return this.socketInstance;
4520
6548
  }
6549
+ requestCommand(event, payload, timeoutMs) {
6550
+ const send = () => new Promise((resolve, reject) => {
6551
+ ;
6552
+ this.socket().timeout(timeoutMs).emit(event, payload, (error, ack) => {
6553
+ if (error) reject(error);
6554
+ else resolve(ack ?? {});
6555
+ });
6556
+ });
6557
+ return this.options.commandTransport?.(event, payload, send) ?? send();
6558
+ }
4521
6559
  async json(method, path, body) {
4522
6560
  const response = await this.fetch(method, path, {
4523
6561
  body: body === void 0 ? void 0 : JSON.stringify(body),
@@ -4582,7 +6620,7 @@ var BladeClient = class {
4582
6620
  signal: init.signal
4583
6621
  });
4584
6622
  if (response.status === 401 && !isRetry && this.shouldRefreshFor401(url)) {
4585
- const refreshed = await this.tryRefresh();
6623
+ const refreshed = await this.tryRefreshUnlessAborted(init.signal);
4586
6624
  if (refreshed) {
4587
6625
  return this.fetch(method, path, init, true);
4588
6626
  }
@@ -4594,7 +6632,7 @@ var BladeClient = class {
4594
6632
  return response;
4595
6633
  }
4596
6634
  buildAuthedUrl(path) {
4597
- const url = new URL(this.toBaseRelativePath(path), this.baseUrlWithTrailingSlash());
6635
+ const url = buildBackendUrl(this.options.baseUrl, path);
4598
6636
  const token = this.resolveTokenForUrl(url);
4599
6637
  if (token) {
4600
6638
  url.searchParams.set("token", token);
@@ -4606,6 +6644,7 @@ var BladeClient = class {
4606
6644
  if (!this.isSameBackendUrl(url)) {
4607
6645
  return result;
4608
6646
  }
6647
+ result.set("X-Blade-Client", `${SDK_NAME}/${SDK_VERSION}`);
4609
6648
  const token = this.resolveRestToken();
4610
6649
  if (token) {
4611
6650
  result.set("Authorization", `Bearer ${token}`);
@@ -4613,14 +6652,11 @@ var BladeClient = class {
4613
6652
  return result;
4614
6653
  }
4615
6654
  buildUrl(path) {
4616
- return new URL(this.toBaseRelativePath(path), this.baseUrlWithTrailingSlash());
6655
+ return buildBackendUrl(this.options.baseUrl, path);
4617
6656
  }
4618
6657
  baseUrlWithTrailingSlash() {
4619
6658
  return this.options.baseUrl.endsWith("/") ? this.options.baseUrl : `${this.options.baseUrl}/`;
4620
6659
  }
4621
- toBaseRelativePath(path) {
4622
- return path.startsWith("/") ? path.slice(1) : path;
4623
- }
4624
6660
  isSameBackendUrl(url) {
4625
6661
  const base = new URL(this.baseUrlWithTrailingSlash());
4626
6662
  if (url.origin !== base.origin) {
@@ -4639,6 +6675,27 @@ var BladeClient = class {
4639
6675
  hasExplicitBearerToken() {
4640
6676
  return Boolean(this.resolveRestToken());
4641
6677
  }
6678
+ /**
6679
+ * 等一次令牌刷新,但调用方中止请求时不再等下去。
6680
+ *
6681
+ * 刷新本身是共享的(`refreshPromise`,别的请求也在等它),这里只是让这次已被取消的
6682
+ * 请求尽早结束——否则它会一直挂在调用方(定位)的串行队列里,用户改选的新目标要等到
6683
+ * 刷新结束才轮得到。
6684
+ */
6685
+ async tryRefreshUnlessAborted(signal) {
6686
+ if (!signal) return this.tryRefresh();
6687
+ if (signal.aborted) throw new DOMException("The operation was aborted.", "AbortError");
6688
+ let onAbort;
6689
+ const aborted = new Promise((_resolve, reject) => {
6690
+ onAbort = () => reject(new DOMException("The operation was aborted.", "AbortError"));
6691
+ signal.addEventListener("abort", onAbort, { once: true });
6692
+ });
6693
+ try {
6694
+ return await Promise.race([this.tryRefresh(), aborted]);
6695
+ } finally {
6696
+ if (onAbort) signal.removeEventListener("abort", onAbort);
6697
+ }
6698
+ }
4642
6699
  async tryRefresh() {
4643
6700
  if (this.refreshPromise) {
4644
6701
  return this.refreshPromise;
@@ -4649,7 +6706,9 @@ var BladeClient = class {
4649
6706
  this.buildUrl(REFRESH_PATH).toString(),
4650
6707
  {
4651
6708
  method: "POST",
4652
- credentials: "include"
6709
+ credentials: "include",
6710
+ // Refresh remains cookie-only; do not attach the REST bearer.
6711
+ headers: { "X-Blade-Client": `${SDK_NAME}/${SDK_VERSION}` }
4653
6712
  }
4654
6713
  );
4655
6714
  if (!response.ok) {
@@ -4776,6 +6835,122 @@ function parseXhrHeaders(rawHeaders) {
4776
6835
  return headers;
4777
6836
  }
4778
6837
 
6838
+ // src/polling-backoff.ts
6839
+ var MAX_BACKOFF_MS = 6e4;
6840
+ var MAX_TIMER_MS = 2147483647;
6841
+ var PollingBackoff = class {
6842
+ constructor(intervalMs) {
6843
+ this.intervalMs = intervalMs;
6844
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0 || intervalMs > MAX_BACKOFF_MS) {
6845
+ throw new RangeError("Polling interval must be between 0 and 60000 ms");
6846
+ }
6847
+ }
6848
+ intervalMs;
6849
+ failures = 0;
6850
+ nextAt = 0;
6851
+ reset() {
6852
+ this.failures = 0;
6853
+ this.nextAt = 0;
6854
+ }
6855
+ failed(error) {
6856
+ if (error instanceof Error && error.name === "AbortError" || error instanceof BladeApiError && error.status < 500 && error.status !== 408 && error.status !== 429) {
6857
+ this.nextAt = false;
6858
+ return;
6859
+ }
6860
+ this.failures = Math.min(this.failures + 1, 10);
6861
+ const ceiling = Math.min(MAX_BACKOFF_MS, this.intervalMs * 2 ** this.failures);
6862
+ const backoff = Math.max(this.intervalMs, ceiling * (0.5 + Math.random() * 0.5));
6863
+ const header = error instanceof BladeApiError ? error.response.headers.get("Retry-After")?.trim() : void 0;
6864
+ const retryAfter = header ? /^\d+$/.test(header) ? Number(header) * 1e3 : Date.parse(header) - Date.now() : 0;
6865
+ const delay = Math.max(backoff, Number.isNaN(retryAfter) ? 0 : retryAfter);
6866
+ this.nextAt = delay > MAX_TIMER_MS ? false : Date.now() + delay;
6867
+ }
6868
+ /** Zero means ready, false means stop, otherwise wait at least this many ms. */
6869
+ waitMs() {
6870
+ return this.nextAt === false ? false : Math.max(0, this.nextAt - Date.now());
6871
+ }
6872
+ };
6873
+
6874
+ // src/platform-endpoints.ts
6875
+ var PLATFORM_SERVICE_NAMES = [
6876
+ "os",
6877
+ "hub",
6878
+ "agent",
6879
+ "oauth",
6880
+ "llmGateway",
6881
+ "gitea",
6882
+ "tile"
6883
+ ];
6884
+ var EMPTY_PLATFORM_ENDPOINTS = Object.freeze({
6885
+ services: Object.freeze({})
6886
+ });
6887
+ function defaultDocumentBaseUrl() {
6888
+ if (typeof document === "undefined") return void 0;
6889
+ if (document.querySelector("base[href]")) return document.baseURI;
6890
+ if (typeof location !== "undefined" && location.origin) return `${location.origin}/`;
6891
+ return document.baseURI;
6892
+ }
6893
+ function normalizeServiceUrl(value) {
6894
+ if (typeof value !== "string" || !value.trim()) return null;
6895
+ if (value.includes("?") || value.includes("#")) return null;
6896
+ try {
6897
+ const url = new URL(value.trim());
6898
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
6899
+ if (url.username || url.password || url.search || url.hash) return null;
6900
+ return url.toString().replace(/\/$/, "");
6901
+ } catch {
6902
+ return null;
6903
+ }
6904
+ }
6905
+ function parsePlatformEndpoints(value) {
6906
+ if (!isRecord(value) || !isRecord(value.services)) return null;
6907
+ const services = {};
6908
+ for (const name of PLATFORM_SERVICE_NAMES) {
6909
+ if (!(name in value.services)) continue;
6910
+ const url = normalizeServiceUrl(value.services[name]);
6911
+ if (!url) return null;
6912
+ if (name === "agent" && new URL(url).pathname !== "/") return null;
6913
+ services[name] = url;
6914
+ }
6915
+ return { services };
6916
+ }
6917
+ async function loadPlatformEndpoints(options = {}) {
6918
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
6919
+ const baseUrl = options.baseUrl ?? defaultDocumentBaseUrl();
6920
+ if (!fetchImpl || !baseUrl) return EMPTY_PLATFORM_ENDPOINTS;
6921
+ const controller = new AbortController();
6922
+ const timeout = globalThis.setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
6923
+ try {
6924
+ const response = await fetchImpl(new URL("config.json", baseUrl), {
6925
+ cache: "no-store",
6926
+ credentials: "same-origin",
6927
+ headers: { Accept: "application/json" },
6928
+ signal: controller.signal
6929
+ });
6930
+ if (!response.ok) return EMPTY_PLATFORM_ENDPOINTS;
6931
+ return parsePlatformEndpoints(await response.json()) ?? EMPTY_PLATFORM_ENDPOINTS;
6932
+ } catch {
6933
+ return EMPTY_PLATFORM_ENDPOINTS;
6934
+ } finally {
6935
+ globalThis.clearTimeout(timeout);
6936
+ }
6937
+ }
6938
+ function resolveServiceUrl(endpoints, name, path = "") {
6939
+ const baseUrl = endpoints.services[name];
6940
+ if (!baseUrl) return null;
6941
+ if (!path) return baseUrl;
6942
+ const pathReference = path.trimStart();
6943
+ if (path.includes("\\") || pathReference.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(pathReference)) {
6944
+ return null;
6945
+ }
6946
+ const base = new URL(`${baseUrl}/`);
6947
+ const target = new URL(path.replace(/^\/+/, ""), base);
6948
+ if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) {
6949
+ return null;
6950
+ }
6951
+ return target.toString();
6952
+ }
6953
+
4779
6954
  // src/commands/protocol.ts
4780
6955
  function isCommandEnvelope(value) {
4781
6956
  if (typeof value !== "object" || value === null) return false;
@@ -4835,6 +7010,19 @@ function connectEmbedded(options) {
4835
7010
  };
4836
7011
  }
4837
7012
 
7013
+ // src/schemas/projection-utils.ts
7014
+ function latestPostChatFollowup(turns) {
7015
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
7016
+ const turn = turns[index];
7017
+ if (turn.loop_id === "root" && (turn.role === "user" || turn.role === "assistant") && (turn.kind ?? "message") === "message" && !turn.blocks.some(
7018
+ (block) => ["mode_change", "plan_status", "ask_user_answer"].includes(block.type)
7019
+ )) {
7020
+ return turn.role === "assistant" ? turn.post_chat_followup ?? null : null;
7021
+ }
7022
+ }
7023
+ return null;
7024
+ }
7025
+
4838
7026
  // src/schemas/solution.ts
4839
7027
  var LayoutType = /* @__PURE__ */ ((LayoutType2) => {
4840
7028
  LayoutType2["Default"] = "default";
@@ -4863,12 +7051,20 @@ var Task = type2({
4863
7051
  });
4864
7052
  export {
4865
7053
  AgentSession,
7054
+ AgentSessionError,
7055
+ AuthBusyReconnect,
4866
7056
  BladeApiError,
4867
7057
  BladeClient,
7058
+ ChatGroupsResource,
4868
7059
  ClientProjectionBuilder,
7060
+ ComputersResource,
4869
7061
  DEFAULT_REPLAY_SPEED,
7062
+ EMPTY_PLATFORM_ENDPOINTS,
7063
+ EMPTY_SESSION_QUEUE,
4870
7064
  LayoutType,
4871
7065
  ModelsResource,
7066
+ PluginsResource,
7067
+ PollingBackoff,
4872
7068
  SDK_NAME,
4873
7069
  SDK_VERSION,
4874
7070
  SessionHub,
@@ -4877,20 +7073,61 @@ export {
4877
7073
  SessionStatus,
4878
7074
  Task,
4879
7075
  TaskStatus,
7076
+ acceptedPostChatFollowupCompletesLatestRun,
7077
+ acknowledgeTurnEvents,
7078
+ authBusyRetryDelayMs,
4880
7079
  buildMessageContent,
7080
+ buildToolUiCardKey,
7081
+ canCancelQueuedMessage,
7082
+ canDeliverQueuedMessage,
7083
+ canEditQueuedMessage,
7084
+ canToggleComputer,
7085
+ chatErrorForDisplay,
7086
+ collectInlineToolUiCards,
7087
+ collectPreviewToolUiCards,
7088
+ computerDaemonVersion,
7089
+ computerOS,
7090
+ computerPlatformLabel,
7091
+ computerState,
4881
7092
  connectEmbedded,
4882
7093
  contentPreview,
7094
+ contextProjectionData,
4883
7095
  createInitialSessionState,
4884
7096
  createSocket,
4885
7097
  extractTextAttachments,
7098
+ getContextDisplayState,
7099
+ getContextGroupDisplayState,
4886
7100
  getFileParts,
4887
7101
  getImageParts,
4888
7102
  getTextContent,
7103
+ groupAdjacentContextRuns,
4889
7104
  groupMessagesByLoop,
7105
+ hasChatRunEvent,
7106
+ inferToolStatus,
7107
+ isAppDevToolUiCard,
7108
+ isBackgroundWakeMessage,
4890
7109
  isCommandEnvelope,
7110
+ isEntryGoneError,
4891
7111
  isHiddenInternalMessage,
4892
7112
  isInboundEnvelope,
7113
+ isInternalStatusToolUiCard,
7114
+ isMcpAppData,
7115
+ isSessionQueueSnapshot,
7116
+ isToolUiCard,
7117
+ latestPostChatFollowup,
7118
+ loadPlatformEndpoints,
4893
7119
  normalizeMessageContent,
7120
+ parseQueueAck,
7121
+ prependOlder,
7122
+ queuePauseReasonLabel,
7123
+ queuedMessageStatusLabel,
7124
+ reconcileHistoricalUserTurns,
7125
+ reconcileOptimisticUserTurns,
7126
+ replaceWindow,
7127
+ resolveServiceUrl,
7128
+ resolveToolUiCardContent,
7129
+ shouldApplySnapshot,
7130
+ sortComputers,
4894
7131
  toReplaySnapshot,
4895
7132
  transformSlashCommand
4896
7133
  };