@blade-hq/agent-client 2610.0.0-beta.9 → 2610.0.0-beta.90

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 (43) hide show
  1. package/README.md +266 -11
  2. package/dist/auth.d.ts +0 -1
  3. package/dist/blade-client.d.ts +24 -3
  4. package/dist/index.d.ts +37 -7
  5. package/dist/index.js +2395 -295
  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 +193 -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 +6 -0
  21. package/dist/schemas/projection.d.ts +37 -2
  22. package/dist/schemas/session.d.ts +10 -1
  23. package/dist/session/agent-session.d.ts +159 -17
  24. package/dist/session/definition.d.ts +6 -1
  25. package/dist/session/errors.d.ts +11 -0
  26. package/dist/session/events.d.ts +16 -2
  27. package/dist/session/history-page.d.ts +9 -0
  28. package/dist/session/hub.d.ts +9 -1
  29. package/dist/session/queue.d.ts +104 -0
  30. package/dist/session/state.d.ts +18 -2
  31. package/dist/shared/auth-busy.d.ts +9 -0
  32. package/dist/shared/projection/builder.d.ts +12 -0
  33. package/dist/shared/projection/context.d.ts +3 -0
  34. package/dist/shared/projection/helpers.d.ts +2 -2
  35. package/dist/shared/projection/history.d.ts +1 -0
  36. package/dist/shared/projection/index.d.ts +2 -1
  37. package/dist/shared/projection/state.d.ts +7 -0
  38. package/dist/shared/tool-ui-card.d.ts +60 -0
  39. package/dist/shared/type-guards.d.ts +2 -0
  40. package/dist/socket.d.ts +2 -0
  41. package/dist/types/socket-events.d.ts +199 -6
  42. package/package.json +1 -1
  43. package/public-api.md +960 -17
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.0-beta.90" : "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) {
@@ -352,8 +365,7 @@ var HeadlessResource = class {
352
365
  socket.on("system:error", onSystemError);
353
366
  socket.emit("session:subscribe", {
354
367
  session_id,
355
- event_format: "raw",
356
- stream_tokens: this.client.options.streamTokens !== false
368
+ event_format: "raw"
357
369
  });
358
370
  socket.emit(
359
371
  "chat:send",
@@ -395,9 +407,6 @@ function errorMessageFromResult(result) {
395
407
  }
396
408
  return null;
397
409
  }
398
- function isRecord(value) {
399
- return typeof value === "object" && value !== null && !Array.isArray(value);
400
- }
401
410
  function ensureSocketConnected(socket, timeoutMs, detail) {
402
411
  if (socket.connected) {
403
412
  return Promise.resolve();
@@ -425,6 +434,81 @@ function ensureSocketConnected(socket, timeoutMs, detail) {
425
434
  });
426
435
  }
427
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
+
428
512
  // src/resources/models.ts
429
513
  var ModelsResource = class {
430
514
  constructor(client) {
@@ -451,6 +535,7 @@ var SessionInfo = type({
451
535
  "shared?": "boolean",
452
536
  "memory_enabled?": "boolean",
453
537
  "is_persistent?": "boolean",
538
+ "initiator?": "'ui' | 'sdk' | 'unknown' | null",
454
539
  "is_headless?": "boolean",
455
540
  "viewer_role?": "'owner' | 'viewer'",
456
541
  "template_id?": "string | null",
@@ -473,7 +558,11 @@ var SessionInfo = type({
473
558
  "disable_tools?": "string[]",
474
559
  "runtime_type?": "string | null",
475
560
  "daemon_id?": "string | null",
561
+ "agent_runtime_id?": "string | null",
476
562
  "workspace_path?": "string | null",
563
+ "ba_version?": "string | null",
564
+ "sandbox_version?": "string | null",
565
+ "chat_group_id?": "string | null",
477
566
  "match?": "unknown"
478
567
  });
479
568
 
@@ -539,7 +628,7 @@ function upsertToolCall(state, toolCallId, toolName, displayName, args) {
539
628
  });
540
629
  }
541
630
  }
542
- function applyToolResult(state, toolCallId, result, status, durationMs, sourceLoop) {
631
+ function applyToolResult(state, toolCallId, result, status, durationMs, ranInParallel, sourceLoop, startedAt = null) {
543
632
  let toolName = "";
544
633
  let displayName = "";
545
634
  let finalResult = result;
@@ -551,6 +640,8 @@ function applyToolResult(state, toolCallId, result, status, durationMs, sourceLo
551
640
  }
552
641
  const update = { status, result: finalResult };
553
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;
554
645
  state.toolCalls[i] = { ...tc, ...update };
555
646
  toolName = tc.tool_name;
556
647
  displayName = tc.display_name;
@@ -732,11 +823,17 @@ function buildCompactionTurn(sequence, compactionId, loopId, status, data) {
732
823
  summary_preview: summaryPreview,
733
824
  summary_full: summaryFull,
734
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,
735
832
  tokens_before: tokensBefore,
736
833
  tokens_after: tokensAfter,
737
834
  saved_ratio: savedRatio,
738
835
  trigger: asTrigger(data.trigger),
739
- 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,
740
837
  fallback_applied: typeof data.fallback_applied === "boolean" ? data.fallback_applied : null
741
838
  };
742
839
  }
@@ -991,6 +1088,12 @@ function createTurnState(turnId, loopId, opts = {}) {
991
1088
  toolCalls: [],
992
1089
  usage: null,
993
1090
  responseDone: false,
1091
+ firstTokenMs: null,
1092
+ streamDurationMs: null,
1093
+ firstTokenDeliveryMs: null,
1094
+ modelStartedAt: null,
1095
+ modelDurationMs: null,
1096
+ modelStartedAtReported: false,
994
1097
  memoryRefs: opts.memoryRefs ?? null,
995
1098
  parentForkToolCallId: null
996
1099
  };
@@ -1008,8 +1111,13 @@ function snapshot(state, sequence, status) {
1008
1111
  tool_calls: [...state.toolCalls],
1009
1112
  model: state.model,
1010
1113
  usage: state.usage ? { ...state.usage } : null,
1011
- duration_ms: Math.max(0, Date.now() - state.startedAt),
1012
- 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,
1013
1121
  context_window: state.contextWindow,
1014
1122
  memory_refs: state.memoryRefs,
1015
1123
  parent_fork_tool_call_id: state.parentForkToolCallId
@@ -1023,10 +1131,134 @@ function toolStatusFromResult(result) {
1023
1131
  return inferToolStatus(text);
1024
1132
  }
1025
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
+
1026
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
+ }
1027
1259
  function hasChatRunEvent(events) {
1028
1260
  return events.some(
1029
- (event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
1261
+ (event) => event.type !== "llm:first_token:delivered" && event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
1030
1262
  );
1031
1263
  }
1032
1264
  function acceptedPostChatFollowupCompletesLatestRun(events, updates) {
@@ -1058,7 +1290,8 @@ function completedResponseText(content) {
1058
1290
  ).filter((part) => part.type === "text").map((part) => String(part.text ?? "")).join("");
1059
1291
  }
1060
1292
  function replaceTextContent(state, content) {
1061
- if (state.blocks.some((block) => block.type === "text")) return;
1293
+ const currentContent = state.blocks.filter((block) => block.type === "text").map((block) => String(block.content ?? "")).join("");
1294
+ if (currentContent === content) return;
1062
1295
  const replacement = { type: "text", content };
1063
1296
  const blocks = [];
1064
1297
  let inserted = false;
@@ -1078,11 +1311,39 @@ function replaceTextContent(state, content) {
1078
1311
  }
1079
1312
  state.blocks = blocks;
1080
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
+ }
1081
1339
  var ClientProjectionBuilder = class {
1082
1340
  activeTurns = /* @__PURE__ */ new Map();
1083
1341
  lastTurnIds = /* @__PURE__ */ new Map();
1084
1342
  latestTurns = /* @__PURE__ */ new Map();
1343
+ turnsById = /* @__PURE__ */ new Map();
1085
1344
  loopDescriptions = /* @__PURE__ */ new Map();
1345
+ /** loop_name → 正在运行的子智能体起点,agent:end 结算并挂到父工具调用上。 */
1346
+ activeSubagentRuns = /* @__PURE__ */ new Map();
1086
1347
  pendingChildPauses = /* @__PURE__ */ new Map();
1087
1348
  pendingMemoryRefs = /* @__PURE__ */ new Map();
1088
1349
  activeCompactions = /* @__PURE__ */ new Map();
@@ -1117,6 +1378,8 @@ var ClientProjectionBuilder = class {
1117
1378
  return this.onMemoryInject(loopId, payload);
1118
1379
  case "memory:inject:none":
1119
1380
  return null;
1381
+ case "context:entry":
1382
+ return this.onContextEntry(payload);
1120
1383
  case "loop:turn":
1121
1384
  return this.onLoopTurn(loopId, payload);
1122
1385
  case "mode:change":
@@ -1140,6 +1403,25 @@ var ClientProjectionBuilder = class {
1140
1403
  }
1141
1404
  if (event.type.startsWith("compaction:"))
1142
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
+ }
1143
1425
  const state = this.activeTurns.get(loopId);
1144
1426
  if (!state) {
1145
1427
  if (event.type === "chat:end") {
@@ -1155,16 +1437,28 @@ var ClientProjectionBuilder = class {
1155
1437
  state.toolCalls = [];
1156
1438
  state.usage = null;
1157
1439
  state.responseDone = false;
1440
+ state.firstTokenMs = null;
1441
+ state.streamDurationMs = null;
1442
+ state.firstTokenDeliveryMs = null;
1158
1443
  return this.syncPatch(state);
1159
1444
  case "llm:thinking:delta":
1445
+ this.applyFirstTokenDelivery(state, payload);
1160
1446
  return this.onDelta(state, "thinking", "", String(payload.content ?? ""));
1161
1447
  case "llm:content:delta":
1448
+ this.applyFirstTokenDelivery(state, payload);
1162
1449
  return this.onDelta(state, "text", "", String(payload.content ?? ""));
1163
1450
  case "llm:content:complete":
1164
1451
  replaceTextContent(state, String(payload.content ?? ""));
1165
1452
  return this.syncPatch(state);
1453
+ case "llm:content:done":
1454
+ this.applyStreamTiming(state, payload);
1455
+ return this.syncPatch(state);
1166
1456
  case "llm:tool_call:created":
1457
+ this.applyFirstTokenDelivery(state, payload);
1167
1458
  return this.onToolCallCreated(state, payload);
1459
+ case "llm:tool_call:arguments:delta":
1460
+ this.applyFirstTokenDelivery(state, payload);
1461
+ return this.onToolCallArgumentsDelta(state, payload);
1168
1462
  case "llm:response:done":
1169
1463
  return this.onResponseDone(state, loopId, payload);
1170
1464
  case "tool:result:delta":
@@ -1191,7 +1485,9 @@ var ClientProjectionBuilder = class {
1191
1485
  this.activeTurns.clear();
1192
1486
  this.lastTurnIds.clear();
1193
1487
  this.latestTurns.clear();
1488
+ this.turnsById.clear();
1194
1489
  this.loopDescriptions.clear();
1490
+ this.activeSubagentRuns.clear();
1195
1491
  this.pendingChildPauses.clear();
1196
1492
  this.pendingMemoryRefs.clear();
1197
1493
  this.activeCompactions.clear();
@@ -1206,15 +1502,18 @@ var ClientProjectionBuilder = class {
1206
1502
  }
1207
1503
  _seedTurns(turns) {
1208
1504
  const latestTurns = /* @__PURE__ */ new Map();
1505
+ const turnsById = /* @__PURE__ */ new Map();
1209
1506
  for (const turn of turns) {
1210
1507
  this.seedSequence(turn.sequence);
1211
1508
  if (!isConversationTurn(turn)) continue;
1509
+ turnsById.set(turn.turn_id, turn);
1212
1510
  const current = latestTurns.get(turn.loop_id);
1213
1511
  if (!current || turn.sequence >= current.sequence) {
1214
1512
  latestTurns.set(turn.loop_id, turn);
1215
1513
  }
1216
1514
  }
1217
1515
  this.latestTurns = latestTurns;
1516
+ this.turnsById = turnsById;
1218
1517
  }
1219
1518
  // --- Event handlers ---
1220
1519
  onMemoryInject(loopId, payload) {
@@ -1232,11 +1531,16 @@ var ClientProjectionBuilder = class {
1232
1531
  }
1233
1532
  return null;
1234
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
+ }
1235
1539
  onLoopTurn(loopId, payload) {
1236
1540
  let turnId = String(payload.turn_id ?? this.nextTurnId(`turn:${loopId}`));
1237
1541
  let memoryRefs = null;
1238
1542
  const pending = this.pendingMemoryRefs.get(loopId);
1239
- if (pending) {
1543
+ if (pending && (!pending.turnId || pending.turnId === turnId)) {
1240
1544
  this.pendingMemoryRefs.delete(loopId);
1241
1545
  if (pending.turnId) turnId = pending.turnId;
1242
1546
  memoryRefs = pending.refs;
@@ -1252,6 +1556,7 @@ var ClientProjectionBuilder = class {
1252
1556
  this.lastTurnIds.set(loopId, turnId);
1253
1557
  const turn = snapshot(state, this.nextSeq(), "streaming");
1254
1558
  this.latestTurns.set(loopId, turn);
1559
+ this.turnsById.set(turn.turn_id, turn);
1255
1560
  return [{ kind: "upsert", turn }];
1256
1561
  }
1257
1562
  onModeChange(loopId, payload) {
@@ -1288,11 +1593,11 @@ var ClientProjectionBuilder = class {
1288
1593
  loopId,
1289
1594
  toolCallId: optStr(payload.tool_call_id) ?? void 0,
1290
1595
  toolName: toolNameFromPayload(payload) ?? void 0,
1291
- 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
1292
1597
  }
1293
1598
  ];
1294
1599
  const change = payload.workspace_change;
1295
- if (isRecord2(change)) {
1600
+ if (isRecord(change)) {
1296
1601
  const source = optStr(change.source) ?? "";
1297
1602
  const projectName = optStr(change.project_name) ?? "";
1298
1603
  const fromPath = optStr(change.from) ?? "";
@@ -1324,6 +1629,13 @@ var ClientProjectionBuilder = class {
1324
1629
  onAgentStart(loopId, payload) {
1325
1630
  const description = optStr(payload.description);
1326
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
+ }
1327
1639
  const turn = buildSystemNotificationTurn(
1328
1640
  this.nextSeq(),
1329
1641
  this.nextTurnId("agent:start"),
@@ -1365,7 +1677,7 @@ var ClientProjectionBuilder = class {
1365
1677
  this.nextTurnId("bg:tasks_completed"),
1366
1678
  loopId,
1367
1679
  "bg:tasks_completed",
1368
- "done",
1680
+ backgroundTaskStatus(payload),
1369
1681
  `Background tasks completed (${count})`,
1370
1682
  detail || null,
1371
1683
  { task_ids: taskIds, count }
@@ -1385,7 +1697,32 @@ var ClientProjectionBuilder = class {
1385
1697
  }
1386
1698
  const directTurn = this.activeTurns.get(loopId) ?? this.latestTurns.get(loopId);
1387
1699
  setToolCallStatus(directTurn, toolCallId, "done", null, null);
1388
- 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 }];
1389
1726
  }
1390
1727
  onUserMessage(_loopId, payload) {
1391
1728
  const entryId = String(payload.entry_id ?? this.nextTurnId("user"));
@@ -1402,8 +1739,12 @@ var ClientProjectionBuilder = class {
1402
1739
  tool_calls: [],
1403
1740
  model: null,
1404
1741
  usage: null,
1405
- duration_ms: 0
1742
+ duration_ms: 0,
1743
+ client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
1406
1744
  };
1745
+ if (payload.start_reason === "background-completion") {
1746
+ turn.blocks.push({ type: "system_notification", content: { notification_type: "bg:wakeup" } });
1747
+ }
1407
1748
  this.latestTurns.set("root", turn);
1408
1749
  return [{ kind: "upsert", turn }];
1409
1750
  }
@@ -1430,13 +1771,7 @@ var ClientProjectionBuilder = class {
1430
1771
  }
1431
1772
  onToolCallCreated(state, payload) {
1432
1773
  if ("arguments_delta" in payload) {
1433
- const delta = String(payload.arguments_delta ?? "");
1434
- const toolCallId2 = String(payload.id ?? "");
1435
- if (delta && toolCallId2) {
1436
- appendToolCallArguments(state, toolCallId2, delta);
1437
- return this.syncPatch(state);
1438
- }
1439
- return null;
1774
+ return this.onToolCallArgumentsDelta(state, payload);
1440
1775
  }
1441
1776
  const toolCallId = String(payload.id ?? "");
1442
1777
  const toolName = toolNameFromPayload(payload);
@@ -1445,16 +1780,21 @@ var ClientProjectionBuilder = class {
1445
1780
  upsertToolCall(state, toolCallId, toolName, displayName, args);
1446
1781
  return this.syncPatch(state);
1447
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
+ }
1448
1790
  onResponseDone(state, loopId, payload) {
1449
1791
  state.responseDone = true;
1792
+ this.applyStreamTiming(state, payload);
1450
1793
  const model = optStr(payload.model);
1451
1794
  if (model) state.model = model;
1452
1795
  if (typeof payload.usage === "object" && payload.usage !== null) {
1453
1796
  state.usage = { ...payload.usage };
1454
1797
  }
1455
- if ("content" in payload) {
1456
- replaceTextContent(state, completedResponseText(payload.content));
1457
- }
1458
1798
  const toolCalls = payload.tool_calls;
1459
1799
  if (Array.isArray(toolCalls)) {
1460
1800
  for (const raw of toolCalls) {
@@ -1472,11 +1812,42 @@ var ClientProjectionBuilder = class {
1472
1812
  );
1473
1813
  }
1474
1814
  }
1475
- 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
+ )) {
1476
1822
  return this.finalize(loopId, "completed");
1477
1823
  }
1478
1824
  return this.syncPatch(state);
1479
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
+ }
1480
1851
  onToolResultDone(state, loopId, payload) {
1481
1852
  const toolCallId = String(payload.tool_call_id ?? "");
1482
1853
  const streamedResult = state.toolCalls.find((toolCall) => toolCall.id === toolCallId)?.result;
@@ -1484,9 +1855,12 @@ var ClientProjectionBuilder = class {
1484
1855
  const status = toolStatusFromResult(content);
1485
1856
  const rawDuration = payload.duration_ms;
1486
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;
1487
1859
  const sl = sourceLoopFor(state.loopId, this.loopDescriptions);
1488
- applyToolResult(state, toolCallId, content, status, durationMs, sl);
1489
- 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)) {
1490
1864
  return this.finalize(loopId, "completed");
1491
1865
  }
1492
1866
  return this.syncPatch(state);
@@ -1515,10 +1889,13 @@ var ClientProjectionBuilder = class {
1515
1889
  onToolBridge(state, payload) {
1516
1890
  const bridge = payload.bridge;
1517
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;
1518
1894
  state.blocks.push({
1519
1895
  type: "tool_bridge",
1520
1896
  content: bridge,
1521
- tool_call_id: String(payload.tool_call_id ?? "") || null
1897
+ delivery_id: String(payload.delivery_id ?? "") || null,
1898
+ tool_call_id: toolCallId
1522
1899
  });
1523
1900
  return this.syncPatch(state);
1524
1901
  }
@@ -1527,6 +1904,11 @@ var ClientProjectionBuilder = class {
1527
1904
  const toolName = String(payload.tool_name ?? "");
1528
1905
  const displayName = this.displayNameResolver(toolName);
1529
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
+ }
1530
1912
  return this.syncPatch(state);
1531
1913
  }
1532
1914
  onChildPause(state, loopId, payload) {
@@ -1632,6 +2014,7 @@ var ClientProjectionBuilder = class {
1632
2014
  }
1633
2015
  onAgentEnd(state, loopId, payload) {
1634
2016
  const ok = payload.ok !== false;
2017
+ this.settleSubagentRun(loopId, payload);
1635
2018
  const status = ok ? "done" : "error";
1636
2019
  const title = `Agent: ${payload.description ?? payload.skill_id ?? "agent"}`;
1637
2020
  const detail = optStr(payload.error);
@@ -1639,6 +2022,7 @@ var ClientProjectionBuilder = class {
1639
2022
  skill_id: optStr(payload.skill_id),
1640
2023
  description: optStr(payload.description),
1641
2024
  parent_fork_tool_call_id: optStr(payload.parent_fork_tool_call_id),
2025
+ ...typeof payload.run_status === "string" ? { run_status: payload.run_status } : {},
1642
2026
  ok
1643
2027
  };
1644
2028
  if (state) {
@@ -1658,6 +2042,32 @@ var ClientProjectionBuilder = class {
1658
2042
  );
1659
2043
  return [{ kind: "upsert", turn }];
1660
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
+ }
1661
2071
  dropIncompleteToolCalls(state) {
1662
2072
  if (state.responseDone) return;
1663
2073
  state.toolCalls = [];
@@ -1707,6 +2117,7 @@ var ClientProjectionBuilder = class {
1707
2117
  syncPatch(state) {
1708
2118
  const turn = snapshot(state, this.nextSeq(), "streaming");
1709
2119
  this.latestTurns.set(state.loopId, turn);
2120
+ this.turnsById.set(turn.turn_id, turn);
1710
2121
  this.lastTurnIds.set(state.loopId, turn.turn_id);
1711
2122
  return [{ kind: "upsert", turn }];
1712
2123
  }
@@ -1717,6 +2128,7 @@ var ClientProjectionBuilder = class {
1717
2128
  const turn = snapshot(state, this.nextSeq(), status);
1718
2129
  this.lastTurnIds.set(loopId, turn.turn_id);
1719
2130
  this.latestTurns.set(loopId, turn);
2131
+ this.turnsById.set(turn.turn_id, turn);
1720
2132
  return [{ kind: "upsert", turn }];
1721
2133
  }
1722
2134
  shouldFinalizeAfterToolResults(state) {
@@ -1754,9 +2166,6 @@ var ClientProjectionBuilder = class {
1754
2166
  return `${prefix}:${this.syntheticCounter}:${hex}`;
1755
2167
  }
1756
2168
  };
1757
- function isRecord2(value) {
1758
- return typeof value === "object" && value !== null && !Array.isArray(value);
1759
- }
1760
2169
  function optStr(value) {
1761
2170
  if (value == null || value === "") return null;
1762
2171
  return String(value);
@@ -1776,6 +2185,101 @@ function toolArgsFromPayload(payload) {
1776
2185
  return "";
1777
2186
  }
1778
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
+
1779
2283
  // src/shared/projection/history.ts
1780
2284
  function projectHistory(entries) {
1781
2285
  const turns = [];
@@ -1783,6 +2287,8 @@ function projectHistory(entries) {
1783
2287
  const latestAssistantByLoop = /* @__PURE__ */ new Map();
1784
2288
  const pendingChildPauses = /* @__PURE__ */ new Map();
1785
2289
  const answeredToolCallIds = /* @__PURE__ */ new Set();
2290
+ const nativeToolTurns = /* @__PURE__ */ new Set();
2291
+ const turnEntryAliases = /* @__PURE__ */ new Map();
1786
2292
  let latestRootMessage = null;
1787
2293
  let latestRootAssistant = null;
1788
2294
  let sequence = 0;
@@ -1790,7 +2296,40 @@ function projectHistory(entries) {
1790
2296
  const kind = String(entry.kind ?? "");
1791
2297
  const loopId = String(entry.loop_name ?? "root");
1792
2298
  const id = String(entry.id ?? `history-${sequence + 1}`);
1793
- 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)) {
1794
2333
  const assistantEntryId = String(entry.data.assistant_entry_id ?? "");
1795
2334
  if (latestRootMessage?.role === "assistant" && latestRootMessage.turn_id === assistantEntryId) {
1796
2335
  const followup = normalizePostChatFollowup(entry.data, assistantEntryId);
@@ -1798,7 +2337,15 @@ function projectHistory(entries) {
1798
2337
  }
1799
2338
  continue;
1800
2339
  }
1801
- 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)) {
1802
2349
  const message = entry.message;
1803
2350
  const role = String(message.role ?? "");
1804
2351
  if (role === "tool") {
@@ -1808,6 +2355,12 @@ function projectHistory(entries) {
1808
2355
  if (owner && toolCall) {
1809
2356
  toolCall.status = message._error ? "error" : inferToolStatus(String(message.content ?? ""));
1810
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;
1811
2364
  owner.blocks.push({
1812
2365
  type: "tool_result",
1813
2366
  content: message.content ?? "",
@@ -1817,8 +2370,11 @@ function projectHistory(entries) {
1817
2370
  continue;
1818
2371
  }
1819
2372
  if (role !== "user" && role !== "assistant" && role !== "system") continue;
1820
- const toolCalls = role === "assistant" ? buildToolCalls(message.tool_calls) : [];
2373
+ const toolCalls = role === "assistant" && !message._error ? buildToolCalls(message.tool_calls) : [];
1821
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
+ }
1822
2378
  const turn = {
1823
2379
  id,
1824
2380
  sequence: ++sequence,
@@ -1830,14 +2386,52 @@ function projectHistory(entries) {
1830
2386
  blocks,
1831
2387
  tool_calls: toolCalls,
1832
2388
  model: stringOrNull(message.model),
1833
- usage: isRecord3(message._usage) ? { ...message._usage } : null,
1834
- duration_ms: numberOrZero(message._duration_ms),
1835
- started_at: typeof entry.timestamp === "string" ? entry.timestamp : void 0
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)
1836
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
+ }
1837
2429
  if (loopId === "root" && (role === "user" || role === "assistant")) {
1838
2430
  latestRootMessage = turn;
1839
2431
  }
1840
2432
  turns.push(turn);
2433
+ turnEntryAliases.set(id, turn);
2434
+ if (isNativeToolEntry) nativeToolTurns.add(turn);
1841
2435
  if (role === "assistant") latestAssistantByLoop.set(loopId, turn);
1842
2436
  if (loopId === "root") {
1843
2437
  if (role === "assistant") latestRootAssistant = turn;
@@ -1846,10 +2440,25 @@ function projectHistory(entries) {
1846
2440
  for (const toolCall of toolCalls) toolOwners.set(toolCall.id, turn);
1847
2441
  continue;
1848
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
+ }
1849
2458
  if (kind === "mode_change") {
1850
2459
  turns.push(markerTurn(id, ++sequence, loopId, "mode_change", entry.data ?? {}));
1851
2460
  } else if (kind === "workspace_change") {
1852
- const change = isRecord3(entry.data) ? entry.data : {};
2461
+ const change = isRecord(entry.data) ? entry.data : {};
1853
2462
  const source = stringOrNull(change.source) ?? "";
1854
2463
  const projectName = stringOrNull(change.project_name) ?? "";
1855
2464
  const fromPath = stringOrNull(change.from) ?? "";
@@ -1878,19 +2487,22 @@ function projectHistory(entries) {
1878
2487
  } else if (kind === "tool_ui" || kind === "tool_bridge") {
1879
2488
  const data = entry.data ?? {};
1880
2489
  const toolCallId = String(data.tool_call_id ?? "");
1881
- const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
1882
- const content = kind === "tool_ui" ? data.ui : data.bridge;
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;
1883
2494
  if (owner && content !== void 0) {
1884
2495
  owner.blocks.push({
1885
2496
  type: kind,
1886
2497
  content,
2498
+ ...kind === "tool_bridge" ? { delivery_id: id } : {},
1887
2499
  tool_call_id: toolCallId || null
1888
2500
  });
1889
2501
  }
1890
2502
  } else if (kind === "compaction" || kind === "tool_result_archive") {
1891
2503
  const data = entry.data ?? {};
1892
2504
  const compactionId = String(data.compaction_id ?? entry.id ?? `history-${sequence + 1}`);
1893
- let content = isRecord3(entry.message) ? entry.message.content : void 0;
2505
+ let content = isRecord(entry.message) ? entry.message.content : void 0;
1894
2506
  if (kind === "tool_result_archive") {
1895
2507
  const archivedCount = Array.isArray(data.archived_files) ? data.archived_files.length : 0;
1896
2508
  content = `<compaction-summary>
@@ -1898,18 +2510,24 @@ function projectHistory(entries) {
1898
2510
  </compaction-summary>`;
1899
2511
  }
1900
2512
  turns.push(
1901
- buildCompactionTurn(++sequence, compactionId, loopId, "completed", {
1902
- ...data,
1903
- content
1904
- })
2513
+ buildCompactionTurn(
2514
+ ++sequence,
2515
+ compactionId,
2516
+ loopId,
2517
+ data.status === "failed" ? "failed" : "completed",
2518
+ {
2519
+ ...data,
2520
+ content
2521
+ }
2522
+ )
1905
2523
  );
1906
2524
  } else if (kind === "child_pause") {
1907
2525
  const data = entry.data ?? {};
1908
- const sourceLoop = isRecord3(data.source_loop) ? data.source_loop : {};
2526
+ const sourceLoop = isRecord(data.source_loop) ? data.source_loop : {};
1909
2527
  const childLoopId = String(data.child_loop_name ?? sourceLoop.name ?? "");
1910
2528
  const childToolCallId = String(data.child_pause_tool_call_id ?? "");
1911
2529
  const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
1912
- const pauseToolData = isRecord3(data.pause_tool_data) ? data.pause_tool_data : {};
2530
+ const pauseToolData = isRecord(data.pause_tool_data) ? data.pause_tool_data : {};
1913
2531
  const description = String(sourceLoop.description ?? "");
1914
2532
  applyAskUserPauseToTurn(
1915
2533
  latestAssistantByLoop.get(childLoopId),
@@ -1997,15 +2615,15 @@ function parseToolArguments(value) {
1997
2615
  if (!value) return {};
1998
2616
  try {
1999
2617
  const parsed = JSON.parse(value);
2000
- return isRecord3(parsed) ? parsed : {};
2618
+ return isRecord(parsed) ? parsed : {};
2001
2619
  } catch {
2002
2620
  return {};
2003
2621
  }
2004
2622
  }
2005
2623
  function buildToolCalls(value) {
2006
2624
  if (!Array.isArray(value)) return [];
2007
- return value.filter(isRecord3).map((raw) => {
2008
- const fn = isRecord3(raw.function) ? raw.function : {};
2625
+ return value.filter(isRecord).map((raw) => {
2626
+ const fn = isRecord(raw.function) ? raw.function : {};
2009
2627
  const name = String(fn.name ?? "");
2010
2628
  return {
2011
2629
  id: String(raw.id ?? ""),
@@ -2021,23 +2639,40 @@ function buildBlocks(message, toolCalls) {
2021
2639
  const displayContent = message.role === "user" ? message._display_content : void 0;
2022
2640
  const stored = message._blocks;
2023
2641
  const blocks = [];
2642
+ const storedToolCallIds = /* @__PURE__ */ new Set();
2024
2643
  if (displayContent !== void 0) {
2025
2644
  blocks.push({ type: "text", content: displayContent });
2026
2645
  } else if (Array.isArray(stored)) {
2027
2646
  for (const raw of stored) {
2028
- if (!isRecord3(raw)) continue;
2647
+ if (!isRecord(raw)) continue;
2029
2648
  const type3 = String(raw.type ?? "");
2030
2649
  if (type3 === "thinking") blocks.push({ type: type3, content: raw.thinking ?? raw.content ?? "" });
2031
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
+ }
2032
2664
  }
2033
2665
  } else if (message.content !== void 0 && message.content !== "") {
2034
2666
  blocks.push({ type: "text", content: message.content });
2035
2667
  }
2036
2668
  for (const toolCall of toolCalls) {
2669
+ if (storedToolCallIds.has(toolCall.id)) continue;
2037
2670
  blocks.push({
2038
2671
  type: "tool_use",
2039
- content: { name: toolCall.tool_name, arguments: toolCall.arguments },
2040
- 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
2041
2676
  });
2042
2677
  }
2043
2678
  return blocks;
@@ -2058,14 +2693,11 @@ function markerTurn(id, sequence, loopId, type3, content, toolCallId = null) {
2058
2693
  duration_ms: 0
2059
2694
  };
2060
2695
  }
2061
- function isRecord3(value) {
2062
- return typeof value === "object" && value !== null && !Array.isArray(value);
2063
- }
2064
2696
  function stringOrNull(value) {
2065
2697
  return typeof value === "string" ? value : null;
2066
2698
  }
2067
- function numberOrZero(value) {
2068
- return typeof value === "number" ? value : 0;
2699
+ function numberOrNull(value) {
2700
+ return typeof value === "number" && value >= 0 ? value : null;
2069
2701
  }
2070
2702
  function findLatestAssistant(turns, loopId) {
2071
2703
  for (let index = turns.length - 1; index >= 0; index -= 1) {
@@ -2076,6 +2708,10 @@ function findLatestAssistant(turns, loopId) {
2076
2708
  }
2077
2709
 
2078
2710
  // src/resources/sessions.ts
2711
+ function isEntryGoneError(error) {
2712
+ const status = error?.status;
2713
+ return status === 404 || status === 409;
2714
+ }
2079
2715
  function toCreateSessionPayload(request) {
2080
2716
  const solutionRef = request.solution_ref ?? externalSolutionRef(request.solution_id);
2081
2717
  return {
@@ -2094,13 +2730,14 @@ function toCreateSessionPayload(request) {
2094
2730
  solution_snapshot_session_id: request.solution_snapshot_session_id ?? null,
2095
2731
  compaction_ratio: request.compaction_ratio ?? null,
2096
2732
  ...request.runtime_type ? { runtime_type: request.runtime_type } : {},
2097
- ...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 } : {}
2098
2736
  };
2099
2737
  }
2100
2738
  var builtinSolutionIds = /* @__PURE__ */ new Set([
2101
2739
  "app-dev",
2102
2740
  "general_chat",
2103
- "multica",
2104
2741
  "night_build",
2105
2742
  "skill_editor",
2106
2743
  "smart_bid",
@@ -2123,6 +2760,54 @@ var SessionsResource = class {
2123
2760
  this.client = client;
2124
2761
  }
2125
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
+ }
2126
2811
  /**
2127
2812
  * 连接一个已存在的会话,返回实时会话对象 AgentSession
2128
2813
  * (自动完成历史加载、Socket.IO 订阅、断线重连补数)。
@@ -2130,6 +2815,56 @@ var SessionsResource = class {
2130
2815
  connect(sessionId) {
2131
2816
  return this.client.hub.connect(sessionId);
2132
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
+ }
2133
2868
  /** 创建新会话并直接连接,返回可收发消息的 AgentSession。 */
2134
2869
  async create(request = {}) {
2135
2870
  if (isSessionDefinition(request)) {
@@ -2226,7 +2961,8 @@ var SessionsResource = class {
2226
2961
  template_id_prefix,
2227
2962
  solution_id,
2228
2963
  solution_registry_id,
2229
- q
2964
+ q,
2965
+ chat_group_id
2230
2966
  }) {
2231
2967
  const searchParams = new URLSearchParams();
2232
2968
  searchParams.set("limit", String(limit));
@@ -2239,6 +2975,7 @@ var SessionsResource = class {
2239
2975
  }
2240
2976
  const trimmedQuery = q?.trim();
2241
2977
  if (trimmedQuery) searchParams.set("q", trimmedQuery);
2978
+ if (chat_group_id) searchParams.set("chat_group_id", chat_group_id);
2242
2979
  return this.client.json("GET", `/api/sessions?${searchParams.toString()}`);
2243
2980
  }
2244
2981
  async listSessions(template_id_prefix, solution_id) {
@@ -2329,6 +3066,55 @@ var SessionsResource = class {
2329
3066
  );
2330
3067
  return projectHistory(entries);
2331
3068
  }
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
+ }
2332
3118
  listResultFeedback(sessionId) {
2333
3119
  return this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/feedback`);
2334
3120
  }
@@ -2396,8 +3182,9 @@ var SessionsResource = class {
2396
3182
  `/api/sessions/${sessionId}/probe-url?url=${encodeURIComponent(url)}`
2397
3183
  );
2398
3184
  }
2399
- listDir(sessionId, dirPath) {
2400
- 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}`);
2401
3188
  }
2402
3189
  async countFiles(sessionId, dirPath = "") {
2403
3190
  const entries = await this.listDir(sessionId, dirPath);
@@ -2592,16 +3379,140 @@ function profileToCreateSessionRequest(profile) {
2592
3379
  };
2593
3380
  }
2594
3381
 
2595
- // src/commands/registry.ts
2596
- var BUFFER_LIMIT_PER_ACTION = 20;
2597
- var CommandRegistry = class {
2598
- handlers = /* @__PURE__ */ new Map();
2599
- buffered = /* @__PURE__ */ new Map();
2600
- onCommand(action, handler) {
2601
- let set = this.handlers.get(action);
2602
- if (!set) {
2603
- set = /* @__PURE__ */ new Set();
2604
- this.handlers.set(action, set);
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
+
3506
+ // src/commands/registry.ts
3507
+ var BUFFER_LIMIT_PER_ACTION = 20;
3508
+ var CommandRegistry = class {
3509
+ handlers = /* @__PURE__ */ new Map();
3510
+ buffered = /* @__PURE__ */ new Map();
3511
+ onCommand(action, handler) {
3512
+ let set = this.handlers.get(action);
3513
+ if (!set) {
3514
+ set = /* @__PURE__ */ new Set();
3515
+ this.handlers.set(action, set);
2605
3516
  }
2606
3517
  set.add(handler);
2607
3518
  const pending = this.buffered.get(action);
@@ -2700,6 +3611,172 @@ var Emitter = class {
2700
3611
  }
2701
3612
  };
2702
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
+
2703
3780
  // src/schemas/message-utils.ts
2704
3781
  var SYSTEM_REMINDER_PATTERN = /^<system-reminder>\s*[\s\S]*?\s*<\/system-reminder>$/i;
2705
3782
  var SYSTEM_NOTIFICATION_PATTERN = /^<system-notification>\s*[\s\S]*?\s*<\/system-notification>$/i;
@@ -2708,6 +3785,36 @@ var LEGACY_FORK_CONTEXT_MARKERS = [
2708
3785
  "\u53EF\u901A\u8FC7 `read_file` \u8BFB\u53D6\u3002",
2709
3786
  "# \u4F60\u7684\u4EFB\u52A1"
2710
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
+ }
2711
3818
  function normalizeMessageContent(content) {
2712
3819
  if (typeof content === "string") return content;
2713
3820
  if (Array.isArray(content)) return content;
@@ -2727,10 +3834,10 @@ function isLegacyForkContextText(text) {
2727
3834
  const normalized = text.trim();
2728
3835
  return LEGACY_FORK_CONTEXT_MARKERS.every((marker) => normalized.includes(marker));
2729
3836
  }
2730
- function isHiddenInternalMessage(message) {
3837
+ function isHiddenInternalMessage(message, options = {}) {
2731
3838
  if (message.role !== "user") return false;
2732
3839
  const text = getTextContent(normalizeMessageContent(message.content));
2733
- return isSystemReminderText(text) || isSystemNotificationText(text) || isLegacyForkContextText(text);
3840
+ return isSystemReminderText(text) || isSystemNotificationText(text) || (options.includeLegacyForkContext ?? true) && isLegacyForkContextText(text);
2734
3841
  }
2735
3842
  function buildMessageContent(text, attachments) {
2736
3843
  if (attachments.length === 0) return text;
@@ -2853,6 +3960,12 @@ function groupMessagesByLoop(messages) {
2853
3960
  }
2854
3961
  return groups;
2855
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
+ }
2856
3969
 
2857
3970
  // src/session/state.ts
2858
3971
  function toReplaySnapshot(raw) {
@@ -2877,7 +3990,10 @@ function createInitialSessionState(sessionId) {
2877
3990
  agentLoops: {},
2878
3991
  activeCompaction: null,
2879
3992
  replay: null,
2880
- viewerRole: null
3993
+ queue: EMPTY_SESSION_QUEUE,
3994
+ viewerRole: null,
3995
+ nextBefore: null,
3996
+ loadingOlder: false
2881
3997
  };
2882
3998
  }
2883
3999
  function parseAgentDescription(argumentsJson) {
@@ -2903,10 +4019,10 @@ function inferLoopStatusFromMessages(messages) {
2903
4019
  }
2904
4020
  function inferLoopStatusFromTurns(turns, messages) {
2905
4021
  const latestAgentNotification = [...turns].reverse().flatMap((turn) => turn.blocks).find((block) => {
2906
- if (block.type !== "system_notification" || !isRecord4(block.content)) return false;
4022
+ if (block.type !== "system_notification" || !isRecord(block.content)) return false;
2907
4023
  return block.content.notification_type === "agent:start" || block.content.notification_type === "agent:end";
2908
4024
  });
2909
- if (latestAgentNotification?.type === "system_notification" && isRecord4(latestAgentNotification.content)) {
4025
+ if (latestAgentNotification?.type === "system_notification" && isRecord(latestAgentNotification.content)) {
2910
4026
  const notificationType = latestAgentNotification.content.notification_type;
2911
4027
  const status = latestAgentNotification.content.status;
2912
4028
  if (notificationType === "agent:start" || status === "running") return "running";
@@ -2915,17 +4031,44 @@ function inferLoopStatusFromTurns(turns, messages) {
2915
4031
  }
2916
4032
  return inferLoopStatusFromMessages(messages);
2917
4033
  }
2918
- function isRecord4(value) {
2919
- 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;
2920
4063
  }
2921
4064
  function parentForkToolCallIdFromTurn(turn) {
2922
4065
  if (typeof turn.parent_fork_tool_call_id === "string" && turn.parent_fork_tool_call_id.length > 0) {
2923
4066
  return turn.parent_fork_tool_call_id;
2924
4067
  }
2925
4068
  for (const block of turn.blocks) {
2926
- if (block.type !== "system_notification" || !isRecord4(block.content)) continue;
4069
+ if (block.type !== "system_notification" || !isRecord(block.content)) continue;
2927
4070
  const metadata = block.content.metadata;
2928
- if (!isRecord4(metadata)) continue;
4071
+ if (!isRecord(metadata)) continue;
2929
4072
  const parentId = metadata.parent_fork_tool_call_id;
2930
4073
  if (typeof parentId === "string" && parentId.length > 0) return parentId;
2931
4074
  }
@@ -2942,16 +4085,16 @@ function buildMessageContent2(turn) {
2942
4085
  }
2943
4086
  function workspaceNotificationContent(turn) {
2944
4087
  const block = turn.blocks.find(
2945
- (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"
2946
4089
  );
2947
- if (!block || !isRecord4(block.content)) return "";
4090
+ if (!block || !isRecord(block.content)) return "";
2948
4091
  const title = typeof block.content.title === "string" ? block.content.title.trim() : "";
2949
4092
  const detail = typeof block.content.detail === "string" ? block.content.detail.trim() : "";
2950
4093
  return [title ? `**${title}**` : "", detail].filter(Boolean).join("\n\n");
2951
4094
  }
2952
4095
  function askUserAnswerContent(turn) {
2953
4096
  const answerBlock = turn.blocks.find((block) => block.type === "ask_user_answer");
2954
- if (!answerBlock || !isRecord4(answerBlock.content)) return null;
4097
+ if (!answerBlock || !isRecord(answerBlock.content)) return null;
2955
4098
  const answer = answerBlock.content.answer;
2956
4099
  return typeof answer === "string" && answer.trim().length > 0 ? answer : null;
2957
4100
  }
@@ -2971,6 +4114,19 @@ function extractModeFromBlocks(blocks) {
2971
4114
  return null;
2972
4115
  }
2973
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
+ }
2974
4130
  if (turn.kind === "compaction" && turn.compaction_id) {
2975
4131
  return {
2976
4132
  role: "assistant",
@@ -3141,21 +4297,58 @@ function orderTurns(turns) {
3141
4297
  return turns;
3142
4298
  }
3143
4299
  function reconcileOptimisticUserTurns(currentTurns, incomingTurns) {
3144
- const currentById = new Map(currentTurns.map((turn) => [turn.turn_id, turn]));
3145
- const optimisticUserTurn = currentTurns.find(
3146
- (turn) => turn.role === "user" && turn.turn_id.startsWith("local-user-")
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)
3147
4338
  );
3148
- let optimisticUserMatched = false;
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]));
3149
4346
  return incomingTurns.map((turn) => {
3150
4347
  const current = currentById.get(turn.turn_id);
3151
4348
  if (current) {
3152
4349
  const currentRenderId = current.client_render_id;
3153
4350
  return turn.client_render_id || !currentRenderId ? turn : { ...turn, client_render_id: currentRenderId };
3154
4351
  }
3155
- if (!optimisticUserMatched && optimisticUserTurn && turn.role === "user" && !turn.turn_id.startsWith("local-user-")) {
3156
- optimisticUserMatched = true;
3157
- return { ...turn, client_render_id: optimisticUserTurn.turn_id };
3158
- }
3159
4352
  return turn;
3160
4353
  });
3161
4354
  }
@@ -3170,7 +4363,7 @@ function areAgentLoopsEqual(left, right) {
3170
4363
  });
3171
4364
  }
3172
4365
  function withTurns(state, turns) {
3173
- const orderedTurns = orderTurns(reconcileOptimisticUserTurns(state.turns, turns));
4366
+ const orderedTurns = orderTurns(reconcileHistoricalUserTurns(state.turns, turns));
3174
4367
  const { messages, agentLoops, activeCompaction } = materialize(orderedTurns);
3175
4368
  const isWaitingForInput = orderedTurns.some(
3176
4369
  (turn) => turn.tool_calls.some((toolCall) => toolCall.status === "awaiting_answer")
@@ -3188,26 +4381,45 @@ function withTurns(state, turns) {
3188
4381
  });
3189
4382
  const mergedMessages = preservedErrors.length > 0 ? [...messagesWithRenderIds, ...preservedErrors] : messagesWithRenderIds;
3190
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
+ }
3191
4394
  return {
3192
4395
  ...state,
3193
4396
  turns: orderedTurns,
3194
4397
  messages: mergedMessages,
3195
4398
  agentLoops: stableAgentLoops,
3196
4399
  activeCompaction,
4400
+ askAnswers,
3197
4401
  mode: latestMode ?? state.mode,
3198
4402
  // 历史恢复时 chat:end(paused) 可能已不在 socket replay 缓存中;
3199
4403
  // awaiting_answer 是投影里的持久权威状态,必须据此恢复问答交互。
3200
4404
  status: isWaitingForInput ? "waiting_for_input" : state.status
3201
4405
  };
3202
4406
  }
4407
+ function withQueue(state, snapshot2) {
4408
+ if (!shouldApplySnapshot(state.queue, snapshot2)) return state;
4409
+ return { ...state, queue: snapshot2 };
4410
+ }
3203
4411
  var localTurnCounter = 0;
3204
- function addUserMessage(state, content) {
4412
+ function addUserMessage(state, content, clientRequestId) {
3205
4413
  localTurnCounter += 1;
3206
- const turnId = `local-user-${Date.now()}-${localTurnCounter}`;
4414
+ const turnId = `local-user-${clientRequestId ?? `${Date.now()}-${localTurnCounter}`}`;
3207
4415
  const optimisticTurn = {
3208
4416
  id: turnId,
3209
4417
  sequence: Math.max(0, ...state.turns.map((turn) => turn.sequence)) + 1,
3210
4418
  turn_id: turnId,
4419
+ // 乐观态就自赋,跟服务端确认后 reconcileUserTurns 补上的
4420
+ // client_render_id(= 乐观 turn 自己的 turn_id)保持同一个值,
4421
+ // 消费方才能拿它当"确认前后不变"的稳定标识使用。
4422
+ client_render_id: turnId,
3211
4423
  loop_id: "root",
3212
4424
  role: "user",
3213
4425
  status: "streaming",
@@ -3222,10 +4434,9 @@ function addUserMessage(state, content) {
3222
4434
  function upsertTurn(state, turn) {
3223
4435
  let existing = [...state.turns];
3224
4436
  const [nextTurn] = reconcileOptimisticUserTurns(existing, [turn]);
3225
- if (nextTurn.client_render_id && turn.role === "user") {
3226
- existing = existing.filter(
3227
- (item) => !(item.role === "user" && item.turn_id.startsWith("local-user-"))
3228
- );
4437
+ const claimedOptimisticId = nextTurn.client_render_id;
4438
+ if (claimedOptimisticId?.startsWith("local-user-")) {
4439
+ existing = existing.filter((item) => item.turn_id !== claimedOptimisticId);
3229
4440
  }
3230
4441
  const index = existing.findIndex((item) => item.turn_id === nextTurn.turn_id);
3231
4442
  if (index >= 0) {
@@ -3233,7 +4444,7 @@ function upsertTurn(state, turn) {
3233
4444
  } else {
3234
4445
  existing.push(nextTurn);
3235
4446
  }
3236
- return withTurns(state, existing);
4447
+ return withTurns({ ...state, turns: existing }, existing);
3237
4448
  }
3238
4449
  function applyTurnPatch(state, patch) {
3239
4450
  const turn = patch.data.turn;
@@ -3244,13 +4455,7 @@ function applyTurnPatch(state, patch) {
3244
4455
  if (lastSequence !== null && patch.sequence <= lastSequence) {
3245
4456
  return state;
3246
4457
  }
3247
- const nextTurn = { ...turn, sequence: patch.sequence };
3248
- if (index >= 0) {
3249
- existing[index] = nextTurn;
3250
- } else {
3251
- existing.push(nextTurn);
3252
- }
3253
- return withTurns(state, existing);
4458
+ return upsertTurn(state, { ...turn, sequence: patch.sequence });
3254
4459
  }
3255
4460
  function addErrorMessage(state, content) {
3256
4461
  const anchorTurnId = state.turns[state.turns.length - 1]?.turn_id ?? null;
@@ -3280,21 +4485,49 @@ function markStreamingTurns(state, turnStatus, toolCallStatus) {
3280
4485
  return withTurns(state, turns);
3281
4486
  }
3282
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
+
3283
4498
  // src/session/agent-session.ts
3284
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";
3285
- var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
3286
- function isRecord5(value) {
3287
- return typeof value === "object" && value !== null && !Array.isArray(value);
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}`;
3288
4510
  }
4511
+ var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
3289
4512
  function isOomText(value) {
3290
4513
  return typeof value === "string" && OOM_KEYWORDS_RE.test(value);
3291
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
+ }
3292
4525
  function parseJsonRecord(value) {
3293
- if (isRecord5(value)) return value;
4526
+ if (isRecord(value)) return value;
3294
4527
  if (typeof value !== "string") return null;
3295
4528
  try {
3296
4529
  const parsed = JSON.parse(value);
3297
- return isRecord5(parsed) ? parsed : null;
4530
+ return isRecord(parsed) ? parsed : null;
3298
4531
  } catch {
3299
4532
  return null;
3300
4533
  }
@@ -3320,6 +4553,21 @@ function isOomResult(value) {
3320
4553
  function isToolBridgeContent(value) {
3321
4554
  return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.action === "string";
3322
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
+ }
3323
4571
  function sessionStatusFromChatEnd(status) {
3324
4572
  if (status === "paused") return "waiting_for_input";
3325
4573
  if (status === "interrupted") return "interrupted";
@@ -3335,14 +4583,6 @@ function extractTerminalChatEndStatus(events) {
3335
4583
  }
3336
4584
  return null;
3337
4585
  }
3338
- function hasChatRunEvent2(events) {
3339
- return events.some(
3340
- (event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
3341
- );
3342
- }
3343
- function isUiMetaLike(value) {
3344
- return isRecord5(value) && ("resourceHTML" in value || "resourceUri" in value || "resourceURI" in value);
3345
- }
3346
4586
  var AgentSession = class _AgentSession {
3347
4587
  sessionId;
3348
4588
  state;
@@ -3356,14 +4596,55 @@ var AgentSession = class _AgentSession {
3356
4596
  patchFlushCancel = null;
3357
4597
  joined = false;
3358
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;
3359
4616
  // 回放控制请求排成一条队列。连着点"5x"再点"退出回放"时,并发发出去的两个
3360
4617
  // 请求响应可能乱序回来,晚到的旧值会把已经退出的会话又显示成回放中;用
3361
4618
  // 序号丢弃过期响应又会在失败时留下中间态。串行执行就没有这些情况:每个
3362
4619
  // 响应都是当时最新的,失败也只影响自己。
3363
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;
3364
4629
  pendingReplayMessage = null;
3365
4630
  pendingReplayMode;
4631
+ pendingReplayKbIds;
3366
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;
3367
4648
  disposed = false;
3368
4649
  getAppContext = null;
3369
4650
  sendPending = false;
@@ -3374,11 +4655,19 @@ var AgentSession = class _AgentSession {
3374
4655
  seenToolCallIds = /* @__PURE__ */ new Set();
3375
4656
  finishedToolCallIds = /* @__PURE__ */ new Set();
3376
4657
  emittedMessageTurnIds = /* @__PURE__ */ new Set();
3377
- constructor(sessionId, runtime, connection) {
4658
+ constructor(sessionId, runtime, connection, options = {}) {
3378
4659
  this.sessionId = sessionId;
3379
4660
  this.runtime = runtime;
3380
4661
  this.state = { ...createInitialSessionState(sessionId), connection };
4662
+ this.isStatic = options.staticHistory === true;
3381
4663
  }
4664
+ /**
4665
+ * 静态历史(分享页 / `fromTurns`):没有目录接口,也**没有**"目录还没到位"这个中间态。
4666
+ *
4667
+ * 宿主据此决定要不要拿已加载窗口当兜底条目——真实会话在 timeline 被重写后,窗口里还是
4668
+ * 旧分支的发言,拿它兜底会把已经失效的 entry id 重新摆到轨道上,点了必然找不到。
4669
+ */
4670
+ isStatic;
3382
4671
  /** 从一份静态 turn 快照构造只展示历史的会话(例如公开分享页)。 */
3383
4672
  static fromTurns(sessionId, turns) {
3384
4673
  const readonlyError = () => {
@@ -3387,11 +4676,13 @@ var AgentSession = class _AgentSession {
3387
4676
  const session = new _AgentSession(
3388
4677
  sessionId,
3389
4678
  {
3390
- streamTokens: false,
3391
4679
  emitEvent: readonlyError,
3392
4680
  emitWithAck: async () => readonlyError(),
3393
4681
  ensureConnected: readonlyError,
3394
- fetchTurns: async () => turns,
4682
+ fetchTurnsPage: async () => ({ turns, nextBefore: null, currentMode: "executing" }),
4683
+ // 静态会话(分享页)没有会话目录接口,宿主用已加载的历史兜底条目。
4684
+ fetchTurnIndex: async () => ({ sessionId, items: [] }),
4685
+ fetchCommands: async () => [],
3395
4686
  // 静态历史没有回放身份可言,给个空详情即可,不要抛错——_bootstrap
3396
4687
  // 不会跑到这里,但保持只读语义一致。
3397
4688
  fetchSessionInfo: async () => ({ replay_state: null }),
@@ -3402,7 +4693,8 @@ var AgentSession = class _AgentSession {
3402
4693
  attachAppCli: async () => readonlyError(),
3403
4694
  onDispose: () => true
3404
4695
  },
3405
- "connected"
4696
+ "connected",
4697
+ { staticHistory: true }
3406
4698
  );
3407
4699
  session.state = {
3408
4700
  ...withTurns(session.state, turns),
@@ -3415,6 +4707,101 @@ var AgentSession = class _AgentSession {
3415
4707
  getState() {
3416
4708
  return this.state;
3417
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
+ }
3418
4805
  /** 订阅状态变化。返回取消订阅函数。 */
3419
4806
  subscribe(listener) {
3420
4807
  this.listeners.add(listener);
@@ -3467,7 +4854,7 @@ var AgentSession = class _AgentSession {
3467
4854
  /** 发送一条消息。字符串或多模态内容均可。 */
3468
4855
  async send(content, options = {}) {
3469
4856
  this.runtime.ensureConnected();
3470
- 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) {
3471
4858
  this.update(
3472
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")
3473
4860
  );
@@ -3480,9 +4867,125 @@ var AgentSession = class _AgentSession {
3480
4867
  this.sendPending = false;
3481
4868
  }
3482
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
+ }
3483
4986
  async sendUnlocked(content, options) {
3484
4987
  await this.ensureIdleBeforeSend();
3485
- if (this.state.isStreaming || this.state.activeCompaction || this.manualCompactionPending) {
4988
+ if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.manualCompactionPending) {
3486
4989
  this.update(
3487
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")
3488
4991
  );
@@ -3492,7 +4995,7 @@ var AgentSession = class _AgentSession {
3492
4995
  if (this.getAppContext) {
3493
4996
  try {
3494
4997
  const rawContext = await this.getAppContext();
3495
- if (!isRecord5(rawContext)) {
4998
+ if (!isRecord(rawContext)) {
3496
4999
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
3497
5000
  }
3498
5001
  const serialized = JSON.stringify(rawContext);
@@ -3500,7 +5003,7 @@ var AgentSession = class _AgentSession {
3500
5003
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE\u53EF\u5E8F\u5217\u5316\u7684 JSON \u5BF9\u8C61");
3501
5004
  }
3502
5005
  const normalized = JSON.parse(serialized);
3503
- if (!isRecord5(normalized)) {
5006
+ if (!isRecord(normalized)) {
3504
5007
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
3505
5008
  }
3506
5009
  appContext = normalized;
@@ -3510,17 +5013,18 @@ var AgentSession = class _AgentSession {
3510
5013
  return false;
3511
5014
  }
3512
5015
  }
5016
+ const wasStreaming = this.state.isStreaming;
3513
5017
  const askUserAnswer = options.askUserAnswer;
3514
5018
  let optimisticTurnId = null;
5019
+ const clientRequestId = createClientRequestId();
3515
5020
  let optimisticAskAnswer;
3516
5021
  let previousAskAnswer;
3517
- if (!(askUserAnswer && typeof askUserAnswer.tool_call_id === "string")) {
3518
- this.update((state) => {
3519
- const next = addUserMessage(state, content);
3520
- optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
3521
- return next;
3522
- });
3523
- }
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);
3524
5028
  this.update((s) => ({ ...s, isStreaming: true }));
3525
5029
  if (askUserAnswer && typeof askUserAnswer.tool_call_id === "string") {
3526
5030
  const { tool_call_id, ...rest } = askUserAnswer;
@@ -3534,57 +5038,103 @@ var AgentSession = class _AgentSession {
3534
5038
  if (!options.replayDecision) {
3535
5039
  this.pendingReplayMessage = content;
3536
5040
  this.pendingReplayMode = options.mode;
5041
+ this.pendingReplayKbIds = options.kbIds;
3537
5042
  }
3538
5043
  const payload = {
3539
5044
  session_id: this.sessionId,
3540
5045
  message: content,
5046
+ client_request_id: clientRequestId,
3541
5047
  mode: options.mode,
3542
5048
  askuser_answer: askUserAnswer,
3543
5049
  model: options.model,
3544
5050
  thinking_override: options.thinkingOverride,
3545
5051
  whatif: options.whatif,
3546
5052
  replay_decision: options.replayDecision,
3547
- app_context: appContext
5053
+ app_context: appContext,
5054
+ kb_ids: options.kbIds,
5055
+ // 运行中提交不必由客户端挑命令:服务端按会话运行状态决定直接执行还是入队。
5056
+ queue_when_running: options.queueWhenRunning === true
3548
5057
  };
3549
5058
  try {
3550
5059
  await this.ensureJoined();
3551
5060
  } catch (error) {
5061
+ const failure = describeSessionError(error, "\u4F1A\u8BDD\u8BA2\u9605\u5931\u8D25\uFF0C\u6D88\u606F\u672A\u53D1\u9001");
3552
5062
  this.rollbackOptimisticSend(
3553
5063
  optimisticTurnId,
3554
5064
  askUserAnswer,
3555
5065
  optimisticAskAnswer,
3556
5066
  previousAskAnswer,
3557
- 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
3558
5070
  );
3559
5071
  return false;
3560
5072
  }
5073
+ const waitStartedAt = this.systemErrorGeneration;
3561
5074
  try {
3562
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
+ }
3563
5091
  if (response?.status !== "accepted") {
3564
- throw new Error(
3565
- 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
3566
5095
  );
3567
5096
  }
3568
5097
  return true;
3569
5098
  } catch (error) {
5099
+ const failure = describeSessionError(error, "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
3570
5100
  this.rollbackOptimisticSend(
3571
5101
  optimisticTurnId,
3572
5102
  askUserAnswer,
3573
5103
  optimisticAskAnswer,
3574
5104
  previousAskAnswer,
3575
- 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
3576
5109
  );
3577
5110
  return false;
3578
5111
  }
3579
5112
  }
3580
- 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) {
3581
5132
  this.pendingReplayMessage = null;
3582
5133
  this.pendingReplayMode = void 0;
5134
+ this.pendingReplayKbIds = void 0;
3583
5135
  this.update((current) => {
3584
- let next = optimisticTurnId ? withTurns(
3585
- current,
3586
- current.turns.filter((turn) => turn.turn_id !== optimisticTurnId)
3587
- ) : 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;
3588
5138
  const toolCallId = askUserAnswer && typeof askUserAnswer.tool_call_id === "string" ? askUserAnswer.tool_call_id : null;
3589
5139
  if (toolCallId && next.askAnswers[toolCallId] === optimisticAskAnswer) {
3590
5140
  const askAnswers = { ...next.askAnswers };
@@ -3595,18 +5145,20 @@ var AgentSession = class _AgentSession {
3595
5145
  }
3596
5146
  next = { ...next, askAnswers };
3597
5147
  }
3598
- return addErrorMessage({ ...next, isStreaming: false }, message);
5148
+ return next;
3599
5149
  });
3600
- }
3601
- /** 智能体运行期间追加补充说明(不打断当前回复)。 */
3602
- append(text) {
3603
- this.runtime.ensureConnected();
3604
- this.runtime.emitEvent("chat:append", { session_id: this.sessionId, message: text });
3605
- this.update((s) => addUserMessage(s, text));
5150
+ if (optimisticTurnId) {
5151
+ this.pendingOptimisticTurnIds.delete(optimisticTurnId);
5152
+ this.liveRevisions.delete(optimisticTurnId);
5153
+ }
3606
5154
  }
3607
5155
  /** 停止当前回复。 */
3608
5156
  stop() {
3609
- 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
+ });
3610
5162
  }
3611
5163
  /** 手动触发上下文压缩。 */
3612
5164
  compact() {
@@ -3626,8 +5178,10 @@ var AgentSession = class _AgentSession {
3626
5178
  trigger: "manual"
3627
5179
  }
3628
5180
  }));
3629
- 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) => response ?? {}).catch((error) => {
3630
5182
  this.manualCompactionPending = false;
5183
+ throw error;
5184
+ }).finally(() => {
3631
5185
  if (this.state.activeCompaction?.compaction_id === "pending-manual-compaction") {
3632
5186
  this.update((state) => ({ ...state, activeCompaction: null }));
3633
5187
  }
@@ -3666,10 +5220,20 @@ var AgentSession = class _AgentSession {
3666
5220
  if (this.disposed) return;
3667
5221
  if (!this.runtime.onDispose(this)) return;
3668
5222
  this.disposed = true;
5223
+ this.invalidateTimeline();
5224
+ this.invalidateOlderPaging();
5225
+ this.state = { ...this.state, nextBefore: null, loadingOlder: false };
3669
5226
  this.cancelPatchFlush();
3670
5227
  this.listeners.clear();
3671
5228
  this.emitter.clear();
3672
5229
  this.commands.clear();
5230
+ this.commandRecoveryGeneration += 1;
5231
+ this.commandRestoreRetryAttempt = 0;
5232
+ this.bufferedCommandDeliveries = null;
5233
+ if (this.commandRestoreRetryHandle !== null) {
5234
+ globalThis.clearTimeout(this.commandRestoreRetryHandle);
5235
+ this.commandRestoreRetryHandle = null;
5236
+ }
3673
5237
  }
3674
5238
  get isDisposed() {
3675
5239
  return this.disposed;
@@ -3680,28 +5244,64 @@ var AgentSession = class _AgentSession {
3680
5244
  if (this.state.connection === connection) return;
3681
5245
  this.update((s) => ({ ...s, connection }));
3682
5246
  }
5247
+ /**
5248
+ * @internal 服务端广播的权威队列快照。
5249
+ * 结构不合法或 revision 不新的一律丢弃,绝不用旧快照覆盖新状态。
5250
+ */
5251
+ _handleQueueUpdated(payload) {
5252
+ if (!isSessionQueueSnapshot(payload)) return;
5253
+ const snapshot2 = payload;
5254
+ this.update((state) => withQueue(state, snapshot2));
5255
+ }
3683
5256
  /** @internal socket 重连后由 Hub 调用:重新加入房间并补数。 */
3684
- async _rejoinAfterReconnect() {
3685
- const turnsBeforeRejoin = this.state.turns;
3686
- this.joined = false;
3687
- this.joinPending = null;
3688
- try {
3689
- await this.ensureJoined();
3690
- await Promise.all([
3691
- this.syncTurnsFromHistory({
3692
- clearOnEmpty: true,
3693
- preserveChangesSince: turnsBeforeRejoin
3694
- }),
3695
- this.syncReplayFromSession()
3696
- ]);
3697
- } catch {
3698
- }
5257
+ _rejoinAfterReconnect() {
5258
+ this.invalidateOlderPaging();
5259
+ this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
5260
+ const reconnectLiveRevision = this.liveRevision;
5261
+ const reconnectModeRevision = this.modeRevision;
5262
+ const commandRecoveryGeneration = this.beginCommandRecovery();
5263
+ return this.enqueueSubscription(async () => {
5264
+ this.joined = false;
5265
+ this.joinPending = null;
5266
+ try {
5267
+ this.subscriptionReplaySource = "reconnect_replay";
5268
+ try {
5269
+ await this.ensureJoined(reconnectLiveRevision);
5270
+ } finally {
5271
+ this.subscriptionReplaySource = null;
5272
+ this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
5273
+ }
5274
+ await Promise.all([
5275
+ this.refreshAuthoritativeHistory({
5276
+ liveRevisionAtStart: reconnectLiveRevision,
5277
+ modeRevisionAtStart: reconnectModeRevision
5278
+ }),
5279
+ this.syncReplayFromSession()
5280
+ ]);
5281
+ } catch {
5282
+ }
5283
+ });
3699
5284
  }
3700
5285
  /** @internal 首次连接:加载历史 + 加入房间。 */
3701
- async _bootstrap() {
3702
- await this.syncTurnsFromHistory({ suppressErrors: false });
3703
- await this.ensureJoined();
3704
- await this.syncReplayFromSession();
5286
+ _bootstrap() {
5287
+ const commandRecoveryGeneration = this.beginCommandRecovery();
5288
+ return this.enqueueSubscription(async () => {
5289
+ await this.replaceFromHistory({ suppressErrors: false });
5290
+ this.subscriptionReplaySource = "bootstrap_replay";
5291
+ try {
5292
+ await this.ensureJoined();
5293
+ } finally {
5294
+ this.subscriptionReplaySource = null;
5295
+ }
5296
+ this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
5297
+ await Promise.all([this.syncReplayFromSession(), this.refreshQueue()]);
5298
+ });
5299
+ }
5300
+ /** 订阅生命周期串行执行;一次失败只由自己的调用方观察,不截断后续重连。 */
5301
+ enqueueSubscription(operation) {
5302
+ const run = this.subscriptionQueue.then(operation);
5303
+ this.subscriptionQueue = run.catch(() => void 0);
5304
+ return run;
3705
5305
  }
3706
5306
  /** 改回放是 owner 专属(服务端 PATCH 走 _get_owned_session)。 */
3707
5307
  get canControlReplay() {
@@ -3758,10 +5358,16 @@ var AgentSession = class _AgentSession {
3758
5358
  this.applyTurn(turn);
3759
5359
  }
3760
5360
  /** @internal 历史回放通道(event_format: raw)。 */
3761
- _handleTurnEvents(events) {
5361
+ _handleTurnEvents(events, replayed = false) {
3762
5362
  if (!events.length) return;
5363
+ if (events.some(
5364
+ (event) => event.type === "compaction:end" || event.type === "compaction:error" || event.type === "compaction:abort"
5365
+ )) {
5366
+ this.manualCompactionPending = false;
5367
+ }
5368
+ this.dispatchRawCommands(events);
3763
5369
  const terminalChatEndStatus = extractTerminalChatEndStatus(events);
3764
- if (!terminalChatEndStatus && hasChatRunEvent2(events)) {
5370
+ if (!terminalChatEndStatus && hasChatRunEvent(events) && this.state.status !== "waiting_for_input") {
3765
5371
  this.markRunningIfIdle();
3766
5372
  }
3767
5373
  if (this.state.turns.length) {
@@ -3770,7 +5376,10 @@ var AgentSession = class _AgentSession {
3770
5376
  const updates = this.projectionBuilder.processBatch(events);
3771
5377
  for (const update of updates) {
3772
5378
  if (update.kind === "upsert") {
3773
- this.applyTurn(update.turn);
5379
+ this.applyTurn(
5380
+ update.turn,
5381
+ replayed ? this.subscriptionReplaySource ?? "bootstrap_replay" : "live"
5382
+ );
3774
5383
  } else if (update.kind === "workspace_changed") {
3775
5384
  this.emitter.emit("workspaceChanged", {});
3776
5385
  }
@@ -3781,7 +5390,12 @@ var AgentSession = class _AgentSession {
3781
5390
  }
3782
5391
  /** @internal */
3783
5392
  _handleChatStart() {
3784
- this.update((s) => ({ ...s, status: "running", errorMessage: null, isStreaming: true }));
5393
+ this.update((s) => ({
5394
+ ...s,
5395
+ status: "running",
5396
+ errorMessage: null,
5397
+ isStreaming: true
5398
+ }));
3785
5399
  }
3786
5400
  /** @internal */
3787
5401
  _handleChatEnd(status) {
@@ -3789,14 +5403,31 @@ var AgentSession = class _AgentSession {
3789
5403
  this.completeChat(status);
3790
5404
  }
3791
5405
  /** @internal */
3792
- _handleSystemError(message) {
5406
+ _handleSystemError(message, code) {
3793
5407
  this.flushPatches();
3794
5408
  const finalMessage = isOomText(message) ? OOM_MESSAGE : message;
3795
5409
  this.update(
3796
- (s) => addErrorMessage(markFailed({ ...s, status: "failed", errorMessage: finalMessage, isStreaming: false }), finalMessage)
5410
+ (s) => addErrorMessage(
5411
+ markFailed({
5412
+ ...s,
5413
+ status: "failed",
5414
+ errorMessage: finalMessage,
5415
+ isStreaming: false
5416
+ }),
5417
+ finalMessage
5418
+ )
3797
5419
  );
3798
5420
  this.recentChatEndAt = Date.now();
3799
- this.emitter.emit("error", { message: finalMessage });
5421
+ this.systemErrorGeneration += 1;
5422
+ this.lastDeliveredSystemError = {
5423
+ message: finalMessage,
5424
+ ...code ? { code } : {},
5425
+ generation: this.systemErrorGeneration
5426
+ };
5427
+ this.emitter.emit("error", {
5428
+ message: finalMessage,
5429
+ ...code ? { code } : {}
5430
+ });
3800
5431
  }
3801
5432
  /** @internal */
3802
5433
  _handleTaskUpdated(tasks) {
@@ -3816,6 +5447,8 @@ var AgentSession = class _AgentSession {
3816
5447
  this.update((s) => ({ ...s, replay }));
3817
5448
  }
3818
5449
  this.emitter.emit("sessionUpdated", {
5450
+ workspacePath: data.workspace_path,
5451
+ chatGroupId: data.chat_group_id,
3819
5452
  intent: data.intent,
3820
5453
  status: data.status,
3821
5454
  model: data.model,
@@ -3825,16 +5458,26 @@ var AgentSession = class _AgentSession {
3825
5458
  }
3826
5459
  /** @internal */
3827
5460
  _handleRewind(checkpointId) {
3828
- this.update((s) => ({ ...s, mode: null, errorMessage: null }));
3829
- void this.syncTurnsFromHistory({ clearOnEmpty: true });
5461
+ this.invalidateTimeline();
5462
+ this.invalidateOlderPaging();
5463
+ this.update((s) => ({
5464
+ ...s,
5465
+ mode: null,
5466
+ errorMessage: null,
5467
+ nextBefore: null,
5468
+ loadingOlder: false
5469
+ }));
5470
+ void this.replaceFromHistory();
3830
5471
  this.emitter.emit("rewind", { checkpointId });
3831
5472
  }
3832
5473
  /** @internal */
3833
5474
  _handleReplayMismatch(data) {
3834
5475
  const message = this.pendingReplayMessage ?? data.actual_message ?? "";
3835
5476
  const mode = this.pendingReplayMode;
5477
+ const kbIds = this.pendingReplayKbIds;
3836
5478
  this.pendingReplayMessage = null;
3837
5479
  this.pendingReplayMode = void 0;
5480
+ this.pendingReplayKbIds = void 0;
3838
5481
  const respond = (decision) => {
3839
5482
  void this.runtime.emitWithAck(
3840
5483
  "chat:send",
@@ -3842,7 +5485,8 @@ var AgentSession = class _AgentSession {
3842
5485
  session_id: this.sessionId,
3843
5486
  message,
3844
5487
  mode,
3845
- replay_decision: decision
5488
+ replay_decision: decision,
5489
+ kb_ids: kbIds
3846
5490
  },
3847
5491
  3e4
3848
5492
  ).then((response) => {
@@ -3883,9 +5527,9 @@ var AgentSession = class _AgentSession {
3883
5527
  detail,
3884
5528
  status,
3885
5529
  loopId,
3886
- metadata: isRecord5(notification.metadata) ? notification.metadata : void 0
5530
+ metadata: isRecord(notification.metadata) ? notification.metadata : void 0
3887
5531
  });
3888
- if (notificationType === "bg:started" && isRecord5(notification.metadata)) {
5532
+ if (notificationType === "bg:started" && isRecord(notification.metadata)) {
3889
5533
  const taskId = typeof notification.metadata.task_id === "string" ? notification.metadata.task_id : "";
3890
5534
  if (taskId) {
3891
5535
  this.emitter.emit("backgroundTask", {
@@ -3907,12 +5551,12 @@ var AgentSession = class _AgentSession {
3907
5551
  this.emitter.emit("artifact", event);
3908
5552
  }
3909
5553
  /** @internal 加入房间的应答里带 is_running,据此校准流式状态。 */
3910
- _applyRunningState(isRunning) {
5554
+ _applyRunningState(isRunning, liveRevisionAtJoin = this.liveRevision) {
3911
5555
  if (isRunning) {
3912
5556
  this.update((s) => ({ ...s, status: "running", isStreaming: true }));
3913
5557
  } else if (this.state.isStreaming) {
3914
5558
  this.update((s) => ({ ...s, isStreaming: false }));
3915
- void this.syncTurnsFromHistory();
5559
+ void this.refreshAuthoritativeHistory({ liveRevisionAtStart: liveRevisionAtJoin });
3916
5560
  }
3917
5561
  }
3918
5562
  // ---------------------------------------------------------------- 内部实现
@@ -3933,14 +5577,36 @@ var AgentSession = class _AgentSession {
3933
5577
  completeChat(status) {
3934
5578
  this.finishStreaming(status);
3935
5579
  this.recentChatEndAt = Date.now();
3936
- void this.syncTurnsFromHistory();
5580
+ void this.refreshAuthoritativeHistory();
3937
5581
  this.emitter.emit("chatEnd", { status });
3938
5582
  }
3939
- applyTurn(turn) {
5583
+ markTurnFresh(turnId) {
5584
+ this.liveRevision += 1;
5585
+ this.liveRevisions.set(turnId, this.liveRevision);
5586
+ }
5587
+ markOptimisticTurnPending(turnId) {
5588
+ this.pendingOptimisticTurnIds.add(turnId);
5589
+ this.markTurnFresh(turnId);
5590
+ }
5591
+ pruneOptimisticLiveRevisions() {
5592
+ const retained = new Set(
5593
+ this.state.turns.map((turn) => turn.turn_id).filter((turnId) => turnId.startsWith("local-user-"))
5594
+ );
5595
+ for (const turnId of this.liveRevisions.keys()) {
5596
+ if (turnId.startsWith("local-user-") && !retained.has(turnId)) {
5597
+ this.pendingOptimisticTurnIds.delete(turnId);
5598
+ this.liveRevisions.delete(turnId);
5599
+ }
5600
+ }
5601
+ }
5602
+ applyTurn(turn, source = "live") {
3940
5603
  this.flushPatches();
5604
+ this.markTurnFresh(turn.turn_id);
5605
+ if (extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
3941
5606
  const previous = this.state;
3942
5607
  this.update((s) => upsertTurn(s, turn));
3943
- this.emitTurnDerivedEvents(previous, turn);
5608
+ this.pruneOptimisticLiveRevisions();
5609
+ this.emitTurnDerivedEvents(previous, turn, source);
3944
5610
  }
3945
5611
  schedulePatchFlush() {
3946
5612
  if (this.patchFlushHandle != null) return;
@@ -3973,11 +5639,15 @@ var AgentSession = class _AgentSession {
3973
5639
  this.pendingTurnPatches.clear();
3974
5640
  for (const patch of patches) {
3975
5641
  const previous = this.state;
3976
- this.update((s) => applyTurnPatch(s, patch));
5642
+ const next = applyTurnPatch(previous, patch);
5643
+ if (next === previous) continue;
5644
+ this.liveRevision += 1;
5645
+ this.liveRevisions.set(patch.turn_id, this.liveRevision);
3977
5646
  const turn = patch.data.turn;
3978
- if (turn) {
3979
- this.emitTurnDerivedEvents(previous, turn);
3980
- }
5647
+ if (turn && extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
5648
+ this.setState(next);
5649
+ this.pruneOptimisticLiveRevisions();
5650
+ if (turn) this.emitTurnDerivedEvents(previous, turn);
3981
5651
  }
3982
5652
  if (this.patchFlushCancel) {
3983
5653
  this.patchFlushCancel();
@@ -3986,7 +5656,7 @@ var AgentSession = class _AgentSession {
3986
5656
  }
3987
5657
  }
3988
5658
  /** turn 数据落地后统一派生语义事件(指令 / 工具调用 / 消息 / 模式 / 预览 / OOM)。 */
3989
- emitTurnDerivedEvents(previous, turn) {
5659
+ emitTurnDerivedEvents(previous, turn, source = "live") {
3990
5660
  const mode = extractModeFromBlocks(turn.blocks);
3991
5661
  if (mode && previous.mode !== mode) {
3992
5662
  this.emitter.emit("modeChange", { mode });
@@ -3999,23 +5669,29 @@ var AgentSession = class _AgentSession {
3999
5669
  this.dispatchCommand(turn, block);
4000
5670
  continue;
4001
5671
  }
4002
- if (block.type === "tool_ui" && block.tool_call_id && isUiMetaLike(block.content)) {
5672
+ if (block.type === "tool_ui" && block.tool_call_id && isToolUiCard(block.content)) {
4003
5673
  const ui = block.content;
4004
5674
  if (ui.target !== "preview") continue;
4005
- const previewType = ui.resourceHTML ? "resource-html" : "resource-uri";
4006
- const previewContent = ui.resourceHTML ?? ui.resourceUri ?? ui.resourceURI;
4007
- if (!previewContent) continue;
4008
- const key = `${block.tool_call_id}:${previewType}`;
4009
- if (this.deliveredToolPreviews.get(key) === previewContent) continue;
4010
- this.deliveredToolPreviews.set(key, previewContent);
5675
+ const resolved = resolveToolUiCardContent(ui);
5676
+ if (!resolved) continue;
5677
+ const key = buildToolUiCardKey(block.tool_call_id, resolved.type, resolved.content, ui.sourcePath);
5678
+ const fingerprint = JSON.stringify([
5679
+ resolved.content,
5680
+ ui.mcpApp?.sourceId,
5681
+ resolved.type === "resource-file" ? block.tool_call_id : void 0
5682
+ ]);
5683
+ if (this.deliveredToolPreviews.get(key) === fingerprint) continue;
5684
+ this.deliveredToolPreviews.set(key, fingerprint);
4011
5685
  this.emitter.emit("toolPreview", {
4012
5686
  toolCallId: block.tool_call_id,
4013
- type: previewType,
4014
- content: previewContent,
4015
- title: ui.title ?? "\u5DE5\u5177\u9884\u89C8"
5687
+ type: resolved.type,
5688
+ content: resolved.content,
5689
+ title: ui.title ?? "\u5DE5\u5177\u9884\u89C8",
5690
+ ...ui.sourcePath ? { sourcePath: ui.sourcePath } : {},
5691
+ ...ui.mcpApp ? { mcpApp: ui.mcpApp } : {}
4016
5692
  });
4017
5693
  }
4018
- if (block.type === "system_notification" && isRecord5(block.content)) {
5694
+ if (block.type === "system_notification" && isRecord(block.content)) {
4019
5695
  this._handleSystemNotification(turn.loop_id, block.content);
4020
5696
  }
4021
5697
  }
@@ -4035,7 +5711,7 @@ var AgentSession = class _AgentSession {
4035
5711
  const finished = toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled";
4036
5712
  if (finished && !this.finishedToolCallIds.has(toolCall.id)) {
4037
5713
  this.finishedToolCallIds.add(toolCall.id);
4038
- this.emitter.emit("toolResult", { toolCall: info, turn });
5714
+ this.emitter.emit("toolResult", { toolCall: info, turn, source });
4039
5715
  if (isOomResult(toolCall.result)) {
4040
5716
  this.notifySandboxOom(toolCall.id);
4041
5717
  }
@@ -4052,17 +5728,36 @@ var AgentSession = class _AgentSession {
4052
5728
  dispatchCommand(turn, block) {
4053
5729
  if (!isToolBridgeContent(block.content)) return;
4054
5730
  const { action, payload } = block.content;
4055
- let payloadKey = "";
4056
- try {
4057
- payloadKey = JSON.stringify(payload ?? null);
4058
- } catch {
4059
- payloadKey = String(payload ?? "");
5731
+ const deliveryKey = block.delivery_id ? commandDeliveryKey(block.delivery_id) : `legacy-tool-command:${turn.turn_id}:${block.tool_call_id ?? ""}:${action}:${JSON.stringify(payload)}`;
5732
+ this.deliverCommand(deliveryKey, action, payload, block.tool_call_id ?? void 0);
5733
+ }
5734
+ dispatchRawCommands(events) {
5735
+ for (const event of events) {
5736
+ if (event.type !== "loop:tool_bridge") continue;
5737
+ const deliveryId = String(event.payload.delivery_id ?? "");
5738
+ const bridge = event.payload.bridge;
5739
+ if (!deliveryId || !isToolBridgeContent(bridge)) continue;
5740
+ this.deliverCommand(
5741
+ commandDeliveryKey(deliveryId),
5742
+ bridge.action,
5743
+ bridge.payload,
5744
+ String(event.payload.tool_call_id ?? "") || void 0
5745
+ );
5746
+ }
5747
+ }
5748
+ deliverCommand(deliveryKey, action, payload, toolCallId) {
5749
+ if (this.bufferedCommandDeliveries) {
5750
+ this.bufferedCommandDeliveries.push({ deliveryKey, action, payload, toolCallId });
5751
+ return;
4060
5752
  }
4061
- const deliveryKey = [block.tool_call_id ?? turn.id, action, payloadKey].join("\0");
5753
+ this.deliverCommandNow({ deliveryKey, action, payload, toolCallId });
5754
+ }
5755
+ deliverCommandNow(command) {
5756
+ const { deliveryKey, action, payload, toolCallId } = command;
4062
5757
  if (this.deliveredCommandKeys.has(deliveryKey)) return;
4063
5758
  this.deliveredCommandKeys.add(deliveryKey);
4064
- const meta = { toolCallId: block.tool_call_id ?? void 0 };
4065
- this.emitter.emit("command", { action, payload, toolCallId: meta.toolCallId });
5759
+ const meta = { toolCallId };
5760
+ this.emitter.emit("command", { action, payload, toolCallId });
4066
5761
  this.commands.dispatch(action, payload, meta);
4067
5762
  }
4068
5763
  notifySandboxOom(key) {
@@ -4071,49 +5766,195 @@ var AgentSession = class _AgentSession {
4071
5766
  this.update((s) => addErrorMessage(s, OOM_MESSAGE));
4072
5767
  this.emitter.emit("sandboxOom", { toolCallId: key });
4073
5768
  }
4074
- async syncTurnsFromHistory(options = {}) {
4075
- const { clearOnEmpty = false, suppressErrors = true, preserveChangesSince } = options;
4076
- const turnsAtRequestStart = this.state.turns;
4077
- try {
4078
- const turns = await this.runtime.fetchTurns(this.sessionId);
4079
- if (turns.length === 0 && !clearOnEmpty) return;
4080
- this.update((state) => {
4081
- const mergeBaseline = preserveChangesSince ?? turnsAtRequestStart;
4082
- if (state.turns === mergeBaseline) return withTurns(state, turns);
4083
- const historyById = new Map(turns.map((turn) => [turn.turn_id, turn]));
4084
- let merged = withTurns(state, turns);
4085
- for (const current of state.turns) {
4086
- const fromHistory = historyById.get(current.turn_id);
4087
- if (!fromHistory || current.sequence > fromHistory.sequence) {
4088
- merged = upsertTurn(merged, current);
4089
- }
4090
- }
4091
- return merged;
4092
- });
4093
- this.replayCommandsFromTurns(turns);
4094
- } catch (error) {
4095
- if (!suppressErrors) throw error;
4096
- }
4097
- }
4098
5769
  /**
4099
- * 把历史里的指令补投给宿主页面。
5770
+ * 权威刷新(运行结束 / 重连补数):**按当前窗口第一条发言取窗口**,不再另记"用户想看哪段"。
4100
5771
  *
4101
- * 重开会话、断线补数时,地图高亮这类"页面当前状态"必须能恢复——
4102
- * 否则刷新一次页面,之前智能体让页面做的事就全丢了。
4103
- * 只回放指令:toolCall / message 这些事件如果按历史全量重放,
4104
- * 监听器会在挂载瞬间收到几百条陈年事件。
4105
- * dispatch 内部按 deliveredCommandKeys 去重,重复调用安全。
5772
+ * 窗口本身就是"用户在看哪段":首屏就是最新一页,向上翻页 / 定位之后首条就是用户滚到的位置。
5773
+ * 直接拿 `from_entry_id = 当前窗口第一条 turn_id` 去取,运行结束、重连补数、翻页之后的对账
5774
+ * 是同一条规则,不需要意图、锚点、已落地锚点这些状态;窗口首条已经不在分支上(404 / 409)时
5775
+ * 回落到最新一页。
4106
5776
  */
4107
- replayCommandsFromTurns(turns) {
5777
+ refreshAuthoritativeHistory(options = {}) {
5778
+ let entryGone = false;
5779
+ const locateTokenAtStart = this.locateToken;
5780
+ return this.replaceFromHistory({
5781
+ ...options,
5782
+ // 锚点在**这次操作真正开始**时采样:排队期间用户可能刚定位过,那时窗口首条才是他要的。
5783
+ fromEntryId: () => this.state.turns[0]?.turn_id,
5784
+ onError: (error) => {
5785
+ entryGone = isEntryGoneError(error);
5786
+ }
5787
+ }).then((located) => {
5788
+ if (located || !entryGone) return located;
5789
+ if (this.locateToken !== locateTokenAtStart) return false;
5790
+ return this.replaceFromHistory(options);
5791
+ });
5792
+ }
5793
+ replaceFromHistory(options = {}) {
5794
+ if (this.disposed) return Promise.resolve(false);
5795
+ const operationEpoch = this.timelineEpoch;
5796
+ const liveRevisionAtStart = options.liveRevisionAtStart ?? this.liveRevision;
5797
+ const modeRevisionAtStart = options.modeRevisionAtStart ?? this.modeRevision;
5798
+ this.invalidateOlderPaging();
5799
+ this.replaceInFlight += 1;
5800
+ const operation = async () => {
5801
+ let previousNextBefore = null;
5802
+ const restoreCursor = () => {
5803
+ if (this.disposed || previousNextBefore === null) return;
5804
+ this.update((state) => ({
5805
+ ...state,
5806
+ nextBefore: previousNextBefore,
5807
+ loadingOlder: false
5808
+ }));
5809
+ };
5810
+ try {
5811
+ if (this.disposed || operationEpoch !== this.timelineEpoch) return false;
5812
+ if (options.isStale?.()) return false;
5813
+ this.invalidateOlderPaging();
5814
+ previousNextBefore = this.state.nextBefore;
5815
+ this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
5816
+ const signal = options.signal;
5817
+ const fromEntryId = typeof options.fromEntryId === "function" ? options.fromEntryId() : options.fromEntryId;
5818
+ const page = await this.runtime.fetchTurnsPage(
5819
+ this.sessionId,
5820
+ fromEntryId ? { fromEntryId } : void 0,
5821
+ signal ? { signal } : void 0
5822
+ );
5823
+ if (this.disposed || operationEpoch !== this.timelineEpoch) {
5824
+ restoreCursor();
5825
+ return false;
5826
+ }
5827
+ if (options.isStale?.()) {
5828
+ restoreCursor();
5829
+ return false;
5830
+ }
5831
+ if (!this.joined) this.seedToolCallDeliveryBaseline(page.turns);
5832
+ this.update((state) => {
5833
+ const next = withTurns(
5834
+ state,
5835
+ replaceWindow(state.turns, page.turns, liveRevisionAtStart, {
5836
+ current: this.liveRevision,
5837
+ byTurnId: this.liveRevisions
5838
+ })
5839
+ );
5840
+ return {
5841
+ ...next,
5842
+ mode: this.modeRevision === modeRevisionAtStart ? page.currentMode : state.mode,
5843
+ nextBefore: page.nextBefore,
5844
+ loadingOlder: false
5845
+ };
5846
+ });
5847
+ this.pruneOptimisticLiveRevisions();
5848
+ this.projectionBuilder._seedTurns(this.state.turns);
5849
+ return true;
5850
+ } catch (error) {
5851
+ options.onError?.(error);
5852
+ if (!this.disposed && operationEpoch === this.timelineEpoch) {
5853
+ this.update((state) => ({
5854
+ ...state,
5855
+ nextBefore: previousNextBefore,
5856
+ loadingOlder: false
5857
+ }));
5858
+ }
5859
+ if (options.suppressErrors === false) throw error;
5860
+ return false;
5861
+ } finally {
5862
+ this.replaceInFlight -= 1;
5863
+ }
5864
+ };
5865
+ const run = this.replaceQueue.then(operation);
5866
+ this.replaceQueue = run.catch(() => void 0);
5867
+ return run;
5868
+ }
5869
+ invalidateOlderPaging() {
5870
+ this.pagingGeneration += 1;
5871
+ this.olderPending = null;
5872
+ }
5873
+ beginCommandRecovery() {
5874
+ this.commandRecoveryGeneration += 1;
5875
+ this.commandRestoreRetryAttempt = 0;
5876
+ this.bufferedCommandDeliveries ??= [];
5877
+ if (this.commandRestoreRetryHandle !== null) {
5878
+ globalThis.clearTimeout(this.commandRestoreRetryHandle);
5879
+ this.commandRestoreRetryHandle = null;
5880
+ }
5881
+ return this.commandRecoveryGeneration;
5882
+ }
5883
+ scheduleHistoricalCommandRestore(generation) {
5884
+ const restore = async () => {
5885
+ if (this.disposed || generation !== this.commandRecoveryGeneration) return;
5886
+ try {
5887
+ const commands = await this.runtime.fetchCommands(this.sessionId);
5888
+ if (this.disposed) return;
5889
+ for (const command of commands) {
5890
+ if (this.disposed) return;
5891
+ this.deliverCommandNow({
5892
+ deliveryKey: commandDeliveryKey(command.deliveryId),
5893
+ action: command.action,
5894
+ payload: command.payload,
5895
+ toolCallId: command.toolCallId
5896
+ });
5897
+ }
5898
+ if (generation !== this.commandRecoveryGeneration) return;
5899
+ this.commandRestoreRetryAttempt = 0;
5900
+ this.flushBufferedCommands(generation);
5901
+ } catch {
5902
+ if (!this.disposed && generation === this.commandRecoveryGeneration && this.commandRestoreRetryHandle === null) {
5903
+ const retryDelay = Math.min(
5904
+ COMMAND_RESTORE_RETRY_BASE_MS * 2 ** this.commandRestoreRetryAttempt,
5905
+ COMMAND_RESTORE_RETRY_MAX_MS
5906
+ );
5907
+ this.commandRestoreRetryAttempt += 1;
5908
+ this.commandRestoreRetryHandle = globalThis.setTimeout(() => {
5909
+ this.commandRestoreRetryHandle = null;
5910
+ if (!this.disposed && generation === this.commandRecoveryGeneration) {
5911
+ this.scheduleHistoricalCommandRestore(generation);
5912
+ }
5913
+ }, retryDelay);
5914
+ }
5915
+ }
5916
+ };
5917
+ const run = this.commandRestoreQueue.then(restore);
5918
+ this.commandRestoreQueue = run.catch(() => void 0);
5919
+ }
5920
+ flushBufferedCommands(generation) {
5921
+ const pending = [...this.bufferedCommandDeliveries ?? []];
5922
+ this.bufferedCommandDeliveries = [];
5923
+ let index = 0;
5924
+ while (index < pending.length) {
5925
+ if (this.disposed) return;
5926
+ if (generation !== this.commandRecoveryGeneration) {
5927
+ const currentGenerationBuffer = this.bufferedCommandDeliveries ?? [];
5928
+ this.bufferedCommandDeliveries = [
5929
+ ...pending.slice(index),
5930
+ ...currentGenerationBuffer
5931
+ ];
5932
+ return;
5933
+ }
5934
+ this.deliverCommandNow(pending[index]);
5935
+ index += 1;
5936
+ const reentrantBuffer = this.bufferedCommandDeliveries;
5937
+ if (index === pending.length && reentrantBuffer && reentrantBuffer.length > 0) {
5938
+ pending.push(...reentrantBuffer);
5939
+ this.bufferedCommandDeliveries = [];
5940
+ }
5941
+ }
5942
+ if (!this.disposed && generation === this.commandRecoveryGeneration) {
5943
+ this.bufferedCommandDeliveries = null;
5944
+ }
5945
+ }
5946
+ /** 首次订阅前的 REST 历史建立语义事件基线,随后同一工具的订阅回放不再投递。 */
5947
+ seedToolCallDeliveryBaseline(turns) {
4108
5948
  for (const turn of turns) {
4109
- for (const block of turn.blocks) {
4110
- if (block.type === "tool_bridge" && isToolBridgeContent(block.content)) {
4111
- this.dispatchCommand(turn, block);
5949
+ for (const toolCall of turn.tool_calls) {
5950
+ this.seenToolCallIds.add(toolCall.id);
5951
+ if (toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled") {
5952
+ this.finishedToolCallIds.add(toolCall.id);
4112
5953
  }
4113
5954
  }
4114
5955
  }
4115
5956
  }
4116
- ensureJoined() {
5957
+ ensureJoined(liveRevisionBoundary = this.liveRevision) {
4117
5958
  if (this.joined) return Promise.resolve();
4118
5959
  if (this.joinPending) return this.joinPending;
4119
5960
  const join = this.runtime.emitWithAck(
@@ -4121,18 +5962,18 @@ var AgentSession = class _AgentSession {
4121
5962
  {
4122
5963
  session_id: this.sessionId,
4123
5964
  event_format: "raw",
4124
- resume: true,
4125
- stream_tokens: this.runtime.streamTokens
5965
+ resume: true
4126
5966
  },
4127
5967
  5e3
4128
5968
  ).then((response) => {
4129
5969
  if (response && response.ok === false) {
4130
- throw new Error(
4131
- typeof response.message === "string" && response.message ? response.message : "Session subscription failed"
5970
+ throw new AgentSessionError(
5971
+ typeof response.message === "string" && response.message ? response.message : "Session subscription failed",
5972
+ typeof response.code === "string" ? response.code : void 0
4132
5973
  );
4133
5974
  }
4134
5975
  this.joined = true;
4135
- this._applyRunningState(Boolean(response?.is_running));
5976
+ this._applyRunningState(Boolean(response?.is_running), liveRevisionBoundary);
4136
5977
  }).finally(() => {
4137
5978
  this.joinPending = null;
4138
5979
  });
@@ -4140,6 +5981,7 @@ var AgentSession = class _AgentSession {
4140
5981
  return join;
4141
5982
  }
4142
5983
  async ensureIdleBeforeSend() {
5984
+ if (this.state.isStreaming) return;
4143
5985
  if (Date.now() - this.recentChatEndAt > 3e3) return;
4144
5986
  const deadline = Date.now() + 3e3;
4145
5987
  while (Date.now() < deadline) {
@@ -4174,6 +6016,7 @@ var SessionHub = class {
4174
6016
  connectionListeners = /* @__PURE__ */ new Set();
4175
6017
  bound = false;
4176
6018
  hasConnected = false;
6019
+ authBusyReconnect = new AuthBusyReconnect();
4177
6020
  constructor(client) {
4178
6021
  this.client = client;
4179
6022
  }
@@ -4195,16 +6038,28 @@ var SessionHub = class {
4195
6038
  * 只有最后一份归还才退订房间——否则 StrictMode 双跑、路由抖动这类"连两次
4196
6039
  * 只释放一次"的场景会把仍在用的会话退订掉,之后所有 turn/chat 事件静默丢失。
4197
6040
  */
4198
- async connect(sessionId) {
6041
+ async connect(sessionId, options = {}) {
4199
6042
  const existing = this.sessions.get(sessionId);
4200
6043
  if (existing && !existing.isDisposed) {
4201
6044
  this.hold(sessionId);
6045
+ try {
6046
+ options.setup?.(existing);
6047
+ } catch (error) {
6048
+ existing.dispose();
6049
+ throw error;
6050
+ }
4202
6051
  return this.bootstrapPending.get(sessionId) ?? existing;
4203
6052
  }
4204
6053
  this.ensureBound();
4205
6054
  const session = new AgentSession(sessionId, this.runtimeFor(), this.connection);
4206
6055
  this.sessions.set(sessionId, session);
4207
6056
  this.hold(sessionId);
6057
+ try {
6058
+ options.setup?.(session);
6059
+ } catch (error) {
6060
+ session.dispose();
6061
+ throw error;
6062
+ }
4208
6063
  this.ensureConnected();
4209
6064
  const bootstrap = session._bootstrap().then(() => session);
4210
6065
  this.bootstrapPending.set(sessionId, bootstrap);
@@ -4228,6 +6083,7 @@ var SessionHub = class {
4228
6083
  /** 手动重连(例如登录态变化后)。 */
4229
6084
  reconnect() {
4230
6085
  this.ensureBound();
6086
+ this.authBusyReconnect.cancel();
4231
6087
  const socket = this.client.socket();
4232
6088
  socket.disconnect();
4233
6089
  this.setConnection("connecting");
@@ -4235,11 +6091,13 @@ var SessionHub = class {
4235
6091
  }
4236
6092
  /** 断开连接(不清理 session 实例)。 */
4237
6093
  disconnect() {
6094
+ this.authBusyReconnect.cancel();
4238
6095
  this.client.socket().disconnect();
4239
6096
  this.setConnection("disconnected");
4240
6097
  }
4241
6098
  /** 清理当前身份下缓存的全部会话,避免切换登录身份后复用旧快照。 */
4242
6099
  disposeAll() {
6100
+ this.authBusyReconnect.cancel();
4243
6101
  this.bootstrapPending.clear();
4244
6102
  for (const session of [...this.sessions.values()]) {
4245
6103
  this.forceRelease(session);
@@ -4251,6 +6109,7 @@ var SessionHub = class {
4251
6109
  * turn:* / chat:* 事件都会静默丢弃。
4252
6110
  */
4253
6111
  _handleSocketReplaced() {
6112
+ this.authBusyReconnect.cancel();
4254
6113
  this.bound = false;
4255
6114
  this.setConnection("disconnected");
4256
6115
  if (this.sessions.size === 0) return;
@@ -4259,23 +6118,15 @@ var SessionHub = class {
4259
6118
  }
4260
6119
  runtimeFor() {
4261
6120
  return {
4262
- streamTokens: this.client.options?.streamTokens !== false,
4263
6121
  emitEvent: (event, data) => {
4264
6122
  ;
4265
6123
  this.client.socket().emit(event, data);
4266
6124
  },
4267
- emitWithAck: (event, data, timeoutMs) => new Promise((resolve, reject) => {
4268
- ;
4269
- this.client.socket().timeout(timeoutMs).emit(event, data, (err, response) => {
4270
- if (err) {
4271
- reject(err);
4272
- return;
4273
- }
4274
- resolve(response);
4275
- });
4276
- }),
6125
+ emitWithAck: (event, data, timeoutMs) => this.client.requestCommand(event, data, timeoutMs),
4277
6126
  ensureConnected: () => this.ensureConnected(),
4278
- fetchTurns: (sessionId) => this.client.sessions.getSessionTurns(sessionId),
6127
+ fetchTurnsPage: (sessionId, query, init) => this.client.sessions.getSessionTurnsPage(sessionId, query, init),
6128
+ fetchTurnIndex: (sessionId) => this.client.sessions.getSessionTurnIndex(sessionId),
6129
+ fetchCommands: (sessionId) => this.client.sessions.getSessionTurnCommands(sessionId),
4279
6130
  fetchSessionInfo: (sessionId) => this.client.sessions.getSession(sessionId),
4280
6131
  updateReplay: (sessionId, payload) => this.client.sessions.updateReplaySession(sessionId, payload).then((result) => result.replay_state ?? null),
4281
6132
  listDir: (sessionId, dirPath) => this.client.sessions.listDir(sessionId, dirPath),
@@ -4303,6 +6154,9 @@ var SessionHub = class {
4303
6154
  if (this.sessions.get(session.sessionId) === session) {
4304
6155
  this.sessions.delete(session.sessionId);
4305
6156
  }
6157
+ if (this.sessions.size === 0) {
6158
+ this.authBusyReconnect.cancel();
6159
+ }
4306
6160
  ;
4307
6161
  this.client.socket().emit("session:unsubscribe", { session_id: session.sessionId });
4308
6162
  return true;
@@ -4311,10 +6165,13 @@ var SessionHub = class {
4311
6165
  forceRelease(session) {
4312
6166
  this.holdCounts.delete(session.sessionId);
4313
6167
  session.dispose();
6168
+ if (this.sessions.size === 0) {
6169
+ this.authBusyReconnect.cancel();
6170
+ }
4314
6171
  }
4315
6172
  ensureConnected() {
4316
6173
  const socket = this.client.socket();
4317
- if (socket.connected || socket.active) return;
6174
+ if (socket.connected || socket.active || this.authBusyReconnect.pending) return;
4318
6175
  this.setConnection("connecting");
4319
6176
  socket.connect();
4320
6177
  }
@@ -4346,17 +6203,35 @@ var SessionHub = class {
4346
6203
  this.bound = true;
4347
6204
  const socket = this.client.socket();
4348
6205
  socket.on("connect", () => {
6206
+ this.authBusyReconnect.cancel();
4349
6207
  const isReconnect = this.hasConnected;
4350
6208
  this.hasConnected = true;
4351
6209
  this.setConnection("connected");
4352
6210
  if (isReconnect) {
4353
6211
  for (const session of this.sessions.values()) {
4354
6212
  void session._rejoinAfterReconnect();
6213
+ void session.refreshQueue();
4355
6214
  }
4356
6215
  }
4357
6216
  });
4358
- socket.on("connect_error", () => this.setConnection("disconnected"));
4359
- socket.on("disconnect", () => this.setConnection("disconnected"));
6217
+ socket.on("connect_error", (error) => {
6218
+ this.setConnection("disconnected");
6219
+ if (this.sessions.size === 0) {
6220
+ this.authBusyReconnect.cancel();
6221
+ return;
6222
+ }
6223
+ if (this.authBusyReconnect.schedule(error, () => {
6224
+ const current = this.client.socket();
6225
+ if (current !== socket || current.connected || current.active) return;
6226
+ this.setConnection("connecting");
6227
+ current.connect();
6228
+ })) return;
6229
+ this.authBusyReconnect.cancel();
6230
+ });
6231
+ socket.on("disconnect", () => {
6232
+ this.authBusyReconnect.cancel();
6233
+ this.setConnection("disconnected");
6234
+ });
4360
6235
  socket.io.on("reconnect_attempt", () => this.setConnection("reconnecting"));
4361
6236
  socket.on("turn:start", (data) => {
4362
6237
  for (const session of this.route(data.session_id)) session._handleTurnStart(data);
@@ -4367,9 +6242,25 @@ var SessionHub = class {
4367
6242
  socket.on("turn:end", (data) => {
4368
6243
  for (const session of this.route(data.session_id)) session._handleTurnEnd(data);
4369
6244
  });
4370
- socket.on("turn:events", (data) => {
4371
- for (const session of this.route(data.session_id)) session._handleTurnEvents(data.events ?? []);
4372
- });
6245
+ socket.on(
6246
+ "turn:timing",
6247
+ (data) => {
6248
+ for (const session of this.route(data.session_id)) {
6249
+ session._handleTurnEvents([
6250
+ { type: "llm:first_token:delivered", payload: data }
6251
+ ]);
6252
+ }
6253
+ }
6254
+ );
6255
+ socket.on(
6256
+ "turn:events",
6257
+ (data, acknowledge) => {
6258
+ acknowledgeTurnEvents(acknowledge);
6259
+ for (const session of this.route(data.session_id)) {
6260
+ session._handleTurnEvents(data.events ?? [], data.replayed === true);
6261
+ }
6262
+ }
6263
+ );
4373
6264
  socket.on("chat:start", (data) => {
4374
6265
  for (const session of this.route(data?.session_id)) session._handleChatStart();
4375
6266
  });
@@ -4378,8 +6269,17 @@ var SessionHub = class {
4378
6269
  session._handleChatEnd(data?.status ?? "completed");
4379
6270
  }
4380
6271
  });
4381
- socket.on("system:error", (data) => {
4382
- for (const session of this.route(data.session_id)) session._handleSystemError(data.message);
6272
+ socket.on(
6273
+ "system:error",
6274
+ (data) => {
6275
+ const code = data.code == null ? void 0 : String(data.code);
6276
+ for (const session of this.route(data.session_id)) {
6277
+ session._handleSystemError(data.message, code);
6278
+ }
6279
+ }
6280
+ );
6281
+ socket.on("chat:queue:updated", (data) => {
6282
+ for (const session of this.route(data?.session_id)) session._handleQueueUpdated(data);
4383
6283
  });
4384
6284
  socket.on(
4385
6285
  "system:notification",
@@ -4497,21 +6397,15 @@ function resolveAuthToken(options) {
4497
6397
  return token ? token : null;
4498
6398
  }
4499
6399
 
4500
- // src/version.ts
4501
- var SDK_NAME = "agent-client";
4502
- var SDK_VERSION = true ? "2610.0.0-beta.9" : "1.1.1";
4503
-
4504
6400
  // src/socket.ts
4505
- function withSdkIdentity(auth) {
4506
- return { ...auth ?? {}, sdk: SDK_NAME, sdk_version: SDK_VERSION };
4507
- }
4508
6401
  function createSocket(options) {
4509
6402
  const token = resolveAuthToken(options);
4510
6403
  return io(options.baseUrl, {
4511
6404
  path: options.path ?? "/socket.io",
4512
6405
  withCredentials: true,
6406
+ ...options.transports ? { transports: options.transports } : {},
4513
6407
  query: token ? { token } : void 0,
4514
- auth: typeof options.token === "function" ? (cb) => cb(withSdkIdentity(buildSocketAuth(options))) : withSdkIdentity(buildSocketAuth(options)),
6408
+ auth: typeof options.token === "function" ? (cb) => cb(buildSocketAuth(options) ?? {}) : buildSocketAuth(options) ?? {},
4515
6409
  autoConnect: false
4516
6410
  });
4517
6411
  }
@@ -4527,8 +6421,17 @@ var BladeClient = class {
4527
6421
  options;
4528
6422
  auth;
4529
6423
  headless;
6424
+ computers;
4530
6425
  models;
4531
6426
  sessions;
6427
+ /** 按用户读取的插件目录与图标;没有会话时也能用。 */
6428
+ plugins;
6429
+ chatGroups;
6430
+ /**
6431
+ * 全站访问统计:匿名读写,不带 bearer 也不触发登录刷新。
6432
+ * 只有内置 Web 的同源页面使用,第三方宿主页面不会被自动统计。
6433
+ */
6434
+ siteVisits;
4532
6435
  /** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
4533
6436
  hub;
4534
6437
  constructor(options) {
@@ -4538,8 +6441,12 @@ var BladeClient = class {
4538
6441
  };
4539
6442
  this.auth = new AuthResource(this);
4540
6443
  this.headless = new HeadlessResource(this);
6444
+ this.computers = new ComputersResource(this);
4541
6445
  this.models = new ModelsResource(this);
4542
6446
  this.sessions = new SessionsResource(this);
6447
+ this.plugins = new PluginsResource(this);
6448
+ this.chatGroups = new ChatGroupsResource(this);
6449
+ this.siteVisits = new SiteVisitsResource(this);
4543
6450
  this.hub = new SessionHub(this);
4544
6451
  if (this.options.token === void 0) {
4545
6452
  this.runtimeToken = readStoredToken(this.options.baseUrl);
@@ -4634,6 +6541,16 @@ var BladeClient = class {
4634
6541
  }
4635
6542
  return this.socketInstance;
4636
6543
  }
6544
+ requestCommand(event, payload, timeoutMs) {
6545
+ const send = () => new Promise((resolve, reject) => {
6546
+ ;
6547
+ this.socket().timeout(timeoutMs).emit(event, payload, (error, ack) => {
6548
+ if (error) reject(error);
6549
+ else resolve(ack ?? {});
6550
+ });
6551
+ });
6552
+ return this.options.commandTransport?.(event, payload, send) ?? send();
6553
+ }
4637
6554
  async json(method, path, body) {
4638
6555
  const response = await this.fetch(method, path, {
4639
6556
  body: body === void 0 ? void 0 : JSON.stringify(body),
@@ -4698,7 +6615,7 @@ var BladeClient = class {
4698
6615
  signal: init.signal
4699
6616
  });
4700
6617
  if (response.status === 401 && !isRetry && this.shouldRefreshFor401(url)) {
4701
- const refreshed = await this.tryRefresh();
6618
+ const refreshed = await this.tryRefreshUnlessAborted(init.signal);
4702
6619
  if (refreshed) {
4703
6620
  return this.fetch(method, path, init, true);
4704
6621
  }
@@ -4710,7 +6627,7 @@ var BladeClient = class {
4710
6627
  return response;
4711
6628
  }
4712
6629
  buildAuthedUrl(path) {
4713
- const url = new URL(this.toBaseRelativePath(path), this.baseUrlWithTrailingSlash());
6630
+ const url = buildBackendUrl(this.options.baseUrl, path);
4714
6631
  const token = this.resolveTokenForUrl(url);
4715
6632
  if (token) {
4716
6633
  url.searchParams.set("token", token);
@@ -4722,6 +6639,7 @@ var BladeClient = class {
4722
6639
  if (!this.isSameBackendUrl(url)) {
4723
6640
  return result;
4724
6641
  }
6642
+ result.set("X-Blade-Client", `${SDK_NAME}/${SDK_VERSION}`);
4725
6643
  const token = this.resolveRestToken();
4726
6644
  if (token) {
4727
6645
  result.set("Authorization", `Bearer ${token}`);
@@ -4729,14 +6647,11 @@ var BladeClient = class {
4729
6647
  return result;
4730
6648
  }
4731
6649
  buildUrl(path) {
4732
- return new URL(this.toBaseRelativePath(path), this.baseUrlWithTrailingSlash());
6650
+ return buildBackendUrl(this.options.baseUrl, path);
4733
6651
  }
4734
6652
  baseUrlWithTrailingSlash() {
4735
6653
  return this.options.baseUrl.endsWith("/") ? this.options.baseUrl : `${this.options.baseUrl}/`;
4736
6654
  }
4737
- toBaseRelativePath(path) {
4738
- return path.startsWith("/") ? path.slice(1) : path;
4739
- }
4740
6655
  isSameBackendUrl(url) {
4741
6656
  const base = new URL(this.baseUrlWithTrailingSlash());
4742
6657
  if (url.origin !== base.origin) {
@@ -4755,6 +6670,27 @@ var BladeClient = class {
4755
6670
  hasExplicitBearerToken() {
4756
6671
  return Boolean(this.resolveRestToken());
4757
6672
  }
6673
+ /**
6674
+ * 等一次令牌刷新,但调用方中止请求时不再等下去。
6675
+ *
6676
+ * 刷新本身是共享的(`refreshPromise`,别的请求也在等它),这里只是让这次已被取消的
6677
+ * 请求尽早结束——否则它会一直挂在调用方(定位)的串行队列里,用户改选的新目标要等到
6678
+ * 刷新结束才轮得到。
6679
+ */
6680
+ async tryRefreshUnlessAborted(signal) {
6681
+ if (!signal) return this.tryRefresh();
6682
+ if (signal.aborted) throw new DOMException("The operation was aborted.", "AbortError");
6683
+ let onAbort;
6684
+ const aborted = new Promise((_resolve, reject) => {
6685
+ onAbort = () => reject(new DOMException("The operation was aborted.", "AbortError"));
6686
+ signal.addEventListener("abort", onAbort, { once: true });
6687
+ });
6688
+ try {
6689
+ return await Promise.race([this.tryRefresh(), aborted]);
6690
+ } finally {
6691
+ if (onAbort) signal.removeEventListener("abort", onAbort);
6692
+ }
6693
+ }
4758
6694
  async tryRefresh() {
4759
6695
  if (this.refreshPromise) {
4760
6696
  return this.refreshPromise;
@@ -4765,7 +6701,9 @@ var BladeClient = class {
4765
6701
  this.buildUrl(REFRESH_PATH).toString(),
4766
6702
  {
4767
6703
  method: "POST",
4768
- credentials: "include"
6704
+ credentials: "include",
6705
+ // Refresh remains cookie-only; do not attach the REST bearer.
6706
+ headers: { "X-Blade-Client": `${SDK_NAME}/${SDK_VERSION}` }
4769
6707
  }
4770
6708
  );
4771
6709
  if (!response.ok) {
@@ -4892,6 +6830,122 @@ function parseXhrHeaders(rawHeaders) {
4892
6830
  return headers;
4893
6831
  }
4894
6832
 
6833
+ // src/polling-backoff.ts
6834
+ var MAX_BACKOFF_MS = 6e4;
6835
+ var MAX_TIMER_MS = 2147483647;
6836
+ var PollingBackoff = class {
6837
+ constructor(intervalMs) {
6838
+ this.intervalMs = intervalMs;
6839
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0 || intervalMs > MAX_BACKOFF_MS) {
6840
+ throw new RangeError("Polling interval must be between 0 and 60000 ms");
6841
+ }
6842
+ }
6843
+ intervalMs;
6844
+ failures = 0;
6845
+ nextAt = 0;
6846
+ reset() {
6847
+ this.failures = 0;
6848
+ this.nextAt = 0;
6849
+ }
6850
+ failed(error) {
6851
+ if (error instanceof Error && error.name === "AbortError" || error instanceof BladeApiError && error.status < 500 && error.status !== 408 && error.status !== 429) {
6852
+ this.nextAt = false;
6853
+ return;
6854
+ }
6855
+ this.failures = Math.min(this.failures + 1, 10);
6856
+ const ceiling = Math.min(MAX_BACKOFF_MS, this.intervalMs * 2 ** this.failures);
6857
+ const backoff = Math.max(this.intervalMs, ceiling * (0.5 + Math.random() * 0.5));
6858
+ const header = error instanceof BladeApiError ? error.response.headers.get("Retry-After")?.trim() : void 0;
6859
+ const retryAfter = header ? /^\d+$/.test(header) ? Number(header) * 1e3 : Date.parse(header) - Date.now() : 0;
6860
+ const delay = Math.max(backoff, Number.isNaN(retryAfter) ? 0 : retryAfter);
6861
+ this.nextAt = delay > MAX_TIMER_MS ? false : Date.now() + delay;
6862
+ }
6863
+ /** Zero means ready, false means stop, otherwise wait at least this many ms. */
6864
+ waitMs() {
6865
+ return this.nextAt === false ? false : Math.max(0, this.nextAt - Date.now());
6866
+ }
6867
+ };
6868
+
6869
+ // src/platform-endpoints.ts
6870
+ var PLATFORM_SERVICE_NAMES = [
6871
+ "os",
6872
+ "hub",
6873
+ "agent",
6874
+ "oauth",
6875
+ "llmGateway",
6876
+ "gitea",
6877
+ "tile"
6878
+ ];
6879
+ var EMPTY_PLATFORM_ENDPOINTS = Object.freeze({
6880
+ services: Object.freeze({})
6881
+ });
6882
+ function defaultDocumentBaseUrl() {
6883
+ if (typeof document === "undefined") return void 0;
6884
+ if (document.querySelector("base[href]")) return document.baseURI;
6885
+ if (typeof location !== "undefined" && location.origin) return `${location.origin}/`;
6886
+ return document.baseURI;
6887
+ }
6888
+ function normalizeServiceUrl(value) {
6889
+ if (typeof value !== "string" || !value.trim()) return null;
6890
+ if (value.includes("?") || value.includes("#")) return null;
6891
+ try {
6892
+ const url = new URL(value.trim());
6893
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
6894
+ if (url.username || url.password || url.search || url.hash) return null;
6895
+ return url.toString().replace(/\/$/, "");
6896
+ } catch {
6897
+ return null;
6898
+ }
6899
+ }
6900
+ function parsePlatformEndpoints(value) {
6901
+ if (!isRecord(value) || !isRecord(value.services)) return null;
6902
+ const services = {};
6903
+ for (const name of PLATFORM_SERVICE_NAMES) {
6904
+ if (!(name in value.services)) continue;
6905
+ const url = normalizeServiceUrl(value.services[name]);
6906
+ if (!url) return null;
6907
+ if (name === "agent" && new URL(url).pathname !== "/") return null;
6908
+ services[name] = url;
6909
+ }
6910
+ return { services };
6911
+ }
6912
+ async function loadPlatformEndpoints(options = {}) {
6913
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
6914
+ const baseUrl = options.baseUrl ?? defaultDocumentBaseUrl();
6915
+ if (!fetchImpl || !baseUrl) return EMPTY_PLATFORM_ENDPOINTS;
6916
+ const controller = new AbortController();
6917
+ const timeout = globalThis.setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
6918
+ try {
6919
+ const response = await fetchImpl(new URL("config.json", baseUrl), {
6920
+ cache: "no-store",
6921
+ credentials: "same-origin",
6922
+ headers: { Accept: "application/json" },
6923
+ signal: controller.signal
6924
+ });
6925
+ if (!response.ok) return EMPTY_PLATFORM_ENDPOINTS;
6926
+ return parsePlatformEndpoints(await response.json()) ?? EMPTY_PLATFORM_ENDPOINTS;
6927
+ } catch {
6928
+ return EMPTY_PLATFORM_ENDPOINTS;
6929
+ } finally {
6930
+ globalThis.clearTimeout(timeout);
6931
+ }
6932
+ }
6933
+ function resolveServiceUrl(endpoints, name, path = "") {
6934
+ const baseUrl = endpoints.services[name];
6935
+ if (!baseUrl) return null;
6936
+ if (!path) return baseUrl;
6937
+ const pathReference = path.trimStart();
6938
+ if (path.includes("\\") || pathReference.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(pathReference)) {
6939
+ return null;
6940
+ }
6941
+ const base = new URL(`${baseUrl}/`);
6942
+ const target = new URL(path.replace(/^\/+/, ""), base);
6943
+ if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) {
6944
+ return null;
6945
+ }
6946
+ return target.toString();
6947
+ }
6948
+
4895
6949
  // src/commands/protocol.ts
4896
6950
  function isCommandEnvelope(value) {
4897
6951
  if (typeof value !== "object" || value === null) return false;
@@ -4992,12 +7046,20 @@ var Task = type2({
4992
7046
  });
4993
7047
  export {
4994
7048
  AgentSession,
7049
+ AgentSessionError,
7050
+ AuthBusyReconnect,
4995
7051
  BladeApiError,
4996
7052
  BladeClient,
7053
+ ChatGroupsResource,
4997
7054
  ClientProjectionBuilder,
7055
+ ComputersResource,
4998
7056
  DEFAULT_REPLAY_SPEED,
7057
+ EMPTY_PLATFORM_ENDPOINTS,
7058
+ EMPTY_SESSION_QUEUE,
4999
7059
  LayoutType,
5000
7060
  ModelsResource,
7061
+ PluginsResource,
7062
+ PollingBackoff,
5001
7063
  SDK_NAME,
5002
7064
  SDK_VERSION,
5003
7065
  SessionHub,
@@ -5007,22 +7069,60 @@ export {
5007
7069
  Task,
5008
7070
  TaskStatus,
5009
7071
  acceptedPostChatFollowupCompletesLatestRun,
7072
+ acknowledgeTurnEvents,
7073
+ authBusyRetryDelayMs,
5010
7074
  buildMessageContent,
7075
+ buildToolUiCardKey,
7076
+ canCancelQueuedMessage,
7077
+ canDeliverQueuedMessage,
7078
+ canEditQueuedMessage,
7079
+ canToggleComputer,
7080
+ chatErrorForDisplay,
7081
+ collectInlineToolUiCards,
7082
+ collectPreviewToolUiCards,
7083
+ computerDaemonVersion,
7084
+ computerOS,
7085
+ computerPlatformLabel,
7086
+ computerState,
5011
7087
  connectEmbedded,
5012
7088
  contentPreview,
7089
+ contextProjectionData,
5013
7090
  createInitialSessionState,
5014
7091
  createSocket,
5015
7092
  extractTextAttachments,
7093
+ getContextDisplayState,
7094
+ getContextGroupDisplayState,
5016
7095
  getFileParts,
5017
7096
  getImageParts,
5018
7097
  getTextContent,
7098
+ groupAdjacentContextRuns,
5019
7099
  groupMessagesByLoop,
7100
+ hasChatRunEvent,
7101
+ inferToolStatus,
7102
+ isAppDevToolUiCard,
7103
+ isBackgroundWakeMessage,
5020
7104
  isCommandEnvelope,
7105
+ isEntryGoneError,
5021
7106
  isHiddenInternalMessage,
5022
7107
  isInboundEnvelope,
7108
+ isInternalStatusToolUiCard,
7109
+ isMcpAppData,
7110
+ isSessionQueueSnapshot,
7111
+ isToolUiCard,
5023
7112
  latestPostChatFollowup,
7113
+ loadPlatformEndpoints,
5024
7114
  normalizeMessageContent,
7115
+ parseQueueAck,
7116
+ prependOlder,
7117
+ queuePauseReasonLabel,
7118
+ queuedMessageStatusLabel,
7119
+ reconcileHistoricalUserTurns,
5025
7120
  reconcileOptimisticUserTurns,
7121
+ replaceWindow,
7122
+ resolveServiceUrl,
7123
+ resolveToolUiCardContent,
7124
+ shouldApplySnapshot,
7125
+ sortComputers,
5026
7126
  toReplaySnapshot,
5027
7127
  transformSlashCommand
5028
7128
  };