@blade-hq/agent-client 2610.0.0-rc.0 → 2610.0.0

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 (37) hide show
  1. package/README.md +202 -12
  2. package/dist/auth.d.ts +0 -1
  3. package/dist/blade-client.d.ts +22 -1
  4. package/dist/index.d.ts +31 -9
  5. package/dist/index.js +1851 -263
  6. package/dist/index.js.map +1 -1
  7. package/dist/polling-backoff.d.ts +11 -0
  8. package/dist/resources/chat-groups.d.ts +31 -0
  9. package/dist/resources/computers.d.ts +54 -2
  10. package/dist/resources/plugins.d.ts +43 -0
  11. package/dist/resources/sessions.d.ts +190 -1
  12. package/dist/resources/site-visits.d.ts +32 -0
  13. package/dist/rest.d.ts +8 -0
  14. package/dist/schemas/mcp-app.d.ts +30 -0
  15. package/dist/schemas/message-utils.d.ts +10 -0
  16. package/dist/schemas/message.d.ts +3 -0
  17. package/dist/schemas/projection.d.ts +25 -1
  18. package/dist/schemas/session.d.ts +7 -0
  19. package/dist/session/agent-session.d.ts +152 -18
  20. package/dist/session/definition.d.ts +6 -1
  21. package/dist/session/errors.d.ts +11 -0
  22. package/dist/session/events.d.ts +14 -2
  23. package/dist/session/history-page.d.ts +9 -0
  24. package/dist/session/hub.d.ts +1 -0
  25. package/dist/session/queue.d.ts +104 -0
  26. package/dist/session/state.d.ts +15 -1
  27. package/dist/shared/auth-busy.d.ts +9 -0
  28. package/dist/shared/projection/builder.d.ts +10 -0
  29. package/dist/shared/projection/helpers.d.ts +2 -2
  30. package/dist/shared/projection/index.d.ts +1 -1
  31. package/dist/shared/projection/state.d.ts +7 -0
  32. package/dist/shared/tool-ui-card.d.ts +60 -0
  33. package/dist/shared/type-guards.d.ts +2 -0
  34. package/dist/socket.d.ts +2 -0
  35. package/dist/types/socket-events.d.ts +186 -2
  36. package/package.json +1 -1
  37. package/public-api.md +747 -11
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" : "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) {
@@ -394,9 +407,6 @@ function errorMessageFromResult(result) {
394
407
  }
395
408
  return null;
396
409
  }
397
- function isRecord(value) {
398
- return typeof value === "object" && value !== null && !Array.isArray(value);
399
- }
400
410
  function ensureSocketConnected(socket, timeoutMs, detail) {
401
411
  if (socket.connected) {
402
412
  return Promise.resolve();
@@ -430,13 +440,29 @@ function computerState(computer) {
430
440
  if (computer.enabled) return "enabled";
431
441
  return computer.online ? "idle" : "offline";
432
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
+ }
433
459
  function canToggleComputer(computer) {
434
460
  return !computer.is_primary;
435
461
  }
436
462
  function sortComputers(computers) {
437
463
  return [...computers].sort((a, b) => {
438
- if (a.enabled !== b.enabled) return a.enabled ? -1 : 1;
439
- if (a.online !== b.online) return a.online ? -1 : 1;
464
+ const byCreated = (a.created_at ?? "").localeCompare(b.created_at ?? "");
465
+ if (byCreated !== 0) return byCreated;
440
466
  return a.label.localeCompare(b.label);
441
467
  });
442
468
  }
@@ -445,6 +471,21 @@ var ComputersResource = class {
445
471
  this.client = client;
446
472
  }
447
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
+ }
448
489
  /**
449
490
  * 列出当前用户的全部电脑,带上"在不在线"和"这次会话能不能用"。
450
491
  * 服务端只查库,电脑离线时同样返回,所以拿得到"它离线了"这个结论。
@@ -494,6 +535,7 @@ var SessionInfo = type({
494
535
  "shared?": "boolean",
495
536
  "memory_enabled?": "boolean",
496
537
  "is_persistent?": "boolean",
538
+ "initiator?": "'ui' | 'sdk' | 'unknown' | null",
497
539
  "is_headless?": "boolean",
498
540
  "viewer_role?": "'owner' | 'viewer'",
499
541
  "template_id?": "string | null",
@@ -518,6 +560,9 @@ var SessionInfo = type({
518
560
  "daemon_id?": "string | null",
519
561
  "agent_runtime_id?": "string | null",
520
562
  "workspace_path?": "string | null",
563
+ "ba_version?": "string | null",
564
+ "sandbox_version?": "string | null",
565
+ "chat_group_id?": "string | null",
521
566
  "match?": "unknown"
522
567
  });
523
568
 
@@ -583,7 +628,7 @@ function upsertToolCall(state, toolCallId, toolName, displayName, args) {
583
628
  });
584
629
  }
585
630
  }
586
- function applyToolResult(state, toolCallId, result, status, durationMs, sourceLoop) {
631
+ function applyToolResult(state, toolCallId, result, status, durationMs, ranInParallel, sourceLoop, startedAt = null) {
587
632
  let toolName = "";
588
633
  let displayName = "";
589
634
  let finalResult = result;
@@ -595,6 +640,8 @@ function applyToolResult(state, toolCallId, result, status, durationMs, sourceLo
595
640
  }
596
641
  const update = { status, result: finalResult };
597
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;
598
645
  state.toolCalls[i] = { ...tc, ...update };
599
646
  toolName = tc.tool_name;
600
647
  displayName = tc.display_name;
@@ -776,11 +823,17 @@ function buildCompactionTurn(sequence, compactionId, loopId, status, data) {
776
823
  summary_preview: summaryPreview,
777
824
  summary_full: summaryFull,
778
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,
779
832
  tokens_before: tokensBefore,
780
833
  tokens_after: tokensAfter,
781
834
  saved_ratio: savedRatio,
782
835
  trigger: asTrigger(data.trigger),
783
- 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,
784
837
  fallback_applied: typeof data.fallback_applied === "boolean" ? data.fallback_applied : null
785
838
  };
786
839
  }
@@ -1035,6 +1088,12 @@ function createTurnState(turnId, loopId, opts = {}) {
1035
1088
  toolCalls: [],
1036
1089
  usage: null,
1037
1090
  responseDone: false,
1091
+ firstTokenMs: null,
1092
+ streamDurationMs: null,
1093
+ firstTokenDeliveryMs: null,
1094
+ modelStartedAt: null,
1095
+ modelDurationMs: null,
1096
+ modelStartedAtReported: false,
1038
1097
  memoryRefs: opts.memoryRefs ?? null,
1039
1098
  parentForkToolCallId: null
1040
1099
  };
@@ -1052,8 +1111,13 @@ function snapshot(state, sequence, status) {
1052
1111
  tool_calls: [...state.toolCalls],
1053
1112
  model: state.model,
1054
1113
  usage: state.usage ? { ...state.usage } : null,
1055
- duration_ms: Math.max(0, Date.now() - state.startedAt),
1056
- 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,
1057
1121
  context_window: state.contextWindow,
1058
1122
  memory_refs: state.memoryRefs,
1059
1123
  parent_fork_tool_call_id: state.parentForkToolCallId
@@ -1147,15 +1211,12 @@ function getContextGroupDisplayState(contexts) {
1147
1211
  }
1148
1212
 
1149
1213
  // src/shared/projection/context.ts
1150
- function isRecord2(value) {
1151
- return typeof value === "object" && value !== null && !Array.isArray(value);
1152
- }
1153
1214
  function buildContextTurn(entry, sequence) {
1154
- if (entry.kind !== "context" || !isRecord2(entry.source) || !isRecord2(entry.data)) {
1215
+ if (entry.kind !== "context" || !isRecord(entry.source) || !isRecord(entry.data)) {
1155
1216
  return null;
1156
1217
  }
1157
- const display = isRecord2(entry.data.display) ? entry.data.display : {};
1158
- const message = isRecord2(entry.message) ? entry.message : {};
1218
+ const display = isRecord(entry.data.display) ? entry.data.display : {};
1219
+ const message = isRecord(entry.message) ? entry.message : {};
1159
1220
  const context = contextProjectionData({
1160
1221
  context_kind: typeof entry.source.kind === "string" ? entry.source.kind : null,
1161
1222
  context_key: typeof entry.source.key === "string" ? entry.source.key : null,
@@ -1165,7 +1226,7 @@ function buildContextTurn(entry, sequence) {
1165
1226
  context_revision: typeof entry.data.revision === "string" ? entry.data.revision : null,
1166
1227
  display_title: typeof display.title === "string" ? display.title : null,
1167
1228
  display_summary: typeof display.summary === "string" ? display.summary : null,
1168
- sources: Array.isArray(display.sources) ? display.sources.filter(isRecord2) : []
1229
+ sources: Array.isArray(display.sources) ? display.sources.filter(isRecord) : []
1169
1230
  });
1170
1231
  if (!context) return null;
1171
1232
  const turnId = typeof entry.id === "string" && entry.id ? entry.id : `context-${sequence}`;
@@ -1188,9 +1249,16 @@ function buildContextTurn(entry, sequence) {
1188
1249
  }
1189
1250
 
1190
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
+ }
1191
1259
  function hasChatRunEvent(events) {
1192
1260
  return events.some(
1193
- (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:")
1194
1262
  );
1195
1263
  }
1196
1264
  function acceptedPostChatFollowupCompletesLatestRun(events, updates) {
@@ -1272,7 +1340,10 @@ var ClientProjectionBuilder = class {
1272
1340
  activeTurns = /* @__PURE__ */ new Map();
1273
1341
  lastTurnIds = /* @__PURE__ */ new Map();
1274
1342
  latestTurns = /* @__PURE__ */ new Map();
1343
+ turnsById = /* @__PURE__ */ new Map();
1275
1344
  loopDescriptions = /* @__PURE__ */ new Map();
1345
+ /** loop_name → 正在运行的子智能体起点,agent:end 结算并挂到父工具调用上。 */
1346
+ activeSubagentRuns = /* @__PURE__ */ new Map();
1276
1347
  pendingChildPauses = /* @__PURE__ */ new Map();
1277
1348
  pendingMemoryRefs = /* @__PURE__ */ new Map();
1278
1349
  activeCompactions = /* @__PURE__ */ new Map();
@@ -1332,6 +1403,25 @@ var ClientProjectionBuilder = class {
1332
1403
  }
1333
1404
  if (event.type.startsWith("compaction:"))
1334
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
+ }
1335
1425
  const state = this.activeTurns.get(loopId);
1336
1426
  if (!state) {
1337
1427
  if (event.type === "chat:end") {
@@ -1347,17 +1437,27 @@ var ClientProjectionBuilder = class {
1347
1437
  state.toolCalls = [];
1348
1438
  state.usage = null;
1349
1439
  state.responseDone = false;
1440
+ state.firstTokenMs = null;
1441
+ state.streamDurationMs = null;
1442
+ state.firstTokenDeliveryMs = null;
1350
1443
  return this.syncPatch(state);
1351
1444
  case "llm:thinking:delta":
1445
+ this.applyFirstTokenDelivery(state, payload);
1352
1446
  return this.onDelta(state, "thinking", "", String(payload.content ?? ""));
1353
1447
  case "llm:content:delta":
1448
+ this.applyFirstTokenDelivery(state, payload);
1354
1449
  return this.onDelta(state, "text", "", String(payload.content ?? ""));
1355
1450
  case "llm:content:complete":
1356
1451
  replaceTextContent(state, String(payload.content ?? ""));
1357
1452
  return this.syncPatch(state);
1453
+ case "llm:content:done":
1454
+ this.applyStreamTiming(state, payload);
1455
+ return this.syncPatch(state);
1358
1456
  case "llm:tool_call:created":
1457
+ this.applyFirstTokenDelivery(state, payload);
1359
1458
  return this.onToolCallCreated(state, payload);
1360
1459
  case "llm:tool_call:arguments:delta":
1460
+ this.applyFirstTokenDelivery(state, payload);
1361
1461
  return this.onToolCallArgumentsDelta(state, payload);
1362
1462
  case "llm:response:done":
1363
1463
  return this.onResponseDone(state, loopId, payload);
@@ -1385,7 +1485,9 @@ var ClientProjectionBuilder = class {
1385
1485
  this.activeTurns.clear();
1386
1486
  this.lastTurnIds.clear();
1387
1487
  this.latestTurns.clear();
1488
+ this.turnsById.clear();
1388
1489
  this.loopDescriptions.clear();
1490
+ this.activeSubagentRuns.clear();
1389
1491
  this.pendingChildPauses.clear();
1390
1492
  this.pendingMemoryRefs.clear();
1391
1493
  this.activeCompactions.clear();
@@ -1400,15 +1502,18 @@ var ClientProjectionBuilder = class {
1400
1502
  }
1401
1503
  _seedTurns(turns) {
1402
1504
  const latestTurns = /* @__PURE__ */ new Map();
1505
+ const turnsById = /* @__PURE__ */ new Map();
1403
1506
  for (const turn of turns) {
1404
1507
  this.seedSequence(turn.sequence);
1405
1508
  if (!isConversationTurn(turn)) continue;
1509
+ turnsById.set(turn.turn_id, turn);
1406
1510
  const current = latestTurns.get(turn.loop_id);
1407
1511
  if (!current || turn.sequence >= current.sequence) {
1408
1512
  latestTurns.set(turn.loop_id, turn);
1409
1513
  }
1410
1514
  }
1411
1515
  this.latestTurns = latestTurns;
1516
+ this.turnsById = turnsById;
1412
1517
  }
1413
1518
  // --- Event handlers ---
1414
1519
  onMemoryInject(loopId, payload) {
@@ -1427,7 +1532,7 @@ var ClientProjectionBuilder = class {
1427
1532
  return null;
1428
1533
  }
1429
1534
  onContextEntry(payload) {
1430
- if (!isRecord3(payload.entry)) return null;
1535
+ if (!isRecord(payload.entry)) return null;
1431
1536
  const turn = buildContextTurn(payload.entry, this.nextSeq());
1432
1537
  return turn ? [{ kind: "upsert", turn }] : null;
1433
1538
  }
@@ -1435,7 +1540,7 @@ var ClientProjectionBuilder = class {
1435
1540
  let turnId = String(payload.turn_id ?? this.nextTurnId(`turn:${loopId}`));
1436
1541
  let memoryRefs = null;
1437
1542
  const pending = this.pendingMemoryRefs.get(loopId);
1438
- if (pending) {
1543
+ if (pending && (!pending.turnId || pending.turnId === turnId)) {
1439
1544
  this.pendingMemoryRefs.delete(loopId);
1440
1545
  if (pending.turnId) turnId = pending.turnId;
1441
1546
  memoryRefs = pending.refs;
@@ -1451,6 +1556,7 @@ var ClientProjectionBuilder = class {
1451
1556
  this.lastTurnIds.set(loopId, turnId);
1452
1557
  const turn = snapshot(state, this.nextSeq(), "streaming");
1453
1558
  this.latestTurns.set(loopId, turn);
1559
+ this.turnsById.set(turn.turn_id, turn);
1454
1560
  return [{ kind: "upsert", turn }];
1455
1561
  }
1456
1562
  onModeChange(loopId, payload) {
@@ -1487,11 +1593,11 @@ var ClientProjectionBuilder = class {
1487
1593
  loopId,
1488
1594
  toolCallId: optStr(payload.tool_call_id) ?? void 0,
1489
1595
  toolName: toolNameFromPayload(payload) ?? void 0,
1490
- workspacePath: isRecord3(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
1491
1597
  }
1492
1598
  ];
1493
1599
  const change = payload.workspace_change;
1494
- if (isRecord3(change)) {
1600
+ if (isRecord(change)) {
1495
1601
  const source = optStr(change.source) ?? "";
1496
1602
  const projectName = optStr(change.project_name) ?? "";
1497
1603
  const fromPath = optStr(change.from) ?? "";
@@ -1523,6 +1629,13 @@ var ClientProjectionBuilder = class {
1523
1629
  onAgentStart(loopId, payload) {
1524
1630
  const description = optStr(payload.description);
1525
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
+ }
1526
1639
  const turn = buildSystemNotificationTurn(
1527
1640
  this.nextSeq(),
1528
1641
  this.nextTurnId("agent:start"),
@@ -1564,7 +1677,7 @@ var ClientProjectionBuilder = class {
1564
1677
  this.nextTurnId("bg:tasks_completed"),
1565
1678
  loopId,
1566
1679
  "bg:tasks_completed",
1567
- "done",
1680
+ backgroundTaskStatus(payload),
1568
1681
  `Background tasks completed (${count})`,
1569
1682
  detail || null,
1570
1683
  { task_ids: taskIds, count }
@@ -1584,7 +1697,32 @@ var ClientProjectionBuilder = class {
1584
1697
  }
1585
1698
  const directTurn = this.activeTurns.get(loopId) ?? this.latestTurns.get(loopId);
1586
1699
  setToolCallStatus(directTurn, toolCallId, "done", null, null);
1587
- 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 }];
1588
1726
  }
1589
1727
  onUserMessage(_loopId, payload) {
1590
1728
  const entryId = String(payload.entry_id ?? this.nextTurnId("user"));
@@ -1601,8 +1739,12 @@ var ClientProjectionBuilder = class {
1601
1739
  tool_calls: [],
1602
1740
  model: null,
1603
1741
  usage: null,
1604
- duration_ms: 0
1742
+ duration_ms: 0,
1743
+ client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
1605
1744
  };
1745
+ if (payload.start_reason === "background-completion") {
1746
+ turn.blocks.push({ type: "system_notification", content: { notification_type: "bg:wakeup" } });
1747
+ }
1606
1748
  this.latestTurns.set("root", turn);
1607
1749
  return [{ kind: "upsert", turn }];
1608
1750
  }
@@ -1647,6 +1789,7 @@ var ClientProjectionBuilder = class {
1647
1789
  }
1648
1790
  onResponseDone(state, loopId, payload) {
1649
1791
  state.responseDone = true;
1792
+ this.applyStreamTiming(state, payload);
1650
1793
  const model = optStr(payload.model);
1651
1794
  if (model) state.model = model;
1652
1795
  if (typeof payload.usage === "object" && payload.usage !== null) {
@@ -1680,6 +1823,31 @@ var ClientProjectionBuilder = class {
1680
1823
  }
1681
1824
  return this.syncPatch(state);
1682
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
+ }
1683
1851
  onToolResultDone(state, loopId, payload) {
1684
1852
  const toolCallId = String(payload.tool_call_id ?? "");
1685
1853
  const streamedResult = state.toolCalls.find((toolCall) => toolCall.id === toolCallId)?.result;
@@ -1687,8 +1855,10 @@ var ClientProjectionBuilder = class {
1687
1855
  const status = toolStatusFromResult(content);
1688
1856
  const rawDuration = payload.duration_ms;
1689
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;
1690
1859
  const sl = sourceLoopFor(state.loopId, this.loopDescriptions);
1691
- applyToolResult(state, toolCallId, content, status, durationMs, sl);
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);
1692
1862
  const isNativeProviderResult = typeof payload.provider === "string" && payload.provider !== "";
1693
1863
  if (!isNativeProviderResult && this.shouldFinalizeAfterToolResults(state)) {
1694
1864
  return this.finalize(loopId, "completed");
@@ -1719,10 +1889,13 @@ var ClientProjectionBuilder = class {
1719
1889
  onToolBridge(state, payload) {
1720
1890
  const bridge = payload.bridge;
1721
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;
1722
1894
  state.blocks.push({
1723
1895
  type: "tool_bridge",
1724
1896
  content: bridge,
1725
- tool_call_id: String(payload.tool_call_id ?? "") || null
1897
+ delivery_id: String(payload.delivery_id ?? "") || null,
1898
+ tool_call_id: toolCallId
1726
1899
  });
1727
1900
  return this.syncPatch(state);
1728
1901
  }
@@ -1841,6 +2014,7 @@ var ClientProjectionBuilder = class {
1841
2014
  }
1842
2015
  onAgentEnd(state, loopId, payload) {
1843
2016
  const ok = payload.ok !== false;
2017
+ this.settleSubagentRun(loopId, payload);
1844
2018
  const status = ok ? "done" : "error";
1845
2019
  const title = `Agent: ${payload.description ?? payload.skill_id ?? "agent"}`;
1846
2020
  const detail = optStr(payload.error);
@@ -1848,6 +2022,7 @@ var ClientProjectionBuilder = class {
1848
2022
  skill_id: optStr(payload.skill_id),
1849
2023
  description: optStr(payload.description),
1850
2024
  parent_fork_tool_call_id: optStr(payload.parent_fork_tool_call_id),
2025
+ ...typeof payload.run_status === "string" ? { run_status: payload.run_status } : {},
1851
2026
  ok
1852
2027
  };
1853
2028
  if (state) {
@@ -1867,6 +2042,32 @@ var ClientProjectionBuilder = class {
1867
2042
  );
1868
2043
  return [{ kind: "upsert", turn }];
1869
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
+ }
1870
2071
  dropIncompleteToolCalls(state) {
1871
2072
  if (state.responseDone) return;
1872
2073
  state.toolCalls = [];
@@ -1916,6 +2117,7 @@ var ClientProjectionBuilder = class {
1916
2117
  syncPatch(state) {
1917
2118
  const turn = snapshot(state, this.nextSeq(), "streaming");
1918
2119
  this.latestTurns.set(state.loopId, turn);
2120
+ this.turnsById.set(turn.turn_id, turn);
1919
2121
  this.lastTurnIds.set(state.loopId, turn.turn_id);
1920
2122
  return [{ kind: "upsert", turn }];
1921
2123
  }
@@ -1926,6 +2128,7 @@ var ClientProjectionBuilder = class {
1926
2128
  const turn = snapshot(state, this.nextSeq(), status);
1927
2129
  this.lastTurnIds.set(loopId, turn.turn_id);
1928
2130
  this.latestTurns.set(loopId, turn);
2131
+ this.turnsById.set(turn.turn_id, turn);
1929
2132
  return [{ kind: "upsert", turn }];
1930
2133
  }
1931
2134
  shouldFinalizeAfterToolResults(state) {
@@ -1963,9 +2166,6 @@ var ClientProjectionBuilder = class {
1963
2166
  return `${prefix}:${this.syntheticCounter}:${hex}`;
1964
2167
  }
1965
2168
  };
1966
- function isRecord3(value) {
1967
- return typeof value === "object" && value !== null && !Array.isArray(value);
1968
- }
1969
2169
  function optStr(value) {
1970
2170
  if (value == null || value === "") return null;
1971
2171
  return String(value);
@@ -1985,6 +2185,101 @@ function toolArgsFromPayload(payload) {
1985
2185
  return "";
1986
2186
  }
1987
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
+
1988
2283
  // src/shared/projection/history.ts
1989
2284
  function projectHistory(entries) {
1990
2285
  const turns = [];
@@ -1993,6 +2288,7 @@ function projectHistory(entries) {
1993
2288
  const pendingChildPauses = /* @__PURE__ */ new Map();
1994
2289
  const answeredToolCallIds = /* @__PURE__ */ new Set();
1995
2290
  const nativeToolTurns = /* @__PURE__ */ new Set();
2291
+ const turnEntryAliases = /* @__PURE__ */ new Map();
1996
2292
  let latestRootMessage = null;
1997
2293
  let latestRootAssistant = null;
1998
2294
  let sequence = 0;
@@ -2000,7 +2296,40 @@ function projectHistory(entries) {
2000
2296
  const kind = String(entry.kind ?? "");
2001
2297
  const loopId = String(entry.loop_name ?? "root");
2002
2298
  const id = String(entry.id ?? `history-${sequence + 1}`);
2003
- if (kind === "post_chat_followup" && isRecord4(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)) {
2004
2333
  const assistantEntryId = String(entry.data.assistant_entry_id ?? "");
2005
2334
  if (latestRootMessage?.role === "assistant" && latestRootMessage.turn_id === assistantEntryId) {
2006
2335
  const followup = normalizePostChatFollowup(entry.data, assistantEntryId);
@@ -2016,7 +2345,7 @@ function projectHistory(entries) {
2016
2345
  }
2017
2346
  continue;
2018
2347
  }
2019
- if (kind === "message" && isRecord4(entry.message)) {
2348
+ if (kind === "message" && isRecord(entry.message)) {
2020
2349
  const message = entry.message;
2021
2350
  const role = String(message.role ?? "");
2022
2351
  if (role === "tool") {
@@ -2026,6 +2355,12 @@ function projectHistory(entries) {
2026
2355
  if (owner && toolCall) {
2027
2356
  toolCall.status = message._error ? "error" : inferToolStatus(String(message.content ?? ""));
2028
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;
2029
2364
  owner.blocks.push({
2030
2365
  type: "tool_result",
2031
2366
  content: message.content ?? "",
@@ -2035,8 +2370,11 @@ function projectHistory(entries) {
2035
2370
  continue;
2036
2371
  }
2037
2372
  if (role !== "user" && role !== "assistant" && role !== "system") continue;
2038
- const toolCalls = role === "assistant" ? buildToolCalls(message.tool_calls) : [];
2373
+ const toolCalls = role === "assistant" && !message._error ? buildToolCalls(message.tool_calls) : [];
2039
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
+ }
2040
2378
  const turn = {
2041
2379
  id,
2042
2380
  sequence: ++sequence,
@@ -2048,26 +2386,42 @@ function projectHistory(entries) {
2048
2386
  blocks,
2049
2387
  tool_calls: toolCalls,
2050
2388
  model: stringOrNull(message.model),
2051
- usage: isRecord4(message._usage) ? { ...message._usage } : null,
2052
- duration_ms: numberOrZero(message._duration_ms),
2053
- 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",
2054
2396
  // fork 子会话的父锚点:历史里落在消息级 _parent_fork_tool_call_id。丢掉它,
2055
2397
  // 刷新后工作流步骤 fork 就配不回 workflow:run 工具调用(普通 Agent fork 有
2056
2398
  // root Agent 工具调用兜底配对,工作流 fork 没有,子智能体卡会整体消失)。
2057
2399
  parent_fork_tool_call_id: stringOrNull(message._parent_fork_tool_call_id)
2058
2400
  };
2059
2401
  const isNativeToolEntry = role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.some(
2060
- (toolCall) => isRecord4(toolCall) && typeof toolCall.provider === "string"
2402
+ (toolCall) => isRecord(toolCall) && typeof toolCall.provider === "string"
2061
2403
  );
2062
2404
  const previousTurn = turns[turns.length - 1];
2063
2405
  if (role === "assistant" && previousTurn?.role === "assistant" && previousTurn.loop_id === loopId && nativeToolTurns.has(previousTurn)) {
2406
+ nativeToolTurns.delete(previousTurn);
2064
2407
  previousTurn.blocks.push(...turn.blocks);
2065
2408
  previousTurn.tool_calls.push(...turn.tool_calls);
2066
2409
  previousTurn.status = turn.status;
2067
2410
  previousTurn.model = turn.model ?? previousTurn.model;
2068
2411
  previousTurn.usage = turn.usage ?? previousTurn.usage;
2069
- previousTurn.duration_ms = turn.duration_ms || previousTurn.duration_ms;
2070
- if (turn.started_at) previousTurn.started_at = turn.started_at;
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);
2071
2425
  if (loopId === "root") latestRootMessage = previousTurn;
2072
2426
  latestAssistantByLoop.set(loopId, previousTurn);
2073
2427
  continue;
@@ -2076,6 +2430,7 @@ function projectHistory(entries) {
2076
2430
  latestRootMessage = turn;
2077
2431
  }
2078
2432
  turns.push(turn);
2433
+ turnEntryAliases.set(id, turn);
2079
2434
  if (isNativeToolEntry) nativeToolTurns.add(turn);
2080
2435
  if (role === "assistant") latestAssistantByLoop.set(loopId, turn);
2081
2436
  if (loopId === "root") {
@@ -2103,7 +2458,7 @@ function projectHistory(entries) {
2103
2458
  if (kind === "mode_change") {
2104
2459
  turns.push(markerTurn(id, ++sequence, loopId, "mode_change", entry.data ?? {}));
2105
2460
  } else if (kind === "workspace_change") {
2106
- const change = isRecord4(entry.data) ? entry.data : {};
2461
+ const change = isRecord(entry.data) ? entry.data : {};
2107
2462
  const source = stringOrNull(change.source) ?? "";
2108
2463
  const projectName = stringOrNull(change.project_name) ?? "";
2109
2464
  const fromPath = stringOrNull(change.from) ?? "";
@@ -2132,19 +2487,22 @@ function projectHistory(entries) {
2132
2487
  } else if (kind === "tool_ui" || kind === "tool_bridge") {
2133
2488
  const data = entry.data ?? {};
2134
2489
  const toolCallId = String(data.tool_call_id ?? "");
2135
- const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
2136
- 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;
2137
2494
  if (owner && content !== void 0) {
2138
2495
  owner.blocks.push({
2139
2496
  type: kind,
2140
2497
  content,
2498
+ ...kind === "tool_bridge" ? { delivery_id: id } : {},
2141
2499
  tool_call_id: toolCallId || null
2142
2500
  });
2143
2501
  }
2144
2502
  } else if (kind === "compaction" || kind === "tool_result_archive") {
2145
2503
  const data = entry.data ?? {};
2146
2504
  const compactionId = String(data.compaction_id ?? entry.id ?? `history-${sequence + 1}`);
2147
- let content = isRecord4(entry.message) ? entry.message.content : void 0;
2505
+ let content = isRecord(entry.message) ? entry.message.content : void 0;
2148
2506
  if (kind === "tool_result_archive") {
2149
2507
  const archivedCount = Array.isArray(data.archived_files) ? data.archived_files.length : 0;
2150
2508
  content = `<compaction-summary>
@@ -2152,18 +2510,24 @@ function projectHistory(entries) {
2152
2510
  </compaction-summary>`;
2153
2511
  }
2154
2512
  turns.push(
2155
- buildCompactionTurn(++sequence, compactionId, loopId, "completed", {
2156
- ...data,
2157
- content
2158
- })
2513
+ buildCompactionTurn(
2514
+ ++sequence,
2515
+ compactionId,
2516
+ loopId,
2517
+ data.status === "failed" ? "failed" : "completed",
2518
+ {
2519
+ ...data,
2520
+ content
2521
+ }
2522
+ )
2159
2523
  );
2160
2524
  } else if (kind === "child_pause") {
2161
2525
  const data = entry.data ?? {};
2162
- const sourceLoop = isRecord4(data.source_loop) ? data.source_loop : {};
2526
+ const sourceLoop = isRecord(data.source_loop) ? data.source_loop : {};
2163
2527
  const childLoopId = String(data.child_loop_name ?? sourceLoop.name ?? "");
2164
2528
  const childToolCallId = String(data.child_pause_tool_call_id ?? "");
2165
2529
  const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
2166
- const pauseToolData = isRecord4(data.pause_tool_data) ? data.pause_tool_data : {};
2530
+ const pauseToolData = isRecord(data.pause_tool_data) ? data.pause_tool_data : {};
2167
2531
  const description = String(sourceLoop.description ?? "");
2168
2532
  applyAskUserPauseToTurn(
2169
2533
  latestAssistantByLoop.get(childLoopId),
@@ -2251,15 +2615,15 @@ function parseToolArguments(value) {
2251
2615
  if (!value) return {};
2252
2616
  try {
2253
2617
  const parsed = JSON.parse(value);
2254
- return isRecord4(parsed) ? parsed : {};
2618
+ return isRecord(parsed) ? parsed : {};
2255
2619
  } catch {
2256
2620
  return {};
2257
2621
  }
2258
2622
  }
2259
2623
  function buildToolCalls(value) {
2260
2624
  if (!Array.isArray(value)) return [];
2261
- return value.filter(isRecord4).map((raw) => {
2262
- const fn = isRecord4(raw.function) ? raw.function : {};
2625
+ return value.filter(isRecord).map((raw) => {
2626
+ const fn = isRecord(raw.function) ? raw.function : {};
2263
2627
  const name = String(fn.name ?? "");
2264
2628
  return {
2265
2629
  id: String(raw.id ?? ""),
@@ -2280,7 +2644,7 @@ function buildBlocks(message, toolCalls) {
2280
2644
  blocks.push({ type: "text", content: displayContent });
2281
2645
  } else if (Array.isArray(stored)) {
2282
2646
  for (const raw of stored) {
2283
- if (!isRecord4(raw)) continue;
2647
+ if (!isRecord(raw)) continue;
2284
2648
  const type3 = String(raw.type ?? "");
2285
2649
  if (type3 === "thinking") blocks.push({ type: type3, content: raw.thinking ?? raw.content ?? "" });
2286
2650
  if (type3 === "text") blocks.push({ type: type3, content: raw.text ?? raw.content ?? "" });
@@ -2329,14 +2693,11 @@ function markerTurn(id, sequence, loopId, type3, content, toolCallId = null) {
2329
2693
  duration_ms: 0
2330
2694
  };
2331
2695
  }
2332
- function isRecord4(value) {
2333
- return typeof value === "object" && value !== null && !Array.isArray(value);
2334
- }
2335
2696
  function stringOrNull(value) {
2336
2697
  return typeof value === "string" ? value : null;
2337
2698
  }
2338
- function numberOrZero(value) {
2339
- return typeof value === "number" ? value : 0;
2699
+ function numberOrNull(value) {
2700
+ return typeof value === "number" && value >= 0 ? value : null;
2340
2701
  }
2341
2702
  function findLatestAssistant(turns, loopId) {
2342
2703
  for (let index = turns.length - 1; index >= 0; index -= 1) {
@@ -2347,6 +2708,10 @@ function findLatestAssistant(turns, loopId) {
2347
2708
  }
2348
2709
 
2349
2710
  // src/resources/sessions.ts
2711
+ function isEntryGoneError(error) {
2712
+ const status = error?.status;
2713
+ return status === 404 || status === 409;
2714
+ }
2350
2715
  function toCreateSessionPayload(request) {
2351
2716
  const solutionRef = request.solution_ref ?? externalSolutionRef(request.solution_id);
2352
2717
  return {
@@ -2366,13 +2731,13 @@ function toCreateSessionPayload(request) {
2366
2731
  compaction_ratio: request.compaction_ratio ?? null,
2367
2732
  ...request.runtime_type ? { runtime_type: request.runtime_type } : {},
2368
2733
  ...request.daemon_id ? { daemon_id: request.daemon_id } : {},
2369
- ...request.agent_runtime_id ? { agent_runtime_id: request.agent_runtime_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 } : {}
2370
2736
  };
2371
2737
  }
2372
2738
  var builtinSolutionIds = /* @__PURE__ */ new Set([
2373
2739
  "app-dev",
2374
2740
  "general_chat",
2375
- "multica",
2376
2741
  "night_build",
2377
2742
  "skill_editor",
2378
2743
  "smart_bid",
@@ -2395,6 +2760,54 @@ var SessionsResource = class {
2395
2760
  this.client = client;
2396
2761
  }
2397
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
+ }
2398
2811
  /**
2399
2812
  * 连接一个已存在的会话,返回实时会话对象 AgentSession
2400
2813
  * (自动完成历史加载、Socket.IO 订阅、断线重连补数)。
@@ -2402,6 +2815,56 @@ var SessionsResource = class {
2402
2815
  connect(sessionId) {
2403
2816
  return this.client.hub.connect(sessionId);
2404
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
+ }
2405
2868
  /** 创建新会话并直接连接,返回可收发消息的 AgentSession。 */
2406
2869
  async create(request = {}) {
2407
2870
  if (isSessionDefinition(request)) {
@@ -2498,7 +2961,8 @@ var SessionsResource = class {
2498
2961
  template_id_prefix,
2499
2962
  solution_id,
2500
2963
  solution_registry_id,
2501
- q
2964
+ q,
2965
+ chat_group_id
2502
2966
  }) {
2503
2967
  const searchParams = new URLSearchParams();
2504
2968
  searchParams.set("limit", String(limit));
@@ -2511,6 +2975,7 @@ var SessionsResource = class {
2511
2975
  }
2512
2976
  const trimmedQuery = q?.trim();
2513
2977
  if (trimmedQuery) searchParams.set("q", trimmedQuery);
2978
+ if (chat_group_id) searchParams.set("chat_group_id", chat_group_id);
2514
2979
  return this.client.json("GET", `/api/sessions?${searchParams.toString()}`);
2515
2980
  }
2516
2981
  async listSessions(template_id_prefix, solution_id) {
@@ -2601,6 +3066,55 @@ var SessionsResource = class {
2601
3066
  );
2602
3067
  return projectHistory(entries);
2603
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
+ }
2604
3118
  listResultFeedback(sessionId) {
2605
3119
  return this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/feedback`);
2606
3120
  }
@@ -2668,8 +3182,9 @@ var SessionsResource = class {
2668
3182
  `/api/sessions/${sessionId}/probe-url?url=${encodeURIComponent(url)}`
2669
3183
  );
2670
3184
  }
2671
- listDir(sessionId, dirPath) {
2672
- 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}`);
2673
3188
  }
2674
3189
  async countFiles(sessionId, dirPath = "") {
2675
3190
  const entries = await this.listDir(sessionId, dirPath);
@@ -2864,6 +3379,130 @@ function profileToCreateSessionRequest(profile) {
2864
3379
  };
2865
3380
  }
2866
3381
 
3382
+ // src/resources/plugins.ts
3383
+ var PluginsResource = class {
3384
+ constructor(client) {
3385
+ this.client = client;
3386
+ }
3387
+ client;
3388
+ /**
3389
+ * 列出当前用户安装的插件及其展示信息。
3390
+ *
3391
+ * 服务端可能为了补齐名称/图标惰性获取包内容,但绝不激活插件、不连 MCP;
3392
+ * 单个插件的展示信息读不出来只会降级成占位,不会让整份目录失败。
3393
+ */
3394
+ catalog(init) {
3395
+ return this.client.jsonFromInit("/api/plugins/catalog", init);
3396
+ }
3397
+ /** 取一个插件的包内图标字节(用户级,不需要会话)。 */
3398
+ fetchIcon(name) {
3399
+ return this.client.blob("GET", `/api/plugins/${encodeURIComponent(name)}/icon`);
3400
+ }
3401
+ /** 带凭据的图标地址;能直接用 blob URL 的地方优先用 `fetchIcon`。 */
3402
+ iconUrl(name) {
3403
+ return this.client.buildAuthedUrl(`/api/plugins/${encodeURIComponent(name)}/icon`);
3404
+ }
3405
+ };
3406
+
3407
+ // src/resources/chat-groups.ts
3408
+ var ChatGroupsResource = class {
3409
+ constructor(client) {
3410
+ this.client = client;
3411
+ }
3412
+ client;
3413
+ list() {
3414
+ return this.client.json("GET", "/api/chat-groups");
3415
+ }
3416
+ create(payload) {
3417
+ return this.client.json("POST", "/api/chat-groups", payload);
3418
+ }
3419
+ update(groupId, payload) {
3420
+ return this.client.json("PATCH", `/api/chat-groups/${encodeURIComponent(groupId)}`, payload);
3421
+ }
3422
+ delete(groupId, options) {
3423
+ const query = options?.deleteWorkspace ? "?delete_workspace=true" : "";
3424
+ return this.client.json("DELETE", `/api/chat-groups/${encodeURIComponent(groupId)}${query}`);
3425
+ }
3426
+ };
3427
+
3428
+ // src/resources/site-visits.ts
3429
+ var SITE_VISITS_PATH = "/api/site-visits";
3430
+ var REQUEST_TIMEOUT_MS = 5e3;
3431
+ var SiteVisitsResource = class {
3432
+ constructor(client) {
3433
+ this.client = client;
3434
+ }
3435
+ client;
3436
+ /** 上报一次页面访问,返回当前快照;不承诺本次已计入或已落盘。 */
3437
+ record() {
3438
+ return this.request("POST");
3439
+ }
3440
+ /** 只读快照:不设置访客 Cookie,也不增加任何计数。 */
3441
+ summary() {
3442
+ return this.request("GET");
3443
+ }
3444
+ async request(method) {
3445
+ const url = buildBackendUrl(this.client.options.baseUrl, SITE_VISITS_PATH);
3446
+ const controller = new AbortController();
3447
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
3448
+ try {
3449
+ const response = await (this.client.options.fetchImpl ?? fetch)(url.toString(), {
3450
+ method,
3451
+ // 访客 Cookie 是唯一的去重依据,必须随请求往返。
3452
+ credentials: "include",
3453
+ signal: controller.signal
3454
+ });
3455
+ if (!response.ok) {
3456
+ throw new BladeApiError(response, await extractErrorDetail(response));
3457
+ }
3458
+ return await response.json();
3459
+ } finally {
3460
+ clearTimeout(timeout);
3461
+ }
3462
+ }
3463
+ };
3464
+
3465
+ // src/shared/auth-busy.ts
3466
+ var DEFAULT_RETRY_AFTER_SECONDS = 1;
3467
+ var MAX_TIMER_DELAY_MS = 2147483647;
3468
+ function errorData(error) {
3469
+ if (typeof error !== "object" || error === null || !("data" in error)) return null;
3470
+ const data = error.data;
3471
+ return typeof data === "object" && data !== null ? data : null;
3472
+ }
3473
+ function authBusyRetryDelayMs(error) {
3474
+ const data = errorData(error);
3475
+ if (data?.code !== "auth_busy" || Number(data.status) !== 429) return null;
3476
+ const rawRetryAfter = data.retry_after;
3477
+ const seconds = rawRetryAfter == null || String(rawRetryAfter).trim() === "" ? Number.NaN : Number(rawRetryAfter);
3478
+ const delaySeconds = Number.isFinite(seconds) && seconds >= 0 ? seconds : DEFAULT_RETRY_AFTER_SECONDS;
3479
+ return Math.min(delaySeconds * 1e3, MAX_TIMER_DELAY_MS);
3480
+ }
3481
+ var AuthBusyReconnect = class {
3482
+ timer = null;
3483
+ get pending() {
3484
+ return this.timer !== null;
3485
+ }
3486
+ schedule(error, reconnect) {
3487
+ const delay = authBusyRetryDelayMs(error);
3488
+ if (delay === null) {
3489
+ this.cancel();
3490
+ return false;
3491
+ }
3492
+ this.cancel();
3493
+ this.timer = globalThis.setTimeout(() => {
3494
+ this.timer = null;
3495
+ reconnect();
3496
+ }, delay);
3497
+ return true;
3498
+ }
3499
+ cancel() {
3500
+ if (this.timer === null) return;
3501
+ globalThis.clearTimeout(this.timer);
3502
+ this.timer = null;
3503
+ }
3504
+ };
3505
+
2867
3506
  // src/commands/registry.ts
2868
3507
  var BUFFER_LIMIT_PER_ACTION = 20;
2869
3508
  var CommandRegistry = class {
@@ -2972,6 +3611,172 @@ var Emitter = class {
2972
3611
  }
2973
3612
  };
2974
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
+
2975
3780
  // src/schemas/message-utils.ts
2976
3781
  var SYSTEM_REMINDER_PATTERN = /^<system-reminder>\s*[\s\S]*?\s*<\/system-reminder>$/i;
2977
3782
  var SYSTEM_NOTIFICATION_PATTERN = /^<system-notification>\s*[\s\S]*?\s*<\/system-notification>$/i;
@@ -3155,6 +3960,12 @@ function groupMessagesByLoop(messages) {
3155
3960
  }
3156
3961
  return groups;
3157
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
+ }
3158
3969
 
3159
3970
  // src/session/state.ts
3160
3971
  function toReplaySnapshot(raw) {
@@ -3179,7 +3990,10 @@ function createInitialSessionState(sessionId) {
3179
3990
  agentLoops: {},
3180
3991
  activeCompaction: null,
3181
3992
  replay: null,
3182
- viewerRole: null
3993
+ queue: EMPTY_SESSION_QUEUE,
3994
+ viewerRole: null,
3995
+ nextBefore: null,
3996
+ loadingOlder: false
3183
3997
  };
3184
3998
  }
3185
3999
  function parseAgentDescription(argumentsJson) {
@@ -3205,10 +4019,10 @@ function inferLoopStatusFromMessages(messages) {
3205
4019
  }
3206
4020
  function inferLoopStatusFromTurns(turns, messages) {
3207
4021
  const latestAgentNotification = [...turns].reverse().flatMap((turn) => turn.blocks).find((block) => {
3208
- if (block.type !== "system_notification" || !isRecord5(block.content)) return false;
4022
+ if (block.type !== "system_notification" || !isRecord(block.content)) return false;
3209
4023
  return block.content.notification_type === "agent:start" || block.content.notification_type === "agent:end";
3210
4024
  });
3211
- if (latestAgentNotification?.type === "system_notification" && isRecord5(latestAgentNotification.content)) {
4025
+ if (latestAgentNotification?.type === "system_notification" && isRecord(latestAgentNotification.content)) {
3212
4026
  const notificationType = latestAgentNotification.content.notification_type;
3213
4027
  const status = latestAgentNotification.content.status;
3214
4028
  if (notificationType === "agent:start" || status === "running") return "running";
@@ -3217,11 +4031,8 @@ function inferLoopStatusFromTurns(turns, messages) {
3217
4031
  }
3218
4032
  return inferLoopStatusFromMessages(messages);
3219
4033
  }
3220
- function isRecord5(value) {
3221
- return typeof value === "object" && value !== null && !Array.isArray(value);
3222
- }
3223
4034
  function toSelectionMap(value) {
3224
- if (!isRecord5(value)) return {};
4035
+ if (!isRecord(value)) return {};
3225
4036
  const entries = Object.entries(value).map(([questionKey, optionIndexes]) => {
3226
4037
  if (!Array.isArray(optionIndexes)) return null;
3227
4038
  const parsedIndexes = optionIndexes.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item));
@@ -3230,7 +4041,7 @@ function toSelectionMap(value) {
3230
4041
  return Object.fromEntries(entries);
3231
4042
  }
3232
4043
  function toCustomMap(value) {
3233
- if (!isRecord5(value)) return {};
4044
+ if (!isRecord(value)) return {};
3234
4045
  const entries = Object.entries(value).filter(([, text]) => typeof text === "string").map(([questionKey, text]) => [Number(questionKey), text]);
3235
4046
  return Object.fromEntries(entries);
3236
4047
  }
@@ -3239,7 +4050,7 @@ function extractAskAnswers(turns) {
3239
4050
  for (const turn of turns) {
3240
4051
  for (const block of turn.blocks) {
3241
4052
  if (block.type !== "ask_user_answer" || typeof block.tool_call_id !== "string") continue;
3242
- if (!isRecord5(block.content)) continue;
4053
+ if (!isRecord(block.content)) continue;
3243
4054
  const note = typeof block.content.note === "string" ? block.content.note.trim() : "";
3244
4055
  answers[block.tool_call_id] = {
3245
4056
  selections: toSelectionMap(block.content.selections),
@@ -3255,9 +4066,9 @@ function parentForkToolCallIdFromTurn(turn) {
3255
4066
  return turn.parent_fork_tool_call_id;
3256
4067
  }
3257
4068
  for (const block of turn.blocks) {
3258
- if (block.type !== "system_notification" || !isRecord5(block.content)) continue;
4069
+ if (block.type !== "system_notification" || !isRecord(block.content)) continue;
3259
4070
  const metadata = block.content.metadata;
3260
- if (!isRecord5(metadata)) continue;
4071
+ if (!isRecord(metadata)) continue;
3261
4072
  const parentId = metadata.parent_fork_tool_call_id;
3262
4073
  if (typeof parentId === "string" && parentId.length > 0) return parentId;
3263
4074
  }
@@ -3274,16 +4085,16 @@ function buildMessageContent2(turn) {
3274
4085
  }
3275
4086
  function workspaceNotificationContent(turn) {
3276
4087
  const block = turn.blocks.find(
3277
- (candidate) => candidate.type === "system_notification" && isRecord5(candidate.content) && candidate.content.notification_type === "workspace_change"
4088
+ (candidate) => candidate.type === "system_notification" && isRecord(candidate.content) && candidate.content.notification_type === "workspace_change"
3278
4089
  );
3279
- if (!block || !isRecord5(block.content)) return "";
4090
+ if (!block || !isRecord(block.content)) return "";
3280
4091
  const title = typeof block.content.title === "string" ? block.content.title.trim() : "";
3281
4092
  const detail = typeof block.content.detail === "string" ? block.content.detail.trim() : "";
3282
4093
  return [title ? `**${title}**` : "", detail].filter(Boolean).join("\n\n");
3283
4094
  }
3284
4095
  function askUserAnswerContent(turn) {
3285
4096
  const answerBlock = turn.blocks.find((block) => block.type === "ask_user_answer");
3286
- if (!answerBlock || !isRecord5(answerBlock.content)) return null;
4097
+ if (!answerBlock || !isRecord(answerBlock.content)) return null;
3287
4098
  const answer = answerBlock.content.answer;
3288
4099
  return typeof answer === "string" && answer.trim().length > 0 ? answer : null;
3289
4100
  }
@@ -3486,21 +4297,58 @@ function orderTurns(turns) {
3486
4297
  return turns;
3487
4298
  }
3488
4299
  function reconcileOptimisticUserTurns(currentTurns, incomingTurns) {
3489
- const currentById = new Map(currentTurns.map((turn) => [turn.turn_id, turn]));
3490
- const optimisticUserTurn = currentTurns.find(
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(
3491
4308
  (turn) => turn.role === "user" && (turn.kind ?? "message") === "message" && turn.turn_id.startsWith("local-user-")
3492
4309
  );
3493
- let optimisticUserMatched = false;
3494
- return incomingTurns.map((turn) => {
3495
- const current = currentById.get(turn.turn_id);
4310
+ const optimisticByRequestId = new Map(
4311
+ optimisticUserTurns.map((turn) => [
4312
+ turn.turn_id.slice("local-user-".length),
4313
+ turn
4314
+ ])
4315
+ );
4316
+ const currentTurnIds = new Set(currentTurns.map((turn) => turn.turn_id));
4317
+ const claimedOptimisticIds = /* @__PURE__ */ new Set();
4318
+ const reconciled = preservedTurns.map((turn) => {
4319
+ const exactOptimistic = typeof turn.client_request_id === "string" ? optimisticByRequestId.get(turn.client_request_id) : void 0;
4320
+ if (exactOptimistic && !claimedOptimisticIds.has(exactOptimistic.turn_id) && isUserConfirmation(turn)) {
4321
+ claimedOptimisticIds.add(exactOptimistic.turn_id);
4322
+ return { ...turn, client_render_id: exactOptimistic.turn_id };
4323
+ }
4324
+ const fallbackOptimistic = optimisticUserTurns.find(
4325
+ (candidate) => !claimedOptimisticIds.has(candidate.turn_id)
4326
+ );
4327
+ if (allowLiveFallback && typeof turn.client_request_id !== "string" && fallbackOptimistic && isUserConfirmation(turn) && !turn.turn_id.startsWith("local-user-") && !currentTurnIds.has(turn.turn_id)) {
4328
+ claimedOptimisticIds.add(fallbackOptimistic.turn_id);
4329
+ return { ...turn, client_render_id: fallbackOptimistic.turn_id };
4330
+ }
4331
+ return turn;
4332
+ });
4333
+ const result = claimedOptimisticIds.size === 0 ? reconciled : reconciled.filter((turn) => !claimedOptimisticIds.has(turn.turn_id));
4334
+ if (!keepUnclaimedOptimistic) return result;
4335
+ const resultIds = new Set(result.map((turn) => turn.turn_id));
4336
+ const unclaimedOptimisticTurns = optimisticUserTurns.filter(
4337
+ (turn) => !claimedOptimisticIds.has(turn.turn_id) && !resultIds.has(turn.turn_id)
4338
+ );
4339
+ return unclaimedOptimisticTurns.length === 0 ? result : orderTurns([...result, ...unclaimedOptimisticTurns]);
4340
+ }
4341
+ function isUserConfirmation(turn) {
4342
+ return turn.role === "user" && (turn.kind ?? "message") === "message" || turn.blocks.some((block) => block.type === "ask_user_answer");
4343
+ }
4344
+ function preserveClientRenderIds(currentTurns, incomingTurns) {
4345
+ const currentById = new Map(currentTurns.map((turn) => [turn.turn_id, turn]));
4346
+ return incomingTurns.map((turn) => {
4347
+ const current = currentById.get(turn.turn_id);
3496
4348
  if (current) {
3497
4349
  const currentRenderId = current.client_render_id;
3498
4350
  return turn.client_render_id || !currentRenderId ? turn : { ...turn, client_render_id: currentRenderId };
3499
4351
  }
3500
- if (!optimisticUserMatched && optimisticUserTurn && turn.role === "user" && (turn.kind ?? "message") === "message" && !turn.turn_id.startsWith("local-user-")) {
3501
- optimisticUserMatched = true;
3502
- return { ...turn, client_render_id: optimisticUserTurn.turn_id };
3503
- }
3504
4352
  return turn;
3505
4353
  });
3506
4354
  }
@@ -3515,7 +4363,7 @@ function areAgentLoopsEqual(left, right) {
3515
4363
  });
3516
4364
  }
3517
4365
  function withTurns(state, turns) {
3518
- const orderedTurns = orderTurns(reconcileOptimisticUserTurns(state.turns, turns));
4366
+ const orderedTurns = orderTurns(reconcileHistoricalUserTurns(state.turns, turns));
3519
4367
  const { messages, agentLoops, activeCompaction } = materialize(orderedTurns);
3520
4368
  const isWaitingForInput = orderedTurns.some(
3521
4369
  (turn) => turn.tool_calls.some((toolCall) => toolCall.status === "awaiting_answer")
@@ -3556,14 +4404,22 @@ function withTurns(state, turns) {
3556
4404
  status: isWaitingForInput ? "waiting_for_input" : state.status
3557
4405
  };
3558
4406
  }
4407
+ function withQueue(state, snapshot2) {
4408
+ if (!shouldApplySnapshot(state.queue, snapshot2)) return state;
4409
+ return { ...state, queue: snapshot2 };
4410
+ }
3559
4411
  var localTurnCounter = 0;
3560
- function addUserMessage(state, content) {
4412
+ function addUserMessage(state, content, clientRequestId) {
3561
4413
  localTurnCounter += 1;
3562
- const turnId = `local-user-${Date.now()}-${localTurnCounter}`;
4414
+ const turnId = `local-user-${clientRequestId ?? `${Date.now()}-${localTurnCounter}`}`;
3563
4415
  const optimisticTurn = {
3564
4416
  id: turnId,
3565
4417
  sequence: Math.max(0, ...state.turns.map((turn) => turn.sequence)) + 1,
3566
4418
  turn_id: turnId,
4419
+ // 乐观态就自赋,跟服务端确认后 reconcileUserTurns 补上的
4420
+ // client_render_id(= 乐观 turn 自己的 turn_id)保持同一个值,
4421
+ // 消费方才能拿它当"确认前后不变"的稳定标识使用。
4422
+ client_render_id: turnId,
3567
4423
  loop_id: "root",
3568
4424
  role: "user",
3569
4425
  status: "streaming",
@@ -3578,10 +4434,9 @@ function addUserMessage(state, content) {
3578
4434
  function upsertTurn(state, turn) {
3579
4435
  let existing = [...state.turns];
3580
4436
  const [nextTurn] = reconcileOptimisticUserTurns(existing, [turn]);
3581
- if (nextTurn.client_render_id && turn.role === "user") {
3582
- existing = existing.filter(
3583
- (item) => !(item.role === "user" && item.turn_id.startsWith("local-user-"))
3584
- );
4437
+ const claimedOptimisticId = nextTurn.client_render_id;
4438
+ if (claimedOptimisticId?.startsWith("local-user-")) {
4439
+ existing = existing.filter((item) => item.turn_id !== claimedOptimisticId);
3585
4440
  }
3586
4441
  const index = existing.findIndex((item) => item.turn_id === nextTurn.turn_id);
3587
4442
  if (index >= 0) {
@@ -3589,7 +4444,7 @@ function upsertTurn(state, turn) {
3589
4444
  } else {
3590
4445
  existing.push(nextTurn);
3591
4446
  }
3592
- return withTurns(state, existing);
4447
+ return withTurns({ ...state, turns: existing }, existing);
3593
4448
  }
3594
4449
  function applyTurnPatch(state, patch) {
3595
4450
  const turn = patch.data.turn;
@@ -3600,13 +4455,7 @@ function applyTurnPatch(state, patch) {
3600
4455
  if (lastSequence !== null && patch.sequence <= lastSequence) {
3601
4456
  return state;
3602
4457
  }
3603
- const nextTurn = { ...turn, sequence: patch.sequence };
3604
- if (index >= 0) {
3605
- existing[index] = nextTurn;
3606
- } else {
3607
- existing.push(nextTurn);
3608
- }
3609
- return withTurns(state, existing);
4458
+ return upsertTurn(state, { ...turn, sequence: patch.sequence });
3610
4459
  }
3611
4460
  function addErrorMessage(state, content) {
3612
4461
  const anchorTurnId = state.turns[state.turns.length - 1]?.turn_id ?? null;
@@ -3636,21 +4485,49 @@ function markStreamingTurns(state, turnStatus, toolCallStatus) {
3636
4485
  return withTurns(state, turns);
3637
4486
  }
3638
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
+
3639
4498
  // src/session/agent-session.ts
3640
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";
3641
- var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
3642
- function isRecord6(value) {
3643
- 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}`;
3644
4510
  }
4511
+ var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
3645
4512
  function isOomText(value) {
3646
4513
  return typeof value === "string" && OOM_KEYWORDS_RE.test(value);
3647
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
+ }
3648
4525
  function parseJsonRecord(value) {
3649
- if (isRecord6(value)) return value;
4526
+ if (isRecord(value)) return value;
3650
4527
  if (typeof value !== "string") return null;
3651
4528
  try {
3652
4529
  const parsed = JSON.parse(value);
3653
- return isRecord6(parsed) ? parsed : null;
4530
+ return isRecord(parsed) ? parsed : null;
3654
4531
  } catch {
3655
4532
  return null;
3656
4533
  }
@@ -3676,6 +4553,21 @@ function isOomResult(value) {
3676
4553
  function isToolBridgeContent(value) {
3677
4554
  return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.action === "string";
3678
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
+ }
3679
4571
  function sessionStatusFromChatEnd(status) {
3680
4572
  if (status === "paused") return "waiting_for_input";
3681
4573
  if (status === "interrupted") return "interrupted";
@@ -3691,14 +4583,6 @@ function extractTerminalChatEndStatus(events) {
3691
4583
  }
3692
4584
  return null;
3693
4585
  }
3694
- function hasChatRunEvent2(events) {
3695
- return events.some(
3696
- (event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
3697
- );
3698
- }
3699
- function isUiMetaLike(value) {
3700
- return isRecord6(value) && ("resourceHTML" in value || "resourceUri" in value || "resourceURI" in value);
3701
- }
3702
4586
  var AgentSession = class _AgentSession {
3703
4587
  sessionId;
3704
4588
  state;
@@ -3716,15 +4600,50 @@ var AgentSession = class _AgentSession {
3716
4600
  // 状态并临时设置 replay source,禁止并发,否则较早操作的 finally 会清掉
3717
4601
  // 较晚操作仍在使用的来源上下文。
3718
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;
3719
4616
  // 回放控制请求排成一条队列。连着点"5x"再点"退出回放"时,并发发出去的两个
3720
4617
  // 请求响应可能乱序回来,晚到的旧值会把已经退出的会话又显示成回放中;用
3721
4618
  // 序号丢弃过期响应又会在失败时留下中间态。串行执行就没有这些情况:每个
3722
4619
  // 响应都是当时最新的,失败也只影响自己。
3723
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;
3724
4629
  pendingReplayMessage = null;
3725
4630
  pendingReplayMode;
3726
4631
  pendingReplayKbIds;
3727
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;
3728
4647
  subscriptionReplaySource = null;
3729
4648
  disposed = false;
3730
4649
  getAppContext = null;
@@ -3736,11 +4655,19 @@ var AgentSession = class _AgentSession {
3736
4655
  seenToolCallIds = /* @__PURE__ */ new Set();
3737
4656
  finishedToolCallIds = /* @__PURE__ */ new Set();
3738
4657
  emittedMessageTurnIds = /* @__PURE__ */ new Set();
3739
- constructor(sessionId, runtime, connection) {
4658
+ constructor(sessionId, runtime, connection, options = {}) {
3740
4659
  this.sessionId = sessionId;
3741
4660
  this.runtime = runtime;
3742
4661
  this.state = { ...createInitialSessionState(sessionId), connection };
4662
+ this.isStatic = options.staticHistory === true;
3743
4663
  }
4664
+ /**
4665
+ * 静态历史(分享页 / `fromTurns`):没有目录接口,也**没有**"目录还没到位"这个中间态。
4666
+ *
4667
+ * 宿主据此决定要不要拿已加载窗口当兜底条目——真实会话在 timeline 被重写后,窗口里还是
4668
+ * 旧分支的发言,拿它兜底会把已经失效的 entry id 重新摆到轨道上,点了必然找不到。
4669
+ */
4670
+ isStatic;
3744
4671
  /** 从一份静态 turn 快照构造只展示历史的会话(例如公开分享页)。 */
3745
4672
  static fromTurns(sessionId, turns) {
3746
4673
  const readonlyError = () => {
@@ -3752,7 +4679,10 @@ var AgentSession = class _AgentSession {
3752
4679
  emitEvent: readonlyError,
3753
4680
  emitWithAck: async () => readonlyError(),
3754
4681
  ensureConnected: readonlyError,
3755
- fetchTurns: async () => turns,
4682
+ fetchTurnsPage: async () => ({ turns, nextBefore: null, currentMode: "executing" }),
4683
+ // 静态会话(分享页)没有会话目录接口,宿主用已加载的历史兜底条目。
4684
+ fetchTurnIndex: async () => ({ sessionId, items: [] }),
4685
+ fetchCommands: async () => [],
3756
4686
  // 静态历史没有回放身份可言,给个空详情即可,不要抛错——_bootstrap
3757
4687
  // 不会跑到这里,但保持只读语义一致。
3758
4688
  fetchSessionInfo: async () => ({ replay_state: null }),
@@ -3763,7 +4693,8 @@ var AgentSession = class _AgentSession {
3763
4693
  attachAppCli: async () => readonlyError(),
3764
4694
  onDispose: () => true
3765
4695
  },
3766
- "connected"
4696
+ "connected",
4697
+ { staticHistory: true }
3767
4698
  );
3768
4699
  session.state = {
3769
4700
  ...withTurns(session.state, turns),
@@ -3776,6 +4707,101 @@ var AgentSession = class _AgentSession {
3776
4707
  getState() {
3777
4708
  return this.state;
3778
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
+ }
3779
4805
  /** 订阅状态变化。返回取消订阅函数。 */
3780
4806
  subscribe(listener) {
3781
4807
  this.listeners.add(listener);
@@ -3828,7 +4854,7 @@ var AgentSession = class _AgentSession {
3828
4854
  /** 发送一条消息。字符串或多模态内容均可。 */
3829
4855
  async send(content, options = {}) {
3830
4856
  this.runtime.ensureConnected();
3831
- 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) {
3832
4858
  this.update(
3833
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")
3834
4860
  );
@@ -3841,9 +4867,125 @@ var AgentSession = class _AgentSession {
3841
4867
  this.sendPending = false;
3842
4868
  }
3843
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
+ }
3844
4986
  async sendUnlocked(content, options) {
3845
4987
  await this.ensureIdleBeforeSend();
3846
- if (this.state.isStreaming || this.state.activeCompaction || this.manualCompactionPending) {
4988
+ if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.manualCompactionPending) {
3847
4989
  this.update(
3848
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")
3849
4991
  );
@@ -3853,7 +4995,7 @@ var AgentSession = class _AgentSession {
3853
4995
  if (this.getAppContext) {
3854
4996
  try {
3855
4997
  const rawContext = await this.getAppContext();
3856
- if (!isRecord6(rawContext)) {
4998
+ if (!isRecord(rawContext)) {
3857
4999
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
3858
5000
  }
3859
5001
  const serialized = JSON.stringify(rawContext);
@@ -3861,7 +5003,7 @@ var AgentSession = class _AgentSession {
3861
5003
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE\u53EF\u5E8F\u5217\u5316\u7684 JSON \u5BF9\u8C61");
3862
5004
  }
3863
5005
  const normalized = JSON.parse(serialized);
3864
- if (!isRecord6(normalized)) {
5006
+ if (!isRecord(normalized)) {
3865
5007
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
3866
5008
  }
3867
5009
  appContext = normalized;
@@ -3871,15 +5013,18 @@ var AgentSession = class _AgentSession {
3871
5013
  return false;
3872
5014
  }
3873
5015
  }
5016
+ const wasStreaming = this.state.isStreaming;
3874
5017
  const askUserAnswer = options.askUserAnswer;
3875
5018
  let optimisticTurnId = null;
5019
+ const clientRequestId = createClientRequestId();
3876
5020
  let optimisticAskAnswer;
3877
5021
  let previousAskAnswer;
3878
5022
  this.update((state) => {
3879
- const next = addUserMessage(state, content);
5023
+ const next = addUserMessage(state, content, clientRequestId);
3880
5024
  optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
3881
5025
  return next;
3882
5026
  });
5027
+ if (optimisticTurnId) this.markOptimisticTurnPending(optimisticTurnId);
3883
5028
  this.update((s) => ({ ...s, isStreaming: true }));
3884
5029
  if (askUserAnswer && typeof askUserAnswer.tool_call_id === "string") {
3885
5030
  const { tool_call_id, ...rest } = askUserAnswer;
@@ -3898,6 +5043,7 @@ var AgentSession = class _AgentSession {
3898
5043
  const payload = {
3899
5044
  session_id: this.sessionId,
3900
5045
  message: content,
5046
+ client_request_id: clientRequestId,
3901
5047
  mode: options.mode,
3902
5048
  askuser_answer: askUserAnswer,
3903
5049
  model: options.model,
@@ -3905,48 +5051,90 @@ var AgentSession = class _AgentSession {
3905
5051
  whatif: options.whatif,
3906
5052
  replay_decision: options.replayDecision,
3907
5053
  app_context: appContext,
3908
- kb_ids: options.kbIds && options.kbIds.length > 0 ? options.kbIds : void 0
5054
+ kb_ids: options.kbIds,
5055
+ // 运行中提交不必由客户端挑命令:服务端按会话运行状态决定直接执行还是入队。
5056
+ queue_when_running: options.queueWhenRunning === true
3909
5057
  };
3910
5058
  try {
3911
5059
  await this.ensureJoined();
3912
5060
  } catch (error) {
5061
+ const failure = describeSessionError(error, "\u4F1A\u8BDD\u8BA2\u9605\u5931\u8D25\uFF0C\u6D88\u606F\u672A\u53D1\u9001");
3913
5062
  this.rollbackOptimisticSend(
3914
5063
  optimisticTurnId,
3915
5064
  askUserAnswer,
3916
5065
  optimisticAskAnswer,
3917
5066
  previousAskAnswer,
3918
- 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
3919
5070
  );
3920
5071
  return false;
3921
5072
  }
5073
+ const waitStartedAt = this.systemErrorGeneration;
3922
5074
  try {
3923
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
+ }
3924
5091
  if (response?.status !== "accepted") {
3925
- throw new Error(
3926
- 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
3927
5095
  );
3928
5096
  }
3929
5097
  return true;
3930
5098
  } catch (error) {
5099
+ const failure = describeSessionError(error, "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
3931
5100
  this.rollbackOptimisticSend(
3932
5101
  optimisticTurnId,
3933
5102
  askUserAnswer,
3934
5103
  optimisticAskAnswer,
3935
5104
  previousAskAnswer,
3936
- 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
3937
5109
  );
3938
5110
  return false;
3939
5111
  }
3940
5112
  }
3941
- 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) {
3942
5132
  this.pendingReplayMessage = null;
3943
5133
  this.pendingReplayMode = void 0;
3944
5134
  this.pendingReplayKbIds = void 0;
3945
5135
  this.update((current) => {
3946
- let next = optimisticTurnId ? withTurns(
3947
- current,
3948
- current.turns.filter((turn) => turn.turn_id !== optimisticTurnId)
3949
- ) : 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;
3950
5138
  const toolCallId = askUserAnswer && typeof askUserAnswer.tool_call_id === "string" ? askUserAnswer.tool_call_id : null;
3951
5139
  if (toolCallId && next.askAnswers[toolCallId] === optimisticAskAnswer) {
3952
5140
  const askAnswers = { ...next.askAnswers };
@@ -3957,18 +5145,20 @@ var AgentSession = class _AgentSession {
3957
5145
  }
3958
5146
  next = { ...next, askAnswers };
3959
5147
  }
3960
- return addErrorMessage({ ...next, isStreaming: false }, message);
5148
+ return next;
3961
5149
  });
3962
- }
3963
- /** 智能体运行期间追加补充说明(不打断当前回复)。 */
3964
- append(text) {
3965
- this.runtime.ensureConnected();
3966
- this.runtime.emitEvent("chat:append", { session_id: this.sessionId, message: text });
3967
- this.update((s) => addUserMessage(s, text));
5150
+ if (optimisticTurnId) {
5151
+ this.pendingOptimisticTurnIds.delete(optimisticTurnId);
5152
+ this.liveRevisions.delete(optimisticTurnId);
5153
+ }
3968
5154
  }
3969
5155
  /** 停止当前回复。 */
3970
5156
  stop() {
3971
- 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
+ });
3972
5162
  }
3973
5163
  /** 手动触发上下文压缩。 */
3974
5164
  compact() {
@@ -3988,8 +5178,10 @@ var AgentSession = class _AgentSession {
3988
5178
  trigger: "manual"
3989
5179
  }
3990
5180
  }));
3991
- 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) => {
3992
5182
  this.manualCompactionPending = false;
5183
+ throw error;
5184
+ }).finally(() => {
3993
5185
  if (this.state.activeCompaction?.compaction_id === "pending-manual-compaction") {
3994
5186
  this.update((state) => ({ ...state, activeCompaction: null }));
3995
5187
  }
@@ -4028,10 +5220,20 @@ var AgentSession = class _AgentSession {
4028
5220
  if (this.disposed) return;
4029
5221
  if (!this.runtime.onDispose(this)) return;
4030
5222
  this.disposed = true;
5223
+ this.invalidateTimeline();
5224
+ this.invalidateOlderPaging();
5225
+ this.state = { ...this.state, nextBefore: null, loadingOlder: false };
4031
5226
  this.cancelPatchFlush();
4032
5227
  this.listeners.clear();
4033
5228
  this.emitter.clear();
4034
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
+ }
4035
5237
  }
4036
5238
  get isDisposed() {
4037
5239
  return this.disposed;
@@ -4042,23 +5244,37 @@ var AgentSession = class _AgentSession {
4042
5244
  if (this.state.connection === connection) return;
4043
5245
  this.update((s) => ({ ...s, connection }));
4044
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
+ }
4045
5256
  /** @internal socket 重连后由 Hub 调用:重新加入房间并补数。 */
4046
5257
  _rejoinAfterReconnect() {
4047
- const turnsBeforeRejoin = this.state.turns;
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();
4048
5263
  return this.enqueueSubscription(async () => {
4049
5264
  this.joined = false;
4050
5265
  this.joinPending = null;
4051
5266
  try {
4052
5267
  this.subscriptionReplaySource = "reconnect_replay";
4053
5268
  try {
4054
- await this.ensureJoined();
5269
+ await this.ensureJoined(reconnectLiveRevision);
4055
5270
  } finally {
4056
5271
  this.subscriptionReplaySource = null;
5272
+ this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
4057
5273
  }
4058
5274
  await Promise.all([
4059
- this.syncTurnsFromHistory({
4060
- clearOnEmpty: true,
4061
- preserveChangesSince: turnsBeforeRejoin
5275
+ this.refreshAuthoritativeHistory({
5276
+ liveRevisionAtStart: reconnectLiveRevision,
5277
+ modeRevisionAtStart: reconnectModeRevision
4062
5278
  }),
4063
5279
  this.syncReplayFromSession()
4064
5280
  ]);
@@ -4068,15 +5284,17 @@ var AgentSession = class _AgentSession {
4068
5284
  }
4069
5285
  /** @internal 首次连接:加载历史 + 加入房间。 */
4070
5286
  _bootstrap() {
5287
+ const commandRecoveryGeneration = this.beginCommandRecovery();
4071
5288
  return this.enqueueSubscription(async () => {
4072
- await this.syncTurnsFromHistory({ suppressErrors: false });
5289
+ await this.replaceFromHistory({ suppressErrors: false });
4073
5290
  this.subscriptionReplaySource = "bootstrap_replay";
4074
5291
  try {
4075
5292
  await this.ensureJoined();
4076
5293
  } finally {
4077
5294
  this.subscriptionReplaySource = null;
4078
5295
  }
4079
- await this.syncReplayFromSession();
5296
+ this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
5297
+ await Promise.all([this.syncReplayFromSession(), this.refreshQueue()]);
4080
5298
  });
4081
5299
  }
4082
5300
  /** 订阅生命周期串行执行;一次失败只由自己的调用方观察,不截断后续重连。 */
@@ -4142,8 +5360,14 @@ var AgentSession = class _AgentSession {
4142
5360
  /** @internal 历史回放通道(event_format: raw)。 */
4143
5361
  _handleTurnEvents(events, replayed = false) {
4144
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);
4145
5369
  const terminalChatEndStatus = extractTerminalChatEndStatus(events);
4146
- if (!terminalChatEndStatus && hasChatRunEvent2(events) && this.state.status !== "waiting_for_input") {
5370
+ if (!terminalChatEndStatus && hasChatRunEvent(events) && this.state.status !== "waiting_for_input") {
4147
5371
  this.markRunningIfIdle();
4148
5372
  }
4149
5373
  if (this.state.turns.length) {
@@ -4166,7 +5390,12 @@ var AgentSession = class _AgentSession {
4166
5390
  }
4167
5391
  /** @internal */
4168
5392
  _handleChatStart() {
4169
- 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
+ }));
4170
5399
  }
4171
5400
  /** @internal */
4172
5401
  _handleChatEnd(status) {
@@ -4174,14 +5403,31 @@ var AgentSession = class _AgentSession {
4174
5403
  this.completeChat(status);
4175
5404
  }
4176
5405
  /** @internal */
4177
- _handleSystemError(message) {
5406
+ _handleSystemError(message, code) {
4178
5407
  this.flushPatches();
4179
5408
  const finalMessage = isOomText(message) ? OOM_MESSAGE : message;
4180
5409
  this.update(
4181
- (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
+ )
4182
5419
  );
4183
5420
  this.recentChatEndAt = Date.now();
4184
- 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
+ });
4185
5431
  }
4186
5432
  /** @internal */
4187
5433
  _handleTaskUpdated(tasks) {
@@ -4201,6 +5447,8 @@ var AgentSession = class _AgentSession {
4201
5447
  this.update((s) => ({ ...s, replay }));
4202
5448
  }
4203
5449
  this.emitter.emit("sessionUpdated", {
5450
+ workspacePath: data.workspace_path,
5451
+ chatGroupId: data.chat_group_id,
4204
5452
  intent: data.intent,
4205
5453
  status: data.status,
4206
5454
  model: data.model,
@@ -4210,8 +5458,16 @@ var AgentSession = class _AgentSession {
4210
5458
  }
4211
5459
  /** @internal */
4212
5460
  _handleRewind(checkpointId) {
4213
- this.update((s) => ({ ...s, mode: null, errorMessage: null }));
4214
- 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();
4215
5471
  this.emitter.emit("rewind", { checkpointId });
4216
5472
  }
4217
5473
  /** @internal */
@@ -4230,7 +5486,7 @@ var AgentSession = class _AgentSession {
4230
5486
  message,
4231
5487
  mode,
4232
5488
  replay_decision: decision,
4233
- kb_ids: kbIds && kbIds.length > 0 ? kbIds : void 0
5489
+ kb_ids: kbIds
4234
5490
  },
4235
5491
  3e4
4236
5492
  ).then((response) => {
@@ -4271,9 +5527,9 @@ var AgentSession = class _AgentSession {
4271
5527
  detail,
4272
5528
  status,
4273
5529
  loopId,
4274
- metadata: isRecord6(notification.metadata) ? notification.metadata : void 0
5530
+ metadata: isRecord(notification.metadata) ? notification.metadata : void 0
4275
5531
  });
4276
- if (notificationType === "bg:started" && isRecord6(notification.metadata)) {
5532
+ if (notificationType === "bg:started" && isRecord(notification.metadata)) {
4277
5533
  const taskId = typeof notification.metadata.task_id === "string" ? notification.metadata.task_id : "";
4278
5534
  if (taskId) {
4279
5535
  this.emitter.emit("backgroundTask", {
@@ -4295,12 +5551,12 @@ var AgentSession = class _AgentSession {
4295
5551
  this.emitter.emit("artifact", event);
4296
5552
  }
4297
5553
  /** @internal 加入房间的应答里带 is_running,据此校准流式状态。 */
4298
- _applyRunningState(isRunning) {
5554
+ _applyRunningState(isRunning, liveRevisionAtJoin = this.liveRevision) {
4299
5555
  if (isRunning) {
4300
5556
  this.update((s) => ({ ...s, status: "running", isStreaming: true }));
4301
5557
  } else if (this.state.isStreaming) {
4302
5558
  this.update((s) => ({ ...s, isStreaming: false }));
4303
- void this.syncTurnsFromHistory();
5559
+ void this.refreshAuthoritativeHistory({ liveRevisionAtStart: liveRevisionAtJoin });
4304
5560
  }
4305
5561
  }
4306
5562
  // ---------------------------------------------------------------- 内部实现
@@ -4321,13 +5577,35 @@ var AgentSession = class _AgentSession {
4321
5577
  completeChat(status) {
4322
5578
  this.finishStreaming(status);
4323
5579
  this.recentChatEndAt = Date.now();
4324
- void this.syncTurnsFromHistory();
5580
+ void this.refreshAuthoritativeHistory();
4325
5581
  this.emitter.emit("chatEnd", { status });
4326
5582
  }
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
+ }
4327
5602
  applyTurn(turn, source = "live") {
4328
5603
  this.flushPatches();
5604
+ this.markTurnFresh(turn.turn_id);
5605
+ if (extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
4329
5606
  const previous = this.state;
4330
5607
  this.update((s) => upsertTurn(s, turn));
5608
+ this.pruneOptimisticLiveRevisions();
4331
5609
  this.emitTurnDerivedEvents(previous, turn, source);
4332
5610
  }
4333
5611
  schedulePatchFlush() {
@@ -4361,11 +5639,15 @@ var AgentSession = class _AgentSession {
4361
5639
  this.pendingTurnPatches.clear();
4362
5640
  for (const patch of patches) {
4363
5641
  const previous = this.state;
4364
- 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);
4365
5646
  const turn = patch.data.turn;
4366
- if (turn) {
4367
- this.emitTurnDerivedEvents(previous, turn);
4368
- }
5647
+ if (turn && extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
5648
+ this.setState(next);
5649
+ this.pruneOptimisticLiveRevisions();
5650
+ if (turn) this.emitTurnDerivedEvents(previous, turn);
4369
5651
  }
4370
5652
  if (this.patchFlushCancel) {
4371
5653
  this.patchFlushCancel();
@@ -4387,23 +5669,29 @@ var AgentSession = class _AgentSession {
4387
5669
  this.dispatchCommand(turn, block);
4388
5670
  continue;
4389
5671
  }
4390
- 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)) {
4391
5673
  const ui = block.content;
4392
5674
  if (ui.target !== "preview") continue;
4393
- const previewType = ui.resourceHTML ? "resource-html" : "resource-uri";
4394
- const previewContent = ui.resourceHTML ?? ui.resourceUri ?? ui.resourceURI;
4395
- if (!previewContent) continue;
4396
- const key = `${block.tool_call_id}:${previewType}`;
4397
- if (this.deliveredToolPreviews.get(key) === previewContent) continue;
4398
- 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);
4399
5685
  this.emitter.emit("toolPreview", {
4400
5686
  toolCallId: block.tool_call_id,
4401
- type: previewType,
4402
- content: previewContent,
4403
- 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 } : {}
4404
5692
  });
4405
5693
  }
4406
- if (block.type === "system_notification" && isRecord6(block.content)) {
5694
+ if (block.type === "system_notification" && isRecord(block.content)) {
4407
5695
  this._handleSystemNotification(turn.loop_id, block.content);
4408
5696
  }
4409
5697
  }
@@ -4440,17 +5728,36 @@ var AgentSession = class _AgentSession {
4440
5728
  dispatchCommand(turn, block) {
4441
5729
  if (!isToolBridgeContent(block.content)) return;
4442
5730
  const { action, payload } = block.content;
4443
- let payloadKey = "";
4444
- try {
4445
- payloadKey = JSON.stringify(payload ?? null);
4446
- } catch {
4447
- 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;
4448
5752
  }
4449
- 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;
4450
5757
  if (this.deliveredCommandKeys.has(deliveryKey)) return;
4451
5758
  this.deliveredCommandKeys.add(deliveryKey);
4452
- const meta = { toolCallId: block.tool_call_id ?? void 0 };
4453
- this.emitter.emit("command", { action, payload, toolCallId: meta.toolCallId });
5759
+ const meta = { toolCallId };
5760
+ this.emitter.emit("command", { action, payload, toolCallId });
4454
5761
  this.commands.dispatch(action, payload, meta);
4455
5762
  }
4456
5763
  notifySandboxOom(key) {
@@ -4459,29 +5766,181 @@ var AgentSession = class _AgentSession {
4459
5766
  this.update((s) => addErrorMessage(s, OOM_MESSAGE));
4460
5767
  this.emitter.emit("sandboxOom", { toolCallId: key });
4461
5768
  }
4462
- async syncTurnsFromHistory(options = {}) {
4463
- const { clearOnEmpty = false, suppressErrors = true, preserveChangesSince } = options;
4464
- const turnsAtRequestStart = this.state.turns;
4465
- try {
4466
- const turns = await this.runtime.fetchTurns(this.sessionId);
4467
- if (turns.length === 0 && !clearOnEmpty) return;
4468
- if (!this.joined) this.seedToolCallDeliveryBaseline(turns);
4469
- this.update((state) => {
4470
- const mergeBaseline = preserveChangesSince ?? turnsAtRequestStart;
4471
- if (state.turns === mergeBaseline) return withTurns(state, turns);
4472
- const historyById = new Map(turns.map((turn) => [turn.turn_id, turn]));
4473
- let merged = withTurns(state, turns);
4474
- for (const current of state.turns) {
4475
- const fromHistory = historyById.get(current.turn_id);
4476
- if (!fromHistory || current.sequence > fromHistory.sequence) {
4477
- merged = upsertTurn(merged, current);
4478
- }
5769
+ /**
5770
+ * 权威刷新(运行结束 / 重连补数):**按当前窗口第一条发言取窗口**,不再另记"用户想看哪段"。
5771
+ *
5772
+ * 窗口本身就是"用户在看哪段":首屏就是最新一页,向上翻页 / 定位之后首条就是用户滚到的位置。
5773
+ * 直接拿 `from_entry_id = 当前窗口第一条 turn_id` 去取,运行结束、重连补数、翻页之后的对账
5774
+ * 是同一条规则,不需要意图、锚点、已落地锚点这些状态;窗口首条已经不在分支上(404 / 409)时
5775
+ * 回落到最新一页。
5776
+ */
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;
4479
5826
  }
4480
- return merged;
4481
- });
4482
- this.replayCommandsFromTurns(turns);
4483
- } catch (error) {
4484
- if (!suppressErrors) throw error;
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;
4485
5944
  }
4486
5945
  }
4487
5946
  /** 首次订阅前的 REST 历史建立语义事件基线,随后同一工具的订阅回放不再投递。 */
@@ -4495,25 +5954,7 @@ var AgentSession = class _AgentSession {
4495
5954
  }
4496
5955
  }
4497
5956
  }
4498
- /**
4499
- * 把历史里的指令补投给宿主页面。
4500
- *
4501
- * 重开会话、断线补数时,地图高亮这类"页面当前状态"必须能恢复——
4502
- * 否则刷新一次页面,之前智能体让页面做的事就全丢了。
4503
- * 只回放指令:toolCall / message 这些事件如果按历史全量重放,
4504
- * 监听器会在挂载瞬间收到几百条陈年事件。
4505
- * dispatch 内部按 deliveredCommandKeys 去重,重复调用安全。
4506
- */
4507
- replayCommandsFromTurns(turns) {
4508
- for (const turn of turns) {
4509
- for (const block of turn.blocks) {
4510
- if (block.type === "tool_bridge" && isToolBridgeContent(block.content)) {
4511
- this.dispatchCommand(turn, block);
4512
- }
4513
- }
4514
- }
4515
- }
4516
- ensureJoined() {
5957
+ ensureJoined(liveRevisionBoundary = this.liveRevision) {
4517
5958
  if (this.joined) return Promise.resolve();
4518
5959
  if (this.joinPending) return this.joinPending;
4519
5960
  const join = this.runtime.emitWithAck(
@@ -4526,12 +5967,13 @@ var AgentSession = class _AgentSession {
4526
5967
  5e3
4527
5968
  ).then((response) => {
4528
5969
  if (response && response.ok === false) {
4529
- throw new Error(
4530
- 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
4531
5973
  );
4532
5974
  }
4533
5975
  this.joined = true;
4534
- this._applyRunningState(Boolean(response?.is_running));
5976
+ this._applyRunningState(Boolean(response?.is_running), liveRevisionBoundary);
4535
5977
  }).finally(() => {
4536
5978
  this.joinPending = null;
4537
5979
  });
@@ -4539,6 +5981,7 @@ var AgentSession = class _AgentSession {
4539
5981
  return join;
4540
5982
  }
4541
5983
  async ensureIdleBeforeSend() {
5984
+ if (this.state.isStreaming) return;
4542
5985
  if (Date.now() - this.recentChatEndAt > 3e3) return;
4543
5986
  const deadline = Date.now() + 3e3;
4544
5987
  while (Date.now() < deadline) {
@@ -4573,6 +6016,7 @@ var SessionHub = class {
4573
6016
  connectionListeners = /* @__PURE__ */ new Set();
4574
6017
  bound = false;
4575
6018
  hasConnected = false;
6019
+ authBusyReconnect = new AuthBusyReconnect();
4576
6020
  constructor(client) {
4577
6021
  this.client = client;
4578
6022
  }
@@ -4639,6 +6083,7 @@ var SessionHub = class {
4639
6083
  /** 手动重连(例如登录态变化后)。 */
4640
6084
  reconnect() {
4641
6085
  this.ensureBound();
6086
+ this.authBusyReconnect.cancel();
4642
6087
  const socket = this.client.socket();
4643
6088
  socket.disconnect();
4644
6089
  this.setConnection("connecting");
@@ -4646,11 +6091,13 @@ var SessionHub = class {
4646
6091
  }
4647
6092
  /** 断开连接(不清理 session 实例)。 */
4648
6093
  disconnect() {
6094
+ this.authBusyReconnect.cancel();
4649
6095
  this.client.socket().disconnect();
4650
6096
  this.setConnection("disconnected");
4651
6097
  }
4652
6098
  /** 清理当前身份下缓存的全部会话,避免切换登录身份后复用旧快照。 */
4653
6099
  disposeAll() {
6100
+ this.authBusyReconnect.cancel();
4654
6101
  this.bootstrapPending.clear();
4655
6102
  for (const session of [...this.sessions.values()]) {
4656
6103
  this.forceRelease(session);
@@ -4662,6 +6109,7 @@ var SessionHub = class {
4662
6109
  * turn:* / chat:* 事件都会静默丢弃。
4663
6110
  */
4664
6111
  _handleSocketReplaced() {
6112
+ this.authBusyReconnect.cancel();
4665
6113
  this.bound = false;
4666
6114
  this.setConnection("disconnected");
4667
6115
  if (this.sessions.size === 0) return;
@@ -4674,18 +6122,11 @@ var SessionHub = class {
4674
6122
  ;
4675
6123
  this.client.socket().emit(event, data);
4676
6124
  },
4677
- emitWithAck: (event, data, timeoutMs) => new Promise((resolve, reject) => {
4678
- ;
4679
- this.client.socket().timeout(timeoutMs).emit(event, data, (err, response) => {
4680
- if (err) {
4681
- reject(err);
4682
- return;
4683
- }
4684
- resolve(response);
4685
- });
4686
- }),
6125
+ emitWithAck: (event, data, timeoutMs) => this.client.requestCommand(event, data, timeoutMs),
4687
6126
  ensureConnected: () => this.ensureConnected(),
4688
- 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),
4689
6130
  fetchSessionInfo: (sessionId) => this.client.sessions.getSession(sessionId),
4690
6131
  updateReplay: (sessionId, payload) => this.client.sessions.updateReplaySession(sessionId, payload).then((result) => result.replay_state ?? null),
4691
6132
  listDir: (sessionId, dirPath) => this.client.sessions.listDir(sessionId, dirPath),
@@ -4713,6 +6154,9 @@ var SessionHub = class {
4713
6154
  if (this.sessions.get(session.sessionId) === session) {
4714
6155
  this.sessions.delete(session.sessionId);
4715
6156
  }
6157
+ if (this.sessions.size === 0) {
6158
+ this.authBusyReconnect.cancel();
6159
+ }
4716
6160
  ;
4717
6161
  this.client.socket().emit("session:unsubscribe", { session_id: session.sessionId });
4718
6162
  return true;
@@ -4721,10 +6165,13 @@ var SessionHub = class {
4721
6165
  forceRelease(session) {
4722
6166
  this.holdCounts.delete(session.sessionId);
4723
6167
  session.dispose();
6168
+ if (this.sessions.size === 0) {
6169
+ this.authBusyReconnect.cancel();
6170
+ }
4724
6171
  }
4725
6172
  ensureConnected() {
4726
6173
  const socket = this.client.socket();
4727
- if (socket.connected || socket.active) return;
6174
+ if (socket.connected || socket.active || this.authBusyReconnect.pending) return;
4728
6175
  this.setConnection("connecting");
4729
6176
  socket.connect();
4730
6177
  }
@@ -4756,17 +6203,35 @@ var SessionHub = class {
4756
6203
  this.bound = true;
4757
6204
  const socket = this.client.socket();
4758
6205
  socket.on("connect", () => {
6206
+ this.authBusyReconnect.cancel();
4759
6207
  const isReconnect = this.hasConnected;
4760
6208
  this.hasConnected = true;
4761
6209
  this.setConnection("connected");
4762
6210
  if (isReconnect) {
4763
6211
  for (const session of this.sessions.values()) {
4764
6212
  void session._rejoinAfterReconnect();
6213
+ void session.refreshQueue();
4765
6214
  }
4766
6215
  }
4767
6216
  });
4768
- socket.on("connect_error", () => this.setConnection("disconnected"));
4769
- 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
+ });
4770
6235
  socket.io.on("reconnect_attempt", () => this.setConnection("reconnecting"));
4771
6236
  socket.on("turn:start", (data) => {
4772
6237
  for (const session of this.route(data.session_id)) session._handleTurnStart(data);
@@ -4778,8 +6243,19 @@ var SessionHub = class {
4778
6243
  for (const session of this.route(data.session_id)) session._handleTurnEnd(data);
4779
6244
  });
4780
6245
  socket.on(
4781
- "turn:events",
6246
+ "turn:timing",
4782
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);
4783
6259
  for (const session of this.route(data.session_id)) {
4784
6260
  session._handleTurnEvents(data.events ?? [], data.replayed === true);
4785
6261
  }
@@ -4793,8 +6269,17 @@ var SessionHub = class {
4793
6269
  session._handleChatEnd(data?.status ?? "completed");
4794
6270
  }
4795
6271
  });
4796
- socket.on("system:error", (data) => {
4797
- 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);
4798
6283
  });
4799
6284
  socket.on(
4800
6285
  "system:notification",
@@ -4912,21 +6397,15 @@ function resolveAuthToken(options) {
4912
6397
  return token ? token : null;
4913
6398
  }
4914
6399
 
4915
- // src/version.ts
4916
- var SDK_NAME = "agent-client";
4917
- var SDK_VERSION = true ? "2610.0.0-rc.0" : "1.1.1";
4918
-
4919
6400
  // src/socket.ts
4920
- function withSdkIdentity(auth) {
4921
- return { ...auth ?? {}, sdk: SDK_NAME, sdk_version: SDK_VERSION };
4922
- }
4923
6401
  function createSocket(options) {
4924
6402
  const token = resolveAuthToken(options);
4925
6403
  return io(options.baseUrl, {
4926
6404
  path: options.path ?? "/socket.io",
4927
6405
  withCredentials: true,
6406
+ ...options.transports ? { transports: options.transports } : {},
4928
6407
  query: token ? { token } : void 0,
4929
- 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) ?? {},
4930
6409
  autoConnect: false
4931
6410
  });
4932
6411
  }
@@ -4945,6 +6424,14 @@ var BladeClient = class {
4945
6424
  computers;
4946
6425
  models;
4947
6426
  sessions;
6427
+ /** 按用户读取的插件目录与图标;没有会话时也能用。 */
6428
+ plugins;
6429
+ chatGroups;
6430
+ /**
6431
+ * 全站访问统计:匿名读写,不带 bearer 也不触发登录刷新。
6432
+ * 只有内置 Web 的同源页面使用,第三方宿主页面不会被自动统计。
6433
+ */
6434
+ siteVisits;
4948
6435
  /** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
4949
6436
  hub;
4950
6437
  constructor(options) {
@@ -4957,6 +6444,9 @@ var BladeClient = class {
4957
6444
  this.computers = new ComputersResource(this);
4958
6445
  this.models = new ModelsResource(this);
4959
6446
  this.sessions = new SessionsResource(this);
6447
+ this.plugins = new PluginsResource(this);
6448
+ this.chatGroups = new ChatGroupsResource(this);
6449
+ this.siteVisits = new SiteVisitsResource(this);
4960
6450
  this.hub = new SessionHub(this);
4961
6451
  if (this.options.token === void 0) {
4962
6452
  this.runtimeToken = readStoredToken(this.options.baseUrl);
@@ -5051,6 +6541,16 @@ var BladeClient = class {
5051
6541
  }
5052
6542
  return this.socketInstance;
5053
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
+ }
5054
6554
  async json(method, path, body) {
5055
6555
  const response = await this.fetch(method, path, {
5056
6556
  body: body === void 0 ? void 0 : JSON.stringify(body),
@@ -5115,7 +6615,7 @@ var BladeClient = class {
5115
6615
  signal: init.signal
5116
6616
  });
5117
6617
  if (response.status === 401 && !isRetry && this.shouldRefreshFor401(url)) {
5118
- const refreshed = await this.tryRefresh();
6618
+ const refreshed = await this.tryRefreshUnlessAborted(init.signal);
5119
6619
  if (refreshed) {
5120
6620
  return this.fetch(method, path, init, true);
5121
6621
  }
@@ -5127,7 +6627,7 @@ var BladeClient = class {
5127
6627
  return response;
5128
6628
  }
5129
6629
  buildAuthedUrl(path) {
5130
- const url = new URL(this.toBaseRelativePath(path), this.baseUrlWithTrailingSlash());
6630
+ const url = buildBackendUrl(this.options.baseUrl, path);
5131
6631
  const token = this.resolveTokenForUrl(url);
5132
6632
  if (token) {
5133
6633
  url.searchParams.set("token", token);
@@ -5139,6 +6639,7 @@ var BladeClient = class {
5139
6639
  if (!this.isSameBackendUrl(url)) {
5140
6640
  return result;
5141
6641
  }
6642
+ result.set("X-Blade-Client", `${SDK_NAME}/${SDK_VERSION}`);
5142
6643
  const token = this.resolveRestToken();
5143
6644
  if (token) {
5144
6645
  result.set("Authorization", `Bearer ${token}`);
@@ -5146,14 +6647,11 @@ var BladeClient = class {
5146
6647
  return result;
5147
6648
  }
5148
6649
  buildUrl(path) {
5149
- return new URL(this.toBaseRelativePath(path), this.baseUrlWithTrailingSlash());
6650
+ return buildBackendUrl(this.options.baseUrl, path);
5150
6651
  }
5151
6652
  baseUrlWithTrailingSlash() {
5152
6653
  return this.options.baseUrl.endsWith("/") ? this.options.baseUrl : `${this.options.baseUrl}/`;
5153
6654
  }
5154
- toBaseRelativePath(path) {
5155
- return path.startsWith("/") ? path.slice(1) : path;
5156
- }
5157
6655
  isSameBackendUrl(url) {
5158
6656
  const base = new URL(this.baseUrlWithTrailingSlash());
5159
6657
  if (url.origin !== base.origin) {
@@ -5172,6 +6670,27 @@ var BladeClient = class {
5172
6670
  hasExplicitBearerToken() {
5173
6671
  return Boolean(this.resolveRestToken());
5174
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
+ }
5175
6694
  async tryRefresh() {
5176
6695
  if (this.refreshPromise) {
5177
6696
  return this.refreshPromise;
@@ -5182,7 +6701,9 @@ var BladeClient = class {
5182
6701
  this.buildUrl(REFRESH_PATH).toString(),
5183
6702
  {
5184
6703
  method: "POST",
5185
- 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}` }
5186
6707
  }
5187
6708
  );
5188
6709
  if (!response.ok) {
@@ -5309,6 +6830,42 @@ function parseXhrHeaders(rawHeaders) {
5309
6830
  return headers;
5310
6831
  }
5311
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
+
5312
6869
  // src/platform-endpoints.ts
5313
6870
  var PLATFORM_SERVICE_NAMES = [
5314
6871
  "os",
@@ -5328,9 +6885,6 @@ function defaultDocumentBaseUrl() {
5328
6885
  if (typeof location !== "undefined" && location.origin) return `${location.origin}/`;
5329
6886
  return document.baseURI;
5330
6887
  }
5331
- function isRecord7(value) {
5332
- return typeof value === "object" && value !== null && !Array.isArray(value);
5333
- }
5334
6888
  function normalizeServiceUrl(value) {
5335
6889
  if (typeof value !== "string" || !value.trim()) return null;
5336
6890
  if (value.includes("?") || value.includes("#")) return null;
@@ -5344,7 +6898,7 @@ function normalizeServiceUrl(value) {
5344
6898
  }
5345
6899
  }
5346
6900
  function parsePlatformEndpoints(value) {
5347
- if (!isRecord7(value) || !isRecord7(value.services)) return null;
6901
+ if (!isRecord(value) || !isRecord(value.services)) return null;
5348
6902
  const services = {};
5349
6903
  for (const name of PLATFORM_SERVICE_NAMES) {
5350
6904
  if (!(name in value.services)) continue;
@@ -5492,14 +7046,20 @@ var Task = type2({
5492
7046
  });
5493
7047
  export {
5494
7048
  AgentSession,
7049
+ AgentSessionError,
7050
+ AuthBusyReconnect,
5495
7051
  BladeApiError,
5496
7052
  BladeClient,
7053
+ ChatGroupsResource,
5497
7054
  ClientProjectionBuilder,
5498
7055
  ComputersResource,
5499
7056
  DEFAULT_REPLAY_SPEED,
5500
7057
  EMPTY_PLATFORM_ENDPOINTS,
7058
+ EMPTY_SESSION_QUEUE,
5501
7059
  LayoutType,
5502
7060
  ModelsResource,
7061
+ PluginsResource,
7062
+ PollingBackoff,
5503
7063
  SDK_NAME,
5504
7064
  SDK_VERSION,
5505
7065
  SessionHub,
@@ -5509,9 +7069,20 @@ export {
5509
7069
  Task,
5510
7070
  TaskStatus,
5511
7071
  acceptedPostChatFollowupCompletesLatestRun,
7072
+ acknowledgeTurnEvents,
7073
+ authBusyRetryDelayMs,
5512
7074
  buildMessageContent,
7075
+ buildToolUiCardKey,
7076
+ canCancelQueuedMessage,
7077
+ canDeliverQueuedMessage,
7078
+ canEditQueuedMessage,
5513
7079
  canToggleComputer,
5514
7080
  chatErrorForDisplay,
7081
+ collectInlineToolUiCards,
7082
+ collectPreviewToolUiCards,
7083
+ computerDaemonVersion,
7084
+ computerOS,
7085
+ computerPlatformLabel,
5515
7086
  computerState,
5516
7087
  connectEmbedded,
5517
7088
  contentPreview,
@@ -5526,14 +7097,31 @@ export {
5526
7097
  getTextContent,
5527
7098
  groupAdjacentContextRuns,
5528
7099
  groupMessagesByLoop,
7100
+ hasChatRunEvent,
7101
+ inferToolStatus,
7102
+ isAppDevToolUiCard,
7103
+ isBackgroundWakeMessage,
5529
7104
  isCommandEnvelope,
7105
+ isEntryGoneError,
5530
7106
  isHiddenInternalMessage,
5531
7107
  isInboundEnvelope,
7108
+ isInternalStatusToolUiCard,
7109
+ isMcpAppData,
7110
+ isSessionQueueSnapshot,
7111
+ isToolUiCard,
5532
7112
  latestPostChatFollowup,
5533
7113
  loadPlatformEndpoints,
5534
7114
  normalizeMessageContent,
7115
+ parseQueueAck,
7116
+ prependOlder,
7117
+ queuePauseReasonLabel,
7118
+ queuedMessageStatusLabel,
7119
+ reconcileHistoricalUserTurns,
5535
7120
  reconcileOptimisticUserTurns,
7121
+ replaceWindow,
5536
7122
  resolveServiceUrl,
7123
+ resolveToolUiCardContent,
7124
+ shouldApplySnapshot,
5537
7125
  sortComputers,
5538
7126
  toReplaySnapshot,
5539
7127
  transformSlashCommand