@blade-hq/agent-client 2610.0.0-beta.4 → 2610.0.0-beta.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -214,6 +214,11 @@ var AuthResource = class {
214
214
  }
215
215
  };
216
216
 
217
+ // src/shared/type-guards.ts
218
+ function isRecord(value) {
219
+ return typeof value === "object" && value !== null && !Array.isArray(value);
220
+ }
221
+
217
222
  // src/resources/headless.ts
218
223
  var HeadlessError = class extends Error {
219
224
  constructor(message, detail) {
@@ -352,8 +357,7 @@ var HeadlessResource = class {
352
357
  socket.on("system:error", onSystemError);
353
358
  socket.emit("session:subscribe", {
354
359
  session_id,
355
- event_format: "raw",
356
- stream_tokens: this.client.options.streamTokens !== false
360
+ event_format: "raw"
357
361
  });
358
362
  socket.emit(
359
363
  "chat:send",
@@ -395,9 +399,6 @@ function errorMessageFromResult(result) {
395
399
  }
396
400
  return null;
397
401
  }
398
- function isRecord(value) {
399
- return typeof value === "object" && value !== null && !Array.isArray(value);
400
- }
401
402
  function ensureSocketConnected(socket, timeoutMs, detail) {
402
403
  if (socket.connected) {
403
404
  return Promise.resolve();
@@ -425,6 +426,66 @@ function ensureSocketConnected(socket, timeoutMs, detail) {
425
426
  });
426
427
  }
427
428
 
429
+ // src/resources/computers.ts
430
+ function computerState(computer) {
431
+ if (computer.is_primary) return "primary";
432
+ if (computer.enabled) return "enabled";
433
+ return computer.online ? "idle" : "offline";
434
+ }
435
+ function computerOS(computer) {
436
+ const os = (computer.os ?? "").toLowerCase();
437
+ if (os.startsWith("darwin") || os.startsWith("mac")) return "macos";
438
+ if (os.startsWith("windows")) return "windows";
439
+ if (os.startsWith("linux")) return "linux";
440
+ return "unknown";
441
+ }
442
+ function computerPlatformLabel(computer) {
443
+ const os = (computer.os ?? "").trim();
444
+ const arch = (computer.arch ?? "").trim();
445
+ if (os && arch) return `${os}/${arch}`;
446
+ return os || arch || "";
447
+ }
448
+ function computerDaemonVersion(computer) {
449
+ return (computer.daemon_version ?? "").trim();
450
+ }
451
+ function canToggleComputer(computer) {
452
+ return !computer.is_primary;
453
+ }
454
+ function sortComputers(computers) {
455
+ return [...computers].sort((a, b) => {
456
+ const byCreated = (a.created_at ?? "").localeCompare(b.created_at ?? "");
457
+ if (byCreated !== 0) return byCreated;
458
+ return a.label.localeCompare(b.label);
459
+ });
460
+ }
461
+ var ComputersResource = class {
462
+ constructor(client) {
463
+ this.client = client;
464
+ }
465
+ client;
466
+ /**
467
+ * 列出当前用户的全部电脑,带上"在不在线"和"这次会话能不能用"。
468
+ * 服务端只查库,电脑离线时同样返回,所以拿得到"它离线了"这个结论。
469
+ */
470
+ list(sessionId) {
471
+ return this.client.json(
472
+ "GET",
473
+ `/api/sessions/${encodeURIComponent(sessionId)}/computers`
474
+ );
475
+ }
476
+ /**
477
+ * 启动或停止一台电脑。只改这次会话的可用范围,不会碰远程机器,
478
+ * 电脑离线时同样能登记,等它上线就直接可用。
479
+ */
480
+ setEnabled(sessionId, computer, enabled) {
481
+ return this.client.json(
482
+ "PUT",
483
+ `/api/sessions/${encodeURIComponent(sessionId)}/computers/${encodeURIComponent(computer)}/enabled`,
484
+ { enabled }
485
+ );
486
+ }
487
+ };
488
+
428
489
  // src/resources/models.ts
429
490
  var ModelsResource = class {
430
491
  constructor(client) {
@@ -473,7 +534,10 @@ var SessionInfo = type({
473
534
  "disable_tools?": "string[]",
474
535
  "runtime_type?": "string | null",
475
536
  "daemon_id?": "string | null",
537
+ "agent_runtime_id?": "string | null",
476
538
  "workspace_path?": "string | null",
539
+ "ba_version?": "string | null",
540
+ "sandbox_version?": "string | null",
477
541
  "match?": "unknown"
478
542
  });
479
543
 
@@ -539,7 +603,7 @@ function upsertToolCall(state, toolCallId, toolName, displayName, args) {
539
603
  });
540
604
  }
541
605
  }
542
- function applyToolResult(state, toolCallId, result, status, durationMs, sourceLoop) {
606
+ function applyToolResult(state, toolCallId, result, status, durationMs, ranInParallel, sourceLoop) {
543
607
  let toolName = "";
544
608
  let displayName = "";
545
609
  let finalResult = result;
@@ -551,6 +615,7 @@ function applyToolResult(state, toolCallId, result, status, durationMs, sourceLo
551
615
  }
552
616
  const update = { status, result: finalResult };
553
617
  if (durationMs != null) update.duration_ms = durationMs;
618
+ if (ranInParallel != null) update.ran_in_parallel = ranInParallel;
554
619
  state.toolCalls[i] = { ...tc, ...update };
555
620
  toolName = tc.tool_name;
556
621
  displayName = tc.display_name;
@@ -732,11 +797,17 @@ function buildCompactionTurn(sequence, compactionId, loopId, status, data) {
732
797
  summary_preview: summaryPreview,
733
798
  summary_full: summaryFull,
734
799
  archived_count: archivedCount,
800
+ archived_files: Array.isArray(data.archived_files) ? data.archived_files.filter(
801
+ (item) => typeof item === "object" && item !== null && !Array.isArray(item)
802
+ ) : null,
803
+ archived_tool_calls: Array.isArray(data.archived_tool_calls) ? data.archived_tool_calls.filter(
804
+ (item) => typeof item === "object" && item !== null && !Array.isArray(item)
805
+ ) : null,
735
806
  tokens_before: tokensBefore,
736
807
  tokens_after: tokensAfter,
737
808
  saved_ratio: savedRatio,
738
809
  trigger: asTrigger(data.trigger),
739
- failure_reason: typeof data.reason === "string" && data.reason ? data.reason : null,
810
+ failure_reason: [data.failure_reason, data.fallback_reason, data.reason].find((value) => typeof value === "string" && value.length > 0) ?? null,
740
811
  fallback_applied: typeof data.fallback_applied === "boolean" ? data.fallback_applied : null
741
812
  };
742
813
  }
@@ -991,6 +1062,9 @@ function createTurnState(turnId, loopId, opts = {}) {
991
1062
  toolCalls: [],
992
1063
  usage: null,
993
1064
  responseDone: false,
1065
+ firstTokenMs: null,
1066
+ streamDurationMs: null,
1067
+ firstTokenDeliveryMs: null,
994
1068
  memoryRefs: opts.memoryRefs ?? null,
995
1069
  parentForkToolCallId: null
996
1070
  };
@@ -1009,6 +1083,9 @@ function snapshot(state, sequence, status) {
1009
1083
  model: state.model,
1010
1084
  usage: state.usage ? { ...state.usage } : null,
1011
1085
  duration_ms: Math.max(0, Date.now() - state.startedAt),
1086
+ first_token_ms: state.firstTokenMs,
1087
+ stream_duration_ms: state.streamDurationMs,
1088
+ first_token_delivery_ms: state.firstTokenDeliveryMs,
1012
1089
  started_at: new Date(state.startedAt).toISOString(),
1013
1090
  context_window: state.contextWindow,
1014
1091
  memory_refs: state.memoryRefs,
@@ -1023,10 +1100,130 @@ function toolStatusFromResult(result) {
1023
1100
  return inferToolStatus(text);
1024
1101
  }
1025
1102
 
1103
+ // src/schemas/context.ts
1104
+ var BUILTIN_CONTEXT_TITLES = {
1105
+ "agent-instructions": "\u9879\u76EE\u8BF4\u660E",
1106
+ "skill-catalog": "\u53EF\u7528\u80FD\u529B",
1107
+ "runtime-context": "\u5F53\u524D\u5DE5\u4F5C\u73AF\u5883",
1108
+ "workspace-context": "\u5F53\u524D\u5DE5\u4F5C\u533A\u73AF\u5883"
1109
+ };
1110
+ var BUILTIN_CONTEXT_DETAILS = {
1111
+ "agent-instructions": "\u8FD9\u4E9B\u8BF4\u660E\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u7406\u89E3\u5F53\u524D\u9879\u76EE\u548C\u5DE5\u4F5C\u8981\u6C42\u3002",
1112
+ "skill-catalog": "\u8FD9\u4E9B\u80FD\u529B\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u9009\u62E9\u5408\u9002\u7684\u65B9\u5F0F\u5B8C\u6210\u4EFB\u52A1\u3002",
1113
+ "runtime-context": "\u8FD9\u4E9B\u4FE1\u606F\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u4E86\u89E3\u5F53\u524D\u6587\u4EF6\u5939\u548C\u8FD0\u884C\u6761\u4EF6\u3002",
1114
+ "workspace-context": "\u8FD9\u4E9B\u4FE1\u606F\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u4E86\u89E3\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u3001\u6587\u4EF6\u548C\u8FD0\u884C\u73AF\u5883\u3002"
1115
+ };
1116
+ function nonEmptyString(value) {
1117
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1118
+ }
1119
+ function contextProjectionData(fields) {
1120
+ const contextKind = nonEmptyString(fields.context_kind);
1121
+ const contextKey = nonEmptyString(fields.context_key);
1122
+ const contextName = nonEmptyString(fields.context_name);
1123
+ const contextRevision = nonEmptyString(fields.context_revision);
1124
+ if (!contextKind || !contextKey || !contextName || !contextRevision || typeof fields.context_order !== "number" || !Number.isFinite(fields.context_order) || !fields.context_action) {
1125
+ return null;
1126
+ }
1127
+ return {
1128
+ context_kind: contextKind,
1129
+ context_key: contextKey,
1130
+ context_name: contextName,
1131
+ context_order: fields.context_order,
1132
+ context_action: fields.context_action,
1133
+ context_revision: contextRevision,
1134
+ display_title: nonEmptyString(fields.display_title),
1135
+ display_summary: nonEmptyString(fields.display_summary),
1136
+ sources: Array.isArray(fields.sources) ? fields.sources : []
1137
+ };
1138
+ }
1139
+ function getContextDisplayState(context) {
1140
+ const title = nonEmptyString(context.display_title) ?? BUILTIN_CONTEXT_TITLES[context.context_kind] ?? "\u5BF9\u8BDD\u80CC\u666F";
1141
+ const isRemoved = context.context_action === "removed";
1142
+ const summary = nonEmptyString(context.display_summary) ?? (isRemoved ? "\u5DF2\u505C\u6B62\u7528\u4E8E\u540E\u7EED\u5BF9\u8BDD" : context.context_action === "retained" ? "\u7EE7\u7EED\u7528\u4E8E\u540E\u7EED\u5BF9\u8BDD" : context.context_kind === "agent-instructions" ? "\u5DF2\u66F4\u65B0\u672C\u6B21\u5BF9\u8BDD\u4F7F\u7528\u7684\u9879\u76EE\u8BF4\u660E" : context.context_kind === "skill-catalog" ? "\u5DF2\u66F4\u65B0\u672C\u6B21\u53EF\u7528\u80FD\u529B" : context.context_kind === "runtime-context" ? "\u5DF2\u66F4\u65B0\u672C\u6B21\u5DE5\u4F5C\u73AF\u5883" : context.context_kind === "workspace-context" ? "\u5DF2\u66F4\u65B0\u672C\u6B21\u5DE5\u4F5C\u533A\u73AF\u5883" : "\u5DF2\u66F4\u65B0\u672C\u6B21\u5BF9\u8BDD\u80CC\u666F");
1143
+ const detail = isRemoved ? `${title}\u5DF2\u4E0D\u518D\u7528\u4E8E\u540E\u7EED\u5BF9\u8BDD\u3002` : BUILTIN_CONTEXT_DETAILS[context.context_kind] ?? "\u8FD9\u4E9B\u4FE1\u606F\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u7406\u89E3\u5F53\u524D\u4EFB\u52A1\u3002";
1144
+ return { title, summary, detail, isRemoved };
1145
+ }
1146
+ function groupAdjacentContextRuns(kinds) {
1147
+ const runs = [];
1148
+ let current = null;
1149
+ kinds.forEach((kind, index) => {
1150
+ if (kind === null) {
1151
+ current = null;
1152
+ return;
1153
+ }
1154
+ if (current === null) {
1155
+ current = [];
1156
+ runs.push(current);
1157
+ }
1158
+ current.push(index);
1159
+ });
1160
+ return runs;
1161
+ }
1162
+ function getContextGroupDisplayState(contexts) {
1163
+ const count = contexts.length;
1164
+ if (count === 1) {
1165
+ const single = getContextDisplayState(contexts[0]);
1166
+ return { title: single.title, summary: single.summary, count };
1167
+ }
1168
+ const removed = contexts.filter(
1169
+ (context) => context.context_action === "removed"
1170
+ ).length;
1171
+ const updated = count - removed;
1172
+ const parts = [];
1173
+ if (updated > 0) parts.push(`${updated} \u9879\u5DF2\u66F4\u65B0`);
1174
+ if (removed > 0) parts.push(`${removed} \u9879\u5DF2\u505C\u7528`);
1175
+ return {
1176
+ title: "\u5BF9\u8BDD\u80CC\u666F",
1177
+ summary: parts.join(" \xB7 ") || `${count} \u9879`,
1178
+ count
1179
+ };
1180
+ }
1181
+
1182
+ // src/shared/projection/context.ts
1183
+ function buildContextTurn(entry, sequence) {
1184
+ if (entry.kind !== "context" || !isRecord(entry.source) || !isRecord(entry.data)) {
1185
+ return null;
1186
+ }
1187
+ const display = isRecord(entry.data.display) ? entry.data.display : {};
1188
+ const message = isRecord(entry.message) ? entry.message : {};
1189
+ const context = contextProjectionData({
1190
+ context_kind: typeof entry.source.kind === "string" ? entry.source.kind : null,
1191
+ context_key: typeof entry.source.key === "string" ? entry.source.key : null,
1192
+ context_name: typeof entry.data.contribution_name === "string" ? entry.data.contribution_name : null,
1193
+ context_order: typeof entry.data.order === "number" ? entry.data.order : null,
1194
+ context_action: entry.data.action === "published" || entry.data.action === "retained" || entry.data.action === "removed" ? entry.data.action : null,
1195
+ context_revision: typeof entry.data.revision === "string" ? entry.data.revision : null,
1196
+ display_title: typeof display.title === "string" ? display.title : null,
1197
+ display_summary: typeof display.summary === "string" ? display.summary : null,
1198
+ sources: Array.isArray(display.sources) ? display.sources.filter(isRecord) : []
1199
+ });
1200
+ if (!context) return null;
1201
+ const turnId = typeof entry.id === "string" && entry.id ? entry.id : `context-${sequence}`;
1202
+ return {
1203
+ id: turnId,
1204
+ sequence,
1205
+ turn_id: turnId,
1206
+ loop_id: typeof entry.loop_name === "string" && entry.loop_name ? entry.loop_name : "root",
1207
+ kind: "context",
1208
+ role: "system",
1209
+ status: "completed",
1210
+ blocks: [],
1211
+ tool_calls: [],
1212
+ model: null,
1213
+ usage: null,
1214
+ duration_ms: 0,
1215
+ context_content: Object.hasOwn(message, "content") ? message.content : null,
1216
+ ...context
1217
+ };
1218
+ }
1219
+
1026
1220
  // src/shared/projection/builder.ts
1221
+ function acknowledgeTurnEvents(acknowledge) {
1222
+ acknowledge?.();
1223
+ }
1027
1224
  function hasChatRunEvent(events) {
1028
1225
  return events.some(
1029
- (event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
1226
+ (event) => event.type !== "llm:first_token:delivered" && event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
1030
1227
  );
1031
1228
  }
1032
1229
  function acceptedPostChatFollowupCompletesLatestRun(events, updates) {
@@ -1058,7 +1255,8 @@ function completedResponseText(content) {
1058
1255
  ).filter((part) => part.type === "text").map((part) => String(part.text ?? "")).join("");
1059
1256
  }
1060
1257
  function replaceTextContent(state, content) {
1061
- if (state.blocks.some((block) => block.type === "text")) return;
1258
+ const currentContent = state.blocks.filter((block) => block.type === "text").map((block) => String(block.content ?? "")).join("");
1259
+ if (currentContent === content) return;
1062
1260
  const replacement = { type: "text", content };
1063
1261
  const blocks = [];
1064
1262
  let inserted = false;
@@ -1078,10 +1276,36 @@ function replaceTextContent(state, content) {
1078
1276
  }
1079
1277
  state.blocks = blocks;
1080
1278
  }
1279
+ function applyResponseBlockOrder(state, value) {
1280
+ if (!Array.isArray(value)) return false;
1281
+ const ordered = [];
1282
+ for (const item of value) {
1283
+ if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
1284
+ const block = item;
1285
+ if (block.type === "text" || block.type === "thinking") {
1286
+ const content = String(block[block.type] ?? "");
1287
+ if (content) ordered.push({ type: block.type, content });
1288
+ continue;
1289
+ }
1290
+ if (block.type !== "tool_call") continue;
1291
+ const toolCallId = String(block.id ?? "");
1292
+ const toolBlock = state.blocks.find(
1293
+ (candidate) => candidate.type === "tool_use" && candidate.tool_call_id === toolCallId
1294
+ );
1295
+ if (toolBlock) ordered.push(toolBlock);
1296
+ }
1297
+ if (ordered.length === 0 && value.length > 0) return false;
1298
+ const unrelatedBlocks = state.blocks.filter(
1299
+ (block) => block.type !== "text" && block.type !== "thinking" && block.type !== "tool_use"
1300
+ );
1301
+ state.blocks = [...ordered, ...unrelatedBlocks];
1302
+ return true;
1303
+ }
1081
1304
  var ClientProjectionBuilder = class {
1082
1305
  activeTurns = /* @__PURE__ */ new Map();
1083
1306
  lastTurnIds = /* @__PURE__ */ new Map();
1084
1307
  latestTurns = /* @__PURE__ */ new Map();
1308
+ turnsById = /* @__PURE__ */ new Map();
1085
1309
  loopDescriptions = /* @__PURE__ */ new Map();
1086
1310
  pendingChildPauses = /* @__PURE__ */ new Map();
1087
1311
  pendingMemoryRefs = /* @__PURE__ */ new Map();
@@ -1117,6 +1341,8 @@ var ClientProjectionBuilder = class {
1117
1341
  return this.onMemoryInject(loopId, payload);
1118
1342
  case "memory:inject:none":
1119
1343
  return null;
1344
+ case "context:entry":
1345
+ return this.onContextEntry(payload);
1120
1346
  case "loop:turn":
1121
1347
  return this.onLoopTurn(loopId, payload);
1122
1348
  case "mode:change":
@@ -1140,6 +1366,25 @@ var ClientProjectionBuilder = class {
1140
1366
  }
1141
1367
  if (event.type.startsWith("compaction:"))
1142
1368
  return this.onCompaction(event.type, loopId, payload);
1369
+ if (event.type === "llm:first_token:delivered") {
1370
+ const targetTurnId = String(payload.turn_id ?? "");
1371
+ const active = this.activeTurns.get(loopId);
1372
+ if (active && (!targetTurnId || active.turnId === targetTurnId)) {
1373
+ this.applyFirstTokenDelivery(active, payload);
1374
+ return this.syncPatch(active);
1375
+ }
1376
+ const latest = targetTurnId ? this.turnsById.get(targetTurnId) : this.latestTurns.get(loopId);
1377
+ const duration = payload.first_token_delivery_ms;
1378
+ if (!latest || latest.role !== "assistant" || typeof duration !== "number" || duration < 0) {
1379
+ return null;
1380
+ }
1381
+ const turn = { ...latest, first_token_delivery_ms: duration };
1382
+ this.turnsById.set(turn.turn_id, turn);
1383
+ if (this.latestTurns.get(loopId)?.turn_id === turn.turn_id) {
1384
+ this.latestTurns.set(loopId, turn);
1385
+ }
1386
+ return [{ kind: "upsert", turn }];
1387
+ }
1143
1388
  const state = this.activeTurns.get(loopId);
1144
1389
  if (!state) {
1145
1390
  if (event.type === "chat:end") {
@@ -1155,16 +1400,28 @@ var ClientProjectionBuilder = class {
1155
1400
  state.toolCalls = [];
1156
1401
  state.usage = null;
1157
1402
  state.responseDone = false;
1403
+ state.firstTokenMs = null;
1404
+ state.streamDurationMs = null;
1405
+ state.firstTokenDeliveryMs = null;
1158
1406
  return this.syncPatch(state);
1159
1407
  case "llm:thinking:delta":
1408
+ this.applyFirstTokenDelivery(state, payload);
1160
1409
  return this.onDelta(state, "thinking", "", String(payload.content ?? ""));
1161
1410
  case "llm:content:delta":
1411
+ this.applyFirstTokenDelivery(state, payload);
1162
1412
  return this.onDelta(state, "text", "", String(payload.content ?? ""));
1163
1413
  case "llm:content:complete":
1164
1414
  replaceTextContent(state, String(payload.content ?? ""));
1165
1415
  return this.syncPatch(state);
1416
+ case "llm:content:done":
1417
+ this.applyStreamTiming(state, payload);
1418
+ return this.syncPatch(state);
1166
1419
  case "llm:tool_call:created":
1420
+ this.applyFirstTokenDelivery(state, payload);
1167
1421
  return this.onToolCallCreated(state, payload);
1422
+ case "llm:tool_call:arguments:delta":
1423
+ this.applyFirstTokenDelivery(state, payload);
1424
+ return this.onToolCallArgumentsDelta(state, payload);
1168
1425
  case "llm:response:done":
1169
1426
  return this.onResponseDone(state, loopId, payload);
1170
1427
  case "tool:result:delta":
@@ -1191,6 +1448,7 @@ var ClientProjectionBuilder = class {
1191
1448
  this.activeTurns.clear();
1192
1449
  this.lastTurnIds.clear();
1193
1450
  this.latestTurns.clear();
1451
+ this.turnsById.clear();
1194
1452
  this.loopDescriptions.clear();
1195
1453
  this.pendingChildPauses.clear();
1196
1454
  this.pendingMemoryRefs.clear();
@@ -1206,15 +1464,18 @@ var ClientProjectionBuilder = class {
1206
1464
  }
1207
1465
  _seedTurns(turns) {
1208
1466
  const latestTurns = /* @__PURE__ */ new Map();
1467
+ const turnsById = /* @__PURE__ */ new Map();
1209
1468
  for (const turn of turns) {
1210
1469
  this.seedSequence(turn.sequence);
1211
1470
  if (!isConversationTurn(turn)) continue;
1471
+ turnsById.set(turn.turn_id, turn);
1212
1472
  const current = latestTurns.get(turn.loop_id);
1213
1473
  if (!current || turn.sequence >= current.sequence) {
1214
1474
  latestTurns.set(turn.loop_id, turn);
1215
1475
  }
1216
1476
  }
1217
1477
  this.latestTurns = latestTurns;
1478
+ this.turnsById = turnsById;
1218
1479
  }
1219
1480
  // --- Event handlers ---
1220
1481
  onMemoryInject(loopId, payload) {
@@ -1232,6 +1493,11 @@ var ClientProjectionBuilder = class {
1232
1493
  }
1233
1494
  return null;
1234
1495
  }
1496
+ onContextEntry(payload) {
1497
+ if (!isRecord(payload.entry)) return null;
1498
+ const turn = buildContextTurn(payload.entry, this.nextSeq());
1499
+ return turn ? [{ kind: "upsert", turn }] : null;
1500
+ }
1235
1501
  onLoopTurn(loopId, payload) {
1236
1502
  let turnId = String(payload.turn_id ?? this.nextTurnId(`turn:${loopId}`));
1237
1503
  let memoryRefs = null;
@@ -1252,6 +1518,7 @@ var ClientProjectionBuilder = class {
1252
1518
  this.lastTurnIds.set(loopId, turnId);
1253
1519
  const turn = snapshot(state, this.nextSeq(), "streaming");
1254
1520
  this.latestTurns.set(loopId, turn);
1521
+ this.turnsById.set(turn.turn_id, turn);
1255
1522
  return [{ kind: "upsert", turn }];
1256
1523
  }
1257
1524
  onModeChange(loopId, payload) {
@@ -1288,11 +1555,11 @@ var ClientProjectionBuilder = class {
1288
1555
  loopId,
1289
1556
  toolCallId: optStr(payload.tool_call_id) ?? void 0,
1290
1557
  toolName: toolNameFromPayload(payload) ?? void 0,
1291
- workspacePath: isRecord2(payload.workspace_change) ? optStr(payload.workspace_change.to) ?? void 0 : void 0
1558
+ workspacePath: isRecord(payload.workspace_change) ? optStr(payload.workspace_change.to) ?? void 0 : void 0
1292
1559
  }
1293
1560
  ];
1294
1561
  const change = payload.workspace_change;
1295
- if (isRecord2(change)) {
1562
+ if (isRecord(change)) {
1296
1563
  const source = optStr(change.source) ?? "";
1297
1564
  const projectName = optStr(change.project_name) ?? "";
1298
1565
  const fromPath = optStr(change.from) ?? "";
@@ -1385,7 +1652,32 @@ var ClientProjectionBuilder = class {
1385
1652
  }
1386
1653
  const directTurn = this.activeTurns.get(loopId) ?? this.latestTurns.get(loopId);
1387
1654
  setToolCallStatus(directTurn, toolCallId, "done", null, null);
1388
- return this.collectLoopUpdates(loopId, parentLoopId);
1655
+ const updates = this.collectLoopUpdates(loopId, parentLoopId);
1656
+ const entryId = typeof payload.entry_id === "string" ? payload.entry_id : "";
1657
+ if (!entryId) return updates;
1658
+ const answerData = isRecord(payload.answer_data) ? payload.answer_data : {};
1659
+ const answerTurn = {
1660
+ id: entryId,
1661
+ sequence: this.nextSeq(),
1662
+ turn_id: entryId,
1663
+ loop_id: loopId,
1664
+ kind: "message",
1665
+ role: "assistant",
1666
+ status: "completed",
1667
+ blocks: [
1668
+ {
1669
+ type: "ask_user_answer",
1670
+ content: answerData,
1671
+ tool_call_id: toolCallId || null
1672
+ }
1673
+ ],
1674
+ tool_calls: [],
1675
+ model: null,
1676
+ usage: null,
1677
+ duration_ms: 0,
1678
+ client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
1679
+ };
1680
+ return [...updates, { kind: "upsert", turn: answerTurn }];
1389
1681
  }
1390
1682
  onUserMessage(_loopId, payload) {
1391
1683
  const entryId = String(payload.entry_id ?? this.nextTurnId("user"));
@@ -1402,7 +1694,8 @@ var ClientProjectionBuilder = class {
1402
1694
  tool_calls: [],
1403
1695
  model: null,
1404
1696
  usage: null,
1405
- duration_ms: 0
1697
+ duration_ms: 0,
1698
+ client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
1406
1699
  };
1407
1700
  this.latestTurns.set("root", turn);
1408
1701
  return [{ kind: "upsert", turn }];
@@ -1430,13 +1723,7 @@ var ClientProjectionBuilder = class {
1430
1723
  }
1431
1724
  onToolCallCreated(state, payload) {
1432
1725
  if ("arguments_delta" in payload) {
1433
- const delta = String(payload.arguments_delta ?? "");
1434
- const toolCallId2 = String(payload.id ?? "");
1435
- if (delta && toolCallId2) {
1436
- appendToolCallArguments(state, toolCallId2, delta);
1437
- return this.syncPatch(state);
1438
- }
1439
- return null;
1726
+ return this.onToolCallArgumentsDelta(state, payload);
1440
1727
  }
1441
1728
  const toolCallId = String(payload.id ?? "");
1442
1729
  const toolName = toolNameFromPayload(payload);
@@ -1445,16 +1732,21 @@ var ClientProjectionBuilder = class {
1445
1732
  upsertToolCall(state, toolCallId, toolName, displayName, args);
1446
1733
  return this.syncPatch(state);
1447
1734
  }
1735
+ onToolCallArgumentsDelta(state, payload) {
1736
+ const delta = String(payload.arguments_delta ?? "");
1737
+ const toolCallId = String(payload.id ?? "");
1738
+ if (!delta || !toolCallId) return null;
1739
+ appendToolCallArguments(state, toolCallId, delta);
1740
+ return this.syncPatch(state);
1741
+ }
1448
1742
  onResponseDone(state, loopId, payload) {
1449
1743
  state.responseDone = true;
1744
+ this.applyStreamTiming(state, payload);
1450
1745
  const model = optStr(payload.model);
1451
1746
  if (model) state.model = model;
1452
1747
  if (typeof payload.usage === "object" && payload.usage !== null) {
1453
1748
  state.usage = { ...payload.usage };
1454
1749
  }
1455
- if ("content" in payload) {
1456
- replaceTextContent(state, completedResponseText(payload.content));
1457
- }
1458
1750
  const toolCalls = payload.tool_calls;
1459
1751
  if (Array.isArray(toolCalls)) {
1460
1752
  for (const raw of toolCalls) {
@@ -1472,11 +1764,35 @@ var ClientProjectionBuilder = class {
1472
1764
  );
1473
1765
  }
1474
1766
  }
1475
- if (state.toolCalls.length === 0) {
1767
+ if (!applyResponseBlockOrder(state, payload.response_blocks) && "content" in payload) {
1768
+ replaceTextContent(state, completedResponseText(payload.content));
1769
+ }
1770
+ const isNativeProviderResponse = typeof payload.provider === "string" && payload.provider !== "";
1771
+ if (state.toolCalls.length === 0 || isNativeProviderResponse && state.toolCalls.every(
1772
+ (toolCall) => toolCall.status !== "pending" && toolCall.status !== "awaiting_answer"
1773
+ )) {
1476
1774
  return this.finalize(loopId, "completed");
1477
1775
  }
1478
1776
  return this.syncPatch(state);
1479
1777
  }
1778
+ applyFirstTokenDelivery(state, payload) {
1779
+ const firstTokenMs = payload.first_token_ms;
1780
+ if (state.firstTokenMs === null && typeof firstTokenMs === "number" && firstTokenMs >= 0) {
1781
+ state.firstTokenMs = Math.round(firstTokenMs);
1782
+ }
1783
+ const duration = payload.first_token_delivery_ms;
1784
+ if (state.firstTokenDeliveryMs === null && typeof duration === "number" && duration >= 0) {
1785
+ state.firstTokenDeliveryMs = duration;
1786
+ }
1787
+ }
1788
+ applyStreamTiming(state, payload) {
1789
+ if (typeof payload.first_token_ms === "number" && payload.first_token_ms >= 0) {
1790
+ state.firstTokenMs = Math.round(payload.first_token_ms);
1791
+ }
1792
+ if (typeof payload.stream_duration_ms === "number" && payload.stream_duration_ms >= 0) {
1793
+ state.streamDurationMs = Math.round(payload.stream_duration_ms);
1794
+ }
1795
+ }
1480
1796
  onToolResultDone(state, loopId, payload) {
1481
1797
  const toolCallId = String(payload.tool_call_id ?? "");
1482
1798
  const streamedResult = state.toolCalls.find((toolCall) => toolCall.id === toolCallId)?.result;
@@ -1484,9 +1800,11 @@ var ClientProjectionBuilder = class {
1484
1800
  const status = toolStatusFromResult(content);
1485
1801
  const rawDuration = payload.duration_ms;
1486
1802
  const durationMs = typeof rawDuration === "number" && rawDuration >= 0 ? Math.round(rawDuration) : null;
1803
+ const ranInParallel = typeof payload.ran_in_parallel === "boolean" ? payload.ran_in_parallel : null;
1487
1804
  const sl = sourceLoopFor(state.loopId, this.loopDescriptions);
1488
- applyToolResult(state, toolCallId, content, status, durationMs, sl);
1489
- if (this.shouldFinalizeAfterToolResults(state)) {
1805
+ applyToolResult(state, toolCallId, content, status, durationMs, ranInParallel, sl);
1806
+ const isNativeProviderResult = typeof payload.provider === "string" && payload.provider !== "";
1807
+ if (!isNativeProviderResult && this.shouldFinalizeAfterToolResults(state)) {
1490
1808
  return this.finalize(loopId, "completed");
1491
1809
  }
1492
1810
  return this.syncPatch(state);
@@ -1515,10 +1833,13 @@ var ClientProjectionBuilder = class {
1515
1833
  onToolBridge(state, payload) {
1516
1834
  const bridge = payload.bridge;
1517
1835
  if (typeof bridge !== "object" || bridge === null) return null;
1836
+ const toolCallId = String(payload.tool_call_id ?? "");
1837
+ if (!toolCallId || !state.toolCalls.some((toolCall) => toolCall.id === toolCallId)) return null;
1518
1838
  state.blocks.push({
1519
1839
  type: "tool_bridge",
1520
1840
  content: bridge,
1521
- tool_call_id: String(payload.tool_call_id ?? "") || null
1841
+ delivery_id: String(payload.delivery_id ?? "") || null,
1842
+ tool_call_id: toolCallId
1522
1843
  });
1523
1844
  return this.syncPatch(state);
1524
1845
  }
@@ -1527,6 +1848,11 @@ var ClientProjectionBuilder = class {
1527
1848
  const toolName = String(payload.tool_name ?? "");
1528
1849
  const displayName = this.displayNameResolver(toolName);
1529
1850
  upsertToolCall(state, toolCallId, toolName, displayName, "");
1851
+ const relatedToolCallId = String(payload.related_tool_call_id ?? "");
1852
+ if (relatedToolCallId && relatedToolCallId !== toolCallId) {
1853
+ const related = state.toolCalls.find((toolCall) => toolCall.id === relatedToolCallId);
1854
+ if (related) related.status = "awaiting_answer";
1855
+ }
1530
1856
  return this.syncPatch(state);
1531
1857
  }
1532
1858
  onChildPause(state, loopId, payload) {
@@ -1707,6 +2033,7 @@ var ClientProjectionBuilder = class {
1707
2033
  syncPatch(state) {
1708
2034
  const turn = snapshot(state, this.nextSeq(), "streaming");
1709
2035
  this.latestTurns.set(state.loopId, turn);
2036
+ this.turnsById.set(turn.turn_id, turn);
1710
2037
  this.lastTurnIds.set(state.loopId, turn.turn_id);
1711
2038
  return [{ kind: "upsert", turn }];
1712
2039
  }
@@ -1717,6 +2044,7 @@ var ClientProjectionBuilder = class {
1717
2044
  const turn = snapshot(state, this.nextSeq(), status);
1718
2045
  this.lastTurnIds.set(loopId, turn.turn_id);
1719
2046
  this.latestTurns.set(loopId, turn);
2047
+ this.turnsById.set(turn.turn_id, turn);
1720
2048
  return [{ kind: "upsert", turn }];
1721
2049
  }
1722
2050
  shouldFinalizeAfterToolResults(state) {
@@ -1754,9 +2082,6 @@ var ClientProjectionBuilder = class {
1754
2082
  return `${prefix}:${this.syntheticCounter}:${hex}`;
1755
2083
  }
1756
2084
  };
1757
- function isRecord2(value) {
1758
- return typeof value === "object" && value !== null && !Array.isArray(value);
1759
- }
1760
2085
  function optStr(value) {
1761
2086
  if (value == null || value === "") return null;
1762
2087
  return String(value);
@@ -1783,6 +2108,8 @@ function projectHistory(entries) {
1783
2108
  const latestAssistantByLoop = /* @__PURE__ */ new Map();
1784
2109
  const pendingChildPauses = /* @__PURE__ */ new Map();
1785
2110
  const answeredToolCallIds = /* @__PURE__ */ new Set();
2111
+ const nativeToolTurns = /* @__PURE__ */ new Set();
2112
+ const turnEntryAliases = /* @__PURE__ */ new Map();
1786
2113
  let latestRootMessage = null;
1787
2114
  let latestRootAssistant = null;
1788
2115
  let sequence = 0;
@@ -1790,7 +2117,14 @@ function projectHistory(entries) {
1790
2117
  const kind = String(entry.kind ?? "");
1791
2118
  const loopId = String(entry.loop_name ?? "root");
1792
2119
  const id = String(entry.id ?? `history-${sequence + 1}`);
1793
- if (kind === "post_chat_followup" && isRecord3(entry.data)) {
2120
+ if (kind === "turn_timing" && isRecord(entry.data)) {
2121
+ const turnId = typeof entry.data.turn_id === "string" ? entry.data.turn_id : "";
2122
+ const duration = numberOrNull(entry.data.first_token_delivery_ms);
2123
+ const target = turnEntryAliases.get(turnId);
2124
+ if (target && duration !== null) target.first_token_delivery_ms = duration;
2125
+ continue;
2126
+ }
2127
+ if (kind === "post_chat_followup" && isRecord(entry.data)) {
1794
2128
  const assistantEntryId = String(entry.data.assistant_entry_id ?? "");
1795
2129
  if (latestRootMessage?.role === "assistant" && latestRootMessage.turn_id === assistantEntryId) {
1796
2130
  const followup = normalizePostChatFollowup(entry.data, assistantEntryId);
@@ -1798,7 +2132,15 @@ function projectHistory(entries) {
1798
2132
  }
1799
2133
  continue;
1800
2134
  }
1801
- if (kind === "message" && isRecord3(entry.message)) {
2135
+ if (kind === "context") {
2136
+ const turn = buildContextTurn(entry, sequence + 1);
2137
+ if (turn) {
2138
+ sequence += 1;
2139
+ turns.push(turn);
2140
+ }
2141
+ continue;
2142
+ }
2143
+ if (kind === "message" && isRecord(entry.message)) {
1802
2144
  const message = entry.message;
1803
2145
  const role = String(message.role ?? "");
1804
2146
  if (role === "tool") {
@@ -1808,6 +2150,9 @@ function projectHistory(entries) {
1808
2150
  if (owner && toolCall) {
1809
2151
  toolCall.status = message._error ? "error" : inferToolStatus(String(message.content ?? ""));
1810
2152
  toolCall.result = message.content ?? "";
2153
+ const duration = numberOrNull(message._duration_ms);
2154
+ if (duration !== null) toolCall.duration_ms = duration;
2155
+ if (message._ran_in_parallel === true) toolCall.ran_in_parallel = true;
1811
2156
  owner.blocks.push({
1812
2157
  type: "tool_result",
1813
2158
  content: message.content ?? "",
@@ -1817,7 +2162,7 @@ function projectHistory(entries) {
1817
2162
  continue;
1818
2163
  }
1819
2164
  if (role !== "user" && role !== "assistant" && role !== "system") continue;
1820
- const toolCalls = role === "assistant" ? buildToolCalls(message.tool_calls) : [];
2165
+ const toolCalls = role === "assistant" && !message._error ? buildToolCalls(message.tool_calls) : [];
1821
2166
  const blocks = buildBlocks(message, toolCalls);
1822
2167
  const turn = {
1823
2168
  id,
@@ -1830,14 +2175,45 @@ function projectHistory(entries) {
1830
2175
  blocks,
1831
2176
  tool_calls: toolCalls,
1832
2177
  model: stringOrNull(message.model),
1833
- usage: isRecord3(message._usage) ? { ...message._usage } : null,
2178
+ usage: isRecord(message._usage) ? { ...message._usage } : null,
1834
2179
  duration_ms: numberOrZero(message._duration_ms),
1835
- started_at: typeof entry.timestamp === "string" ? entry.timestamp : void 0
2180
+ first_token_ms: numberOrNull(message._first_token_ms),
2181
+ stream_duration_ms: numberOrNull(message._stream_duration_ms),
2182
+ started_at: typeof entry.timestamp === "string" ? entry.timestamp : void 0,
2183
+ // fork 子会话的父锚点:历史里落在消息级 _parent_fork_tool_call_id。丢掉它,
2184
+ // 刷新后工作流步骤 fork 就配不回 workflow:run 工具调用(普通 Agent fork 有
2185
+ // root Agent 工具调用兜底配对,工作流 fork 没有,子智能体卡会整体消失)。
2186
+ parent_fork_tool_call_id: stringOrNull(message._parent_fork_tool_call_id)
1836
2187
  };
2188
+ const isNativeToolEntry = role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.some(
2189
+ (toolCall) => isRecord(toolCall) && typeof toolCall.provider === "string"
2190
+ );
2191
+ const previousTurn = turns[turns.length - 1];
2192
+ if (role === "assistant" && previousTurn?.role === "assistant" && previousTurn.loop_id === loopId && nativeToolTurns.has(previousTurn)) {
2193
+ nativeToolTurns.delete(previousTurn);
2194
+ previousTurn.blocks.push(...turn.blocks);
2195
+ previousTurn.tool_calls.push(...turn.tool_calls);
2196
+ previousTurn.status = turn.status;
2197
+ previousTurn.model = turn.model ?? previousTurn.model;
2198
+ previousTurn.usage = turn.usage ?? previousTurn.usage;
2199
+ previousTurn.duration_ms = turn.duration_ms || previousTurn.duration_ms;
2200
+ previousTurn.first_token_ms = turn.first_token_ms ?? previousTurn.first_token_ms;
2201
+ previousTurn.stream_duration_ms = turn.stream_duration_ms ?? previousTurn.stream_duration_ms;
2202
+ if (turn.started_at) previousTurn.started_at = turn.started_at;
2203
+ previousTurn.id = turn.id;
2204
+ previousTurn.turn_id = turn.turn_id;
2205
+ if (isNativeToolEntry) nativeToolTurns.add(previousTurn);
2206
+ turnEntryAliases.set(id, previousTurn);
2207
+ if (loopId === "root") latestRootMessage = previousTurn;
2208
+ latestAssistantByLoop.set(loopId, previousTurn);
2209
+ continue;
2210
+ }
1837
2211
  if (loopId === "root" && (role === "user" || role === "assistant")) {
1838
2212
  latestRootMessage = turn;
1839
2213
  }
1840
2214
  turns.push(turn);
2215
+ turnEntryAliases.set(id, turn);
2216
+ if (isNativeToolEntry) nativeToolTurns.add(turn);
1841
2217
  if (role === "assistant") latestAssistantByLoop.set(loopId, turn);
1842
2218
  if (loopId === "root") {
1843
2219
  if (role === "assistant") latestRootAssistant = turn;
@@ -1846,10 +2222,25 @@ function projectHistory(entries) {
1846
2222
  for (const toolCall of toolCalls) toolOwners.set(toolCall.id, turn);
1847
2223
  continue;
1848
2224
  }
2225
+ if (kind === "tool_result") {
2226
+ const data = entry.data ?? {};
2227
+ const toolCallId = String(data.tool_call_id ?? "");
2228
+ const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
2229
+ if (owner && toolCallId) {
2230
+ const rawContent = data.content;
2231
+ const content = typeof rawContent === "string" ? rawContent : Array.isArray(rawContent) ? rawContent : String(rawContent ?? "");
2232
+ const status = data.is_error === true ? "error" : inferToolStatus(typeof content === "string" ? content : JSON.stringify(content));
2233
+ setToolCallStatus(owner, toolCallId, status, content, null);
2234
+ if (!owner.blocks.some((block) => block.type === "tool_result" && block.tool_call_id === toolCallId)) {
2235
+ owner.blocks.push({ type: "tool_result", content, tool_call_id: toolCallId });
2236
+ }
2237
+ }
2238
+ continue;
2239
+ }
1849
2240
  if (kind === "mode_change") {
1850
2241
  turns.push(markerTurn(id, ++sequence, loopId, "mode_change", entry.data ?? {}));
1851
2242
  } else if (kind === "workspace_change") {
1852
- const change = isRecord3(entry.data) ? entry.data : {};
2243
+ const change = isRecord(entry.data) ? entry.data : {};
1853
2244
  const source = stringOrNull(change.source) ?? "";
1854
2245
  const projectName = stringOrNull(change.project_name) ?? "";
1855
2246
  const fromPath = stringOrNull(change.from) ?? "";
@@ -1878,19 +2269,20 @@ function projectHistory(entries) {
1878
2269
  } else if (kind === "tool_ui" || kind === "tool_bridge") {
1879
2270
  const data = entry.data ?? {};
1880
2271
  const toolCallId = String(data.tool_call_id ?? "");
1881
- const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
2272
+ const owner = kind === "tool_bridge" ? toolCallId ? toolOwners.get(toolCallId) : void 0 : toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
1882
2273
  const content = kind === "tool_ui" ? data.ui : data.bridge;
1883
2274
  if (owner && content !== void 0) {
1884
2275
  owner.blocks.push({
1885
2276
  type: kind,
1886
2277
  content,
2278
+ ...kind === "tool_bridge" ? { delivery_id: id } : {},
1887
2279
  tool_call_id: toolCallId || null
1888
2280
  });
1889
2281
  }
1890
2282
  } else if (kind === "compaction" || kind === "tool_result_archive") {
1891
2283
  const data = entry.data ?? {};
1892
2284
  const compactionId = String(data.compaction_id ?? entry.id ?? `history-${sequence + 1}`);
1893
- let content = isRecord3(entry.message) ? entry.message.content : void 0;
2285
+ let content = isRecord(entry.message) ? entry.message.content : void 0;
1894
2286
  if (kind === "tool_result_archive") {
1895
2287
  const archivedCount = Array.isArray(data.archived_files) ? data.archived_files.length : 0;
1896
2288
  content = `<compaction-summary>
@@ -1898,18 +2290,24 @@ function projectHistory(entries) {
1898
2290
  </compaction-summary>`;
1899
2291
  }
1900
2292
  turns.push(
1901
- buildCompactionTurn(++sequence, compactionId, loopId, "completed", {
1902
- ...data,
1903
- content
1904
- })
2293
+ buildCompactionTurn(
2294
+ ++sequence,
2295
+ compactionId,
2296
+ loopId,
2297
+ data.status === "failed" ? "failed" : "completed",
2298
+ {
2299
+ ...data,
2300
+ content
2301
+ }
2302
+ )
1905
2303
  );
1906
2304
  } else if (kind === "child_pause") {
1907
2305
  const data = entry.data ?? {};
1908
- const sourceLoop = isRecord3(data.source_loop) ? data.source_loop : {};
2306
+ const sourceLoop = isRecord(data.source_loop) ? data.source_loop : {};
1909
2307
  const childLoopId = String(data.child_loop_name ?? sourceLoop.name ?? "");
1910
2308
  const childToolCallId = String(data.child_pause_tool_call_id ?? "");
1911
2309
  const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
1912
- const pauseToolData = isRecord3(data.pause_tool_data) ? data.pause_tool_data : {};
2310
+ const pauseToolData = isRecord(data.pause_tool_data) ? data.pause_tool_data : {};
1913
2311
  const description = String(sourceLoop.description ?? "");
1914
2312
  applyAskUserPauseToTurn(
1915
2313
  latestAssistantByLoop.get(childLoopId),
@@ -1997,15 +2395,15 @@ function parseToolArguments(value) {
1997
2395
  if (!value) return {};
1998
2396
  try {
1999
2397
  const parsed = JSON.parse(value);
2000
- return isRecord3(parsed) ? parsed : {};
2398
+ return isRecord(parsed) ? parsed : {};
2001
2399
  } catch {
2002
2400
  return {};
2003
2401
  }
2004
2402
  }
2005
2403
  function buildToolCalls(value) {
2006
2404
  if (!Array.isArray(value)) return [];
2007
- return value.filter(isRecord3).map((raw) => {
2008
- const fn = isRecord3(raw.function) ? raw.function : {};
2405
+ return value.filter(isRecord).map((raw) => {
2406
+ const fn = isRecord(raw.function) ? raw.function : {};
2009
2407
  const name = String(fn.name ?? "");
2010
2408
  return {
2011
2409
  id: String(raw.id ?? ""),
@@ -2021,23 +2419,40 @@ function buildBlocks(message, toolCalls) {
2021
2419
  const displayContent = message.role === "user" ? message._display_content : void 0;
2022
2420
  const stored = message._blocks;
2023
2421
  const blocks = [];
2422
+ const storedToolCallIds = /* @__PURE__ */ new Set();
2024
2423
  if (displayContent !== void 0) {
2025
2424
  blocks.push({ type: "text", content: displayContent });
2026
2425
  } else if (Array.isArray(stored)) {
2027
2426
  for (const raw of stored) {
2028
- if (!isRecord3(raw)) continue;
2427
+ if (!isRecord(raw)) continue;
2029
2428
  const type3 = String(raw.type ?? "");
2030
2429
  if (type3 === "thinking") blocks.push({ type: type3, content: raw.thinking ?? raw.content ?? "" });
2031
2430
  if (type3 === "text") blocks.push({ type: type3, content: raw.text ?? raw.content ?? "" });
2431
+ if (type3 === "tool_call") {
2432
+ const toolCallId = String(raw.id ?? "");
2433
+ const toolCall = toolCalls.find((candidate) => candidate.id === toolCallId);
2434
+ if (!toolCall) continue;
2435
+ storedToolCallIds.add(toolCallId);
2436
+ blocks.push({
2437
+ type: "tool_use",
2438
+ content: toolCall.arguments,
2439
+ tool_call_id: toolCall.id,
2440
+ tool_name: toolCall.tool_name,
2441
+ display_name: toolCall.display_name
2442
+ });
2443
+ }
2032
2444
  }
2033
2445
  } else if (message.content !== void 0 && message.content !== "") {
2034
2446
  blocks.push({ type: "text", content: message.content });
2035
2447
  }
2036
2448
  for (const toolCall of toolCalls) {
2449
+ if (storedToolCallIds.has(toolCall.id)) continue;
2037
2450
  blocks.push({
2038
2451
  type: "tool_use",
2039
- content: { name: toolCall.tool_name, arguments: toolCall.arguments },
2040
- tool_call_id: toolCall.id
2452
+ content: toolCall.arguments,
2453
+ tool_call_id: toolCall.id,
2454
+ tool_name: toolCall.tool_name,
2455
+ display_name: toolCall.display_name
2041
2456
  });
2042
2457
  }
2043
2458
  return blocks;
@@ -2058,15 +2473,15 @@ function markerTurn(id, sequence, loopId, type3, content, toolCallId = null) {
2058
2473
  duration_ms: 0
2059
2474
  };
2060
2475
  }
2061
- function isRecord3(value) {
2062
- return typeof value === "object" && value !== null && !Array.isArray(value);
2063
- }
2064
2476
  function stringOrNull(value) {
2065
2477
  return typeof value === "string" ? value : null;
2066
2478
  }
2067
2479
  function numberOrZero(value) {
2068
2480
  return typeof value === "number" ? value : 0;
2069
2481
  }
2482
+ function numberOrNull(value) {
2483
+ return typeof value === "number" && value >= 0 ? value : null;
2484
+ }
2070
2485
  function findLatestAssistant(turns, loopId) {
2071
2486
  for (let index = turns.length - 1; index >= 0; index -= 1) {
2072
2487
  const turn = turns[index];
@@ -2094,13 +2509,13 @@ function toCreateSessionPayload(request) {
2094
2509
  solution_snapshot_session_id: request.solution_snapshot_session_id ?? null,
2095
2510
  compaction_ratio: request.compaction_ratio ?? null,
2096
2511
  ...request.runtime_type ? { runtime_type: request.runtime_type } : {},
2097
- ...request.daemon_id ? { daemon_id: request.daemon_id } : {}
2512
+ ...request.daemon_id ? { daemon_id: request.daemon_id } : {},
2513
+ ...request.agent_runtime_id ? { agent_runtime_id: request.agent_runtime_id } : {}
2098
2514
  };
2099
2515
  }
2100
2516
  var builtinSolutionIds = /* @__PURE__ */ new Set([
2101
2517
  "app-dev",
2102
2518
  "general_chat",
2103
- "multica",
2104
2519
  "night_build",
2105
2520
  "skill_editor",
2106
2521
  "smart_bid",
@@ -2329,6 +2744,27 @@ var SessionsResource = class {
2329
2744
  );
2330
2745
  return projectHistory(entries);
2331
2746
  }
2747
+ async getSessionTurnsPage(sessionId, options = {}) {
2748
+ const query = new URLSearchParams();
2749
+ if (options.limit !== void 0) query.set("limit", String(options.limit));
2750
+ if (options.before !== void 0) query.set("before", options.before);
2751
+ const suffix = query.size ? `?${query}` : "";
2752
+ const page = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turns${suffix}`);
2753
+ return {
2754
+ turns: page.turns,
2755
+ nextBefore: page.next_before,
2756
+ currentMode: page.current_mode
2757
+ };
2758
+ }
2759
+ async getSessionTurnCommands(sessionId) {
2760
+ const commands = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turn-commands`);
2761
+ return commands.map((command) => ({
2762
+ deliveryId: command.delivery_id,
2763
+ toolCallId: command.tool_call_id ?? void 0,
2764
+ action: command.action,
2765
+ payload: command.payload
2766
+ }));
2767
+ }
2332
2768
  listResultFeedback(sessionId) {
2333
2769
  return this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/feedback`);
2334
2770
  }
@@ -2700,6 +3136,36 @@ var Emitter = class {
2700
3136
  }
2701
3137
  };
2702
3138
 
3139
+ // src/session/history-page.ts
3140
+ function sortedUnique(turns) {
3141
+ const byId = /* @__PURE__ */ new Map();
3142
+ for (const turn of turns) byId.set(turn.turn_id, turn);
3143
+ return [...byId.values()].sort((left, right) => left.sequence - right.sequence);
3144
+ }
3145
+ function prependOlder(current, page) {
3146
+ const currentById = new Map(current.map((turn) => [turn.turn_id, turn]));
3147
+ return sortedUnique([
3148
+ ...current,
3149
+ ...page.map((turn) => {
3150
+ const live = currentById.get(turn.turn_id);
3151
+ return live ? { ...live, sequence: turn.sequence } : turn;
3152
+ })
3153
+ ]);
3154
+ }
3155
+ function replaceWindow(current, page, liveRevisionAtStart, liveRevisions) {
3156
+ const pageById = new Map(page.map((turn) => [turn.turn_id, turn]));
3157
+ const newerLive = current.filter(
3158
+ (turn) => turn.turn_id.startsWith("local-user-") || (liveRevisions.byTurnId.get(turn.turn_id) ?? 0) > liveRevisionAtStart
3159
+ );
3160
+ return sortedUnique([
3161
+ ...page,
3162
+ ...newerLive.map((turn) => {
3163
+ const snapshot2 = pageById.get(turn.turn_id);
3164
+ return snapshot2 ? { ...turn, sequence: snapshot2.sequence } : turn;
3165
+ })
3166
+ ]);
3167
+ }
3168
+
2703
3169
  // src/schemas/message-utils.ts
2704
3170
  var SYSTEM_REMINDER_PATTERN = /^<system-reminder>\s*[\s\S]*?\s*<\/system-reminder>$/i;
2705
3171
  var SYSTEM_NOTIFICATION_PATTERN = /^<system-notification>\s*[\s\S]*?\s*<\/system-notification>$/i;
@@ -2708,6 +3174,36 @@ var LEGACY_FORK_CONTEXT_MARKERS = [
2708
3174
  "\u53EF\u901A\u8FC7 `read_file` \u8BFB\u53D6\u3002",
2709
3175
  "# \u4F60\u7684\u4EFB\u52A1"
2710
3176
  ];
3177
+ var INTERNAL_CHAT_ERROR_PATTERNS = [
3178
+ /\b(?:ENOENT|EACCES|EPERM)\b/i,
3179
+ /\[(?:Errno -?\d+|WinError \d+)\]/i,
3180
+ /\b(?:ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|ENOTFOUND|EAI_AGAIN)\b/i,
3181
+ /\b(?:Connection refused|Connection reset by peer|All connection attempts failed|getaddrinfo failed)\b/i,
3182
+ /(?:^|[\s("'`])\/(?:root|home|Users|tmp|var|opt|etc)\//,
3183
+ /[A-Za-z]:\\(?:Users|Windows|Program Files|AppData)\\/i,
3184
+ /(?:^|\n)Traceback \(most recent call last\):/,
3185
+ /(?:^|\n)\s*at\s+\S+(?:\s+\(|:\d+:\d+)/,
3186
+ /(?:^|\n)File "[^"]+", line \d+/
3187
+ ];
3188
+ var SAFE_CHAT_ERROR_PREFIXES = [
3189
+ "\u6C99\u76D2\u8FD0\u884C\u73AF\u5883\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
3190
+ "\u6A21\u578B\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u68C0\u67E5\u6A21\u578B\u914D\u7F6E\u540E\u91CD\u8BD5\u3002"
3191
+ ];
3192
+ var GENERIC_CHAT_ERROR = "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002";
3193
+ function chatErrorForDisplay(message, developer = false) {
3194
+ const normalized = message.trim();
3195
+ if (!developer) {
3196
+ const safePrefix = SAFE_CHAT_ERROR_PREFIXES.find((prefix) => normalized.startsWith(prefix));
3197
+ if (safePrefix) return safePrefix;
3198
+ }
3199
+ if (!developer && /no source matches this model/i.test(normalized)) {
3200
+ return "\u5F53\u524D\u6A21\u578B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u6216\u91CD\u65B0\u9009\u62E9\u6A21\u578B\u3002";
3201
+ }
3202
+ if (!developer && INTERNAL_CHAT_ERROR_PATTERNS.some((pattern) => pattern.test(normalized))) {
3203
+ return GENERIC_CHAT_ERROR;
3204
+ }
3205
+ return normalized || GENERIC_CHAT_ERROR;
3206
+ }
2711
3207
  function normalizeMessageContent(content) {
2712
3208
  if (typeof content === "string") return content;
2713
3209
  if (Array.isArray(content)) return content;
@@ -2727,10 +3223,10 @@ function isLegacyForkContextText(text) {
2727
3223
  const normalized = text.trim();
2728
3224
  return LEGACY_FORK_CONTEXT_MARKERS.every((marker) => normalized.includes(marker));
2729
3225
  }
2730
- function isHiddenInternalMessage(message) {
3226
+ function isHiddenInternalMessage(message, options = {}) {
2731
3227
  if (message.role !== "user") return false;
2732
3228
  const text = getTextContent(normalizeMessageContent(message.content));
2733
- return isSystemReminderText(text) || isSystemNotificationText(text) || isLegacyForkContextText(text);
3229
+ return isSystemReminderText(text) || isSystemNotificationText(text) || (options.includeLegacyForkContext ?? true) && isLegacyForkContextText(text);
2734
3230
  }
2735
3231
  function buildMessageContent(text, attachments) {
2736
3232
  if (attachments.length === 0) return text;
@@ -2877,7 +3373,9 @@ function createInitialSessionState(sessionId) {
2877
3373
  agentLoops: {},
2878
3374
  activeCompaction: null,
2879
3375
  replay: null,
2880
- viewerRole: null
3376
+ viewerRole: null,
3377
+ nextBefore: null,
3378
+ loadingOlder: false
2881
3379
  };
2882
3380
  }
2883
3381
  function parseAgentDescription(argumentsJson) {
@@ -2903,10 +3401,10 @@ function inferLoopStatusFromMessages(messages) {
2903
3401
  }
2904
3402
  function inferLoopStatusFromTurns(turns, messages) {
2905
3403
  const latestAgentNotification = [...turns].reverse().flatMap((turn) => turn.blocks).find((block) => {
2906
- if (block.type !== "system_notification" || !isRecord4(block.content)) return false;
3404
+ if (block.type !== "system_notification" || !isRecord(block.content)) return false;
2907
3405
  return block.content.notification_type === "agent:start" || block.content.notification_type === "agent:end";
2908
3406
  });
2909
- if (latestAgentNotification?.type === "system_notification" && isRecord4(latestAgentNotification.content)) {
3407
+ if (latestAgentNotification?.type === "system_notification" && isRecord(latestAgentNotification.content)) {
2910
3408
  const notificationType = latestAgentNotification.content.notification_type;
2911
3409
  const status = latestAgentNotification.content.status;
2912
3410
  if (notificationType === "agent:start" || status === "running") return "running";
@@ -2915,17 +3413,44 @@ function inferLoopStatusFromTurns(turns, messages) {
2915
3413
  }
2916
3414
  return inferLoopStatusFromMessages(messages);
2917
3415
  }
2918
- function isRecord4(value) {
2919
- return typeof value === "object" && value !== null && !Array.isArray(value);
3416
+ function toSelectionMap(value) {
3417
+ if (!isRecord(value)) return {};
3418
+ const entries = Object.entries(value).map(([questionKey, optionIndexes]) => {
3419
+ if (!Array.isArray(optionIndexes)) return null;
3420
+ const parsedIndexes = optionIndexes.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item));
3421
+ return [Number(questionKey), parsedIndexes];
3422
+ }).filter((entry) => entry !== null);
3423
+ return Object.fromEntries(entries);
3424
+ }
3425
+ function toCustomMap(value) {
3426
+ if (!isRecord(value)) return {};
3427
+ const entries = Object.entries(value).filter(([, text]) => typeof text === "string").map(([questionKey, text]) => [Number(questionKey), text]);
3428
+ return Object.fromEntries(entries);
3429
+ }
3430
+ function extractAskAnswers(turns) {
3431
+ const answers = {};
3432
+ for (const turn of turns) {
3433
+ for (const block of turn.blocks) {
3434
+ if (block.type !== "ask_user_answer" || typeof block.tool_call_id !== "string") continue;
3435
+ if (!isRecord(block.content)) continue;
3436
+ const note = typeof block.content.note === "string" ? block.content.note.trim() : "";
3437
+ answers[block.tool_call_id] = {
3438
+ selections: toSelectionMap(block.content.selections),
3439
+ custom: toCustomMap(block.content.custom),
3440
+ ...note ? { note } : {}
3441
+ };
3442
+ }
3443
+ }
3444
+ return answers;
2920
3445
  }
2921
3446
  function parentForkToolCallIdFromTurn(turn) {
2922
3447
  if (typeof turn.parent_fork_tool_call_id === "string" && turn.parent_fork_tool_call_id.length > 0) {
2923
3448
  return turn.parent_fork_tool_call_id;
2924
3449
  }
2925
3450
  for (const block of turn.blocks) {
2926
- if (block.type !== "system_notification" || !isRecord4(block.content)) continue;
3451
+ if (block.type !== "system_notification" || !isRecord(block.content)) continue;
2927
3452
  const metadata = block.content.metadata;
2928
- if (!isRecord4(metadata)) continue;
3453
+ if (!isRecord(metadata)) continue;
2929
3454
  const parentId = metadata.parent_fork_tool_call_id;
2930
3455
  if (typeof parentId === "string" && parentId.length > 0) return parentId;
2931
3456
  }
@@ -2942,16 +3467,16 @@ function buildMessageContent2(turn) {
2942
3467
  }
2943
3468
  function workspaceNotificationContent(turn) {
2944
3469
  const block = turn.blocks.find(
2945
- (candidate) => candidate.type === "system_notification" && isRecord4(candidate.content) && candidate.content.notification_type === "workspace_change"
3470
+ (candidate) => candidate.type === "system_notification" && isRecord(candidate.content) && candidate.content.notification_type === "workspace_change"
2946
3471
  );
2947
- if (!block || !isRecord4(block.content)) return "";
3472
+ if (!block || !isRecord(block.content)) return "";
2948
3473
  const title = typeof block.content.title === "string" ? block.content.title.trim() : "";
2949
3474
  const detail = typeof block.content.detail === "string" ? block.content.detail.trim() : "";
2950
3475
  return [title ? `**${title}**` : "", detail].filter(Boolean).join("\n\n");
2951
3476
  }
2952
3477
  function askUserAnswerContent(turn) {
2953
3478
  const answerBlock = turn.blocks.find((block) => block.type === "ask_user_answer");
2954
- if (!answerBlock || !isRecord4(answerBlock.content)) return null;
3479
+ if (!answerBlock || !isRecord(answerBlock.content)) return null;
2955
3480
  const answer = answerBlock.content.answer;
2956
3481
  return typeof answer === "string" && answer.trim().length > 0 ? answer : null;
2957
3482
  }
@@ -2971,6 +3496,19 @@ function extractModeFromBlocks(blocks) {
2971
3496
  return null;
2972
3497
  }
2973
3498
  function projectionToMessage(turn) {
3499
+ if (turn.kind === "context") {
3500
+ const context = contextProjectionData(turn);
3501
+ if (!context) return null;
3502
+ return {
3503
+ role: "assistant",
3504
+ content: "",
3505
+ kind: "context",
3506
+ context,
3507
+ loop_name: turn.loop_id,
3508
+ entry_id: turn.turn_id,
3509
+ status: turn.status
3510
+ };
3511
+ }
2974
3512
  if (turn.kind === "compaction" && turn.compaction_id) {
2975
3513
  return {
2976
3514
  role: "assistant",
@@ -3140,6 +3678,56 @@ function orderTurns(turns) {
3140
3678
  }
3141
3679
  return turns;
3142
3680
  }
3681
+ function reconcileOptimisticUserTurns(currentTurns, incomingTurns) {
3682
+ return reconcileUserTurns(currentTurns, incomingTurns, true);
3683
+ }
3684
+ function reconcileHistoricalUserTurns(currentTurns, incomingTurns) {
3685
+ return reconcileUserTurns(currentTurns, incomingTurns, false);
3686
+ }
3687
+ function reconcileUserTurns(currentTurns, incomingTurns, allowLiveFallback) {
3688
+ const preservedTurns = preserveClientRenderIds(currentTurns, incomingTurns);
3689
+ const optimisticUserTurns = currentTurns.filter(
3690
+ (turn) => turn.role === "user" && (turn.kind ?? "message") === "message" && turn.turn_id.startsWith("local-user-")
3691
+ );
3692
+ const optimisticByRequestId = new Map(
3693
+ optimisticUserTurns.map((turn) => [
3694
+ turn.turn_id.slice("local-user-".length),
3695
+ turn
3696
+ ])
3697
+ );
3698
+ const currentTurnIds = new Set(currentTurns.map((turn) => turn.turn_id));
3699
+ const claimedOptimisticIds = /* @__PURE__ */ new Set();
3700
+ const reconciled = preservedTurns.map((turn) => {
3701
+ const exactOptimistic = typeof turn.client_request_id === "string" ? optimisticByRequestId.get(turn.client_request_id) : void 0;
3702
+ if (exactOptimistic && !claimedOptimisticIds.has(exactOptimistic.turn_id) && isUserConfirmation(turn)) {
3703
+ claimedOptimisticIds.add(exactOptimistic.turn_id);
3704
+ return { ...turn, client_render_id: exactOptimistic.turn_id };
3705
+ }
3706
+ const fallbackOptimistic = optimisticUserTurns.find(
3707
+ (candidate) => !claimedOptimisticIds.has(candidate.turn_id)
3708
+ );
3709
+ if (allowLiveFallback && typeof turn.client_request_id !== "string" && fallbackOptimistic && isUserConfirmation(turn) && !turn.turn_id.startsWith("local-user-") && !currentTurnIds.has(turn.turn_id)) {
3710
+ claimedOptimisticIds.add(fallbackOptimistic.turn_id);
3711
+ return { ...turn, client_render_id: fallbackOptimistic.turn_id };
3712
+ }
3713
+ return turn;
3714
+ });
3715
+ return claimedOptimisticIds.size === 0 ? reconciled : reconciled.filter((turn) => !claimedOptimisticIds.has(turn.turn_id));
3716
+ }
3717
+ function isUserConfirmation(turn) {
3718
+ return turn.role === "user" && (turn.kind ?? "message") === "message" || turn.blocks.some((block) => block.type === "ask_user_answer");
3719
+ }
3720
+ function preserveClientRenderIds(currentTurns, incomingTurns) {
3721
+ const currentById = new Map(currentTurns.map((turn) => [turn.turn_id, turn]));
3722
+ return incomingTurns.map((turn) => {
3723
+ const current = currentById.get(turn.turn_id);
3724
+ if (current) {
3725
+ const currentRenderId = current.client_render_id;
3726
+ return turn.client_render_id || !currentRenderId ? turn : { ...turn, client_render_id: currentRenderId };
3727
+ }
3728
+ return turn;
3729
+ });
3730
+ }
3143
3731
  function areAgentLoopsEqual(left, right) {
3144
3732
  if (!left) return Object.keys(right).length === 0;
3145
3733
  const leftEntries = Object.entries(left);
@@ -3151,7 +3739,7 @@ function areAgentLoopsEqual(left, right) {
3151
3739
  });
3152
3740
  }
3153
3741
  function withTurns(state, turns) {
3154
- const orderedTurns = orderTurns(turns);
3742
+ const orderedTurns = orderTurns(reconcileHistoricalUserTurns(state.turns, turns));
3155
3743
  const { messages, agentLoops, activeCompaction } = materialize(orderedTurns);
3156
3744
  const isWaitingForInput = orderedTurns.some(
3157
3745
  (turn) => turn.tool_calls.some((toolCall) => toolCall.status === "awaiting_answer")
@@ -3161,14 +3749,31 @@ function withTurns(state, turns) {
3161
3749
  const preservedErrors = lastTurnId ? state.messages.filter(
3162
3750
  (m) => m.role === "error" && typeof m.entry_id === "string" && m.entry_id.startsWith(`${ERROR_ANCHOR_PREFIX}${lastTurnId}:`)
3163
3751
  ) : [];
3164
- const mergedMessages = preservedErrors.length > 0 ? [...messages, ...preservedErrors] : messages;
3752
+ const turnById = new Map(orderedTurns.map((turn) => [turn.turn_id, turn]));
3753
+ const messagesWithRenderIds = messages.map((message) => {
3754
+ if (!message.entry_id) return message;
3755
+ const renderId = turnById.get(message.entry_id)?.client_render_id;
3756
+ return renderId ? { ...message, render_id: renderId } : message;
3757
+ });
3758
+ const mergedMessages = preservedErrors.length > 0 ? [...messagesWithRenderIds, ...preservedErrors] : messagesWithRenderIds;
3165
3759
  const latestMode = [...orderedTurns].reverse().map((turn) => extractModeFromBlocks(turn.blocks)).find((mode) => mode !== null);
3760
+ const askAnswersFromHistory = extractAskAnswers(orderedTurns);
3761
+ const presentToolCallIds = new Set(
3762
+ orderedTurns.flatMap((turn) => turn.tool_calls.map((toolCall) => toolCall.id))
3763
+ );
3764
+ const askAnswers = { ...askAnswersFromHistory };
3765
+ for (const [toolCallId, answer] of Object.entries(state.askAnswers)) {
3766
+ if (!(toolCallId in askAnswers) && presentToolCallIds.has(toolCallId)) {
3767
+ askAnswers[toolCallId] = answer;
3768
+ }
3769
+ }
3166
3770
  return {
3167
3771
  ...state,
3168
3772
  turns: orderedTurns,
3169
3773
  messages: mergedMessages,
3170
3774
  agentLoops: stableAgentLoops,
3171
3775
  activeCompaction,
3776
+ askAnswers,
3172
3777
  mode: latestMode ?? state.mode,
3173
3778
  // 历史恢复时 chat:end(paused) 可能已不在 socket replay 缓存中;
3174
3779
  // awaiting_answer 是投影里的持久权威状态,必须据此恢复问答交互。
@@ -3176,9 +3781,9 @@ function withTurns(state, turns) {
3176
3781
  };
3177
3782
  }
3178
3783
  var localTurnCounter = 0;
3179
- function addUserMessage(state, content) {
3784
+ function addUserMessage(state, content, clientRequestId) {
3180
3785
  localTurnCounter += 1;
3181
- const turnId = `local-user-${Date.now()}-${localTurnCounter}`;
3786
+ const turnId = `local-user-${clientRequestId ?? `${Date.now()}-${localTurnCounter}`}`;
3182
3787
  const optimisticTurn = {
3183
3788
  id: turnId,
3184
3789
  sequence: Math.max(0, ...state.turns.map((turn) => turn.sequence)) + 1,
@@ -3196,14 +3801,16 @@ function addUserMessage(state, content) {
3196
3801
  }
3197
3802
  function upsertTurn(state, turn) {
3198
3803
  let existing = [...state.turns];
3199
- if (turn.role === "user" && !turn.turn_id.startsWith("local-user-")) {
3200
- existing = existing.filter((t) => !t.turn_id.startsWith("local-user-"));
3804
+ const [nextTurn] = reconcileOptimisticUserTurns(existing, [turn]);
3805
+ const claimedOptimisticId = nextTurn.client_render_id;
3806
+ if (claimedOptimisticId?.startsWith("local-user-")) {
3807
+ existing = existing.filter((item) => item.turn_id !== claimedOptimisticId);
3201
3808
  }
3202
- const index = existing.findIndex((item) => item.turn_id === turn.turn_id);
3809
+ const index = existing.findIndex((item) => item.turn_id === nextTurn.turn_id);
3203
3810
  if (index >= 0) {
3204
- existing[index] = turn;
3811
+ existing[index] = nextTurn;
3205
3812
  } else {
3206
- existing.push(turn);
3813
+ existing.push(nextTurn);
3207
3814
  }
3208
3815
  return withTurns(state, existing);
3209
3816
  }
@@ -3216,13 +3823,7 @@ function applyTurnPatch(state, patch) {
3216
3823
  if (lastSequence !== null && patch.sequence <= lastSequence) {
3217
3824
  return state;
3218
3825
  }
3219
- const nextTurn = { ...turn, sequence: patch.sequence };
3220
- if (index >= 0) {
3221
- existing[index] = nextTurn;
3222
- } else {
3223
- existing.push(nextTurn);
3224
- }
3225
- return withTurns(state, existing);
3826
+ return upsertTurn(state, { ...turn, sequence: patch.sequence });
3226
3827
  }
3227
3828
  function addErrorMessage(state, content) {
3228
3829
  const anchorTurnId = state.turns[state.turns.length - 1]?.turn_id ?? null;
@@ -3254,19 +3855,26 @@ function markStreamingTurns(state, turnStatus, toolCallStatus) {
3254
3855
 
3255
3856
  // src/session/agent-session.ts
3256
3857
  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";
3257
- var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
3258
- function isRecord5(value) {
3259
- return typeof value === "object" && value !== null && !Array.isArray(value);
3858
+ var COMMAND_RESTORE_RETRY_BASE_MS = 1e3;
3859
+ var COMMAND_RESTORE_RETRY_MAX_MS = 3e4;
3860
+ var fallbackRequestCounter = 0;
3861
+ function createClientRequestId() {
3862
+ if (typeof globalThis.crypto?.randomUUID === "function") {
3863
+ return globalThis.crypto.randomUUID();
3864
+ }
3865
+ fallbackRequestCounter += 1;
3866
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}-${fallbackRequestCounter}`;
3260
3867
  }
3868
+ var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
3261
3869
  function isOomText(value) {
3262
3870
  return typeof value === "string" && OOM_KEYWORDS_RE.test(value);
3263
3871
  }
3264
3872
  function parseJsonRecord(value) {
3265
- if (isRecord5(value)) return value;
3873
+ if (isRecord(value)) return value;
3266
3874
  if (typeof value !== "string") return null;
3267
3875
  try {
3268
3876
  const parsed = JSON.parse(value);
3269
- return isRecord5(parsed) ? parsed : null;
3877
+ return isRecord(parsed) ? parsed : null;
3270
3878
  } catch {
3271
3879
  return null;
3272
3880
  }
@@ -3292,6 +3900,12 @@ function isOomResult(value) {
3292
3900
  function isToolBridgeContent(value) {
3293
3901
  return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.action === "string";
3294
3902
  }
3903
+ function isCursorInvalidError(error) {
3904
+ return typeof error === "object" && error !== null && "status" in error && error.status === 409;
3905
+ }
3906
+ function commandDeliveryKey(deliveryId) {
3907
+ return `tool-command:${deliveryId}`;
3908
+ }
3295
3909
  function sessionStatusFromChatEnd(status) {
3296
3910
  if (status === "paused") return "waiting_for_input";
3297
3911
  if (status === "interrupted") return "interrupted";
@@ -3307,13 +3921,8 @@ function extractTerminalChatEndStatus(events) {
3307
3921
  }
3308
3922
  return null;
3309
3923
  }
3310
- function hasChatRunEvent2(events) {
3311
- return events.some(
3312
- (event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
3313
- );
3314
- }
3315
3924
  function isUiMetaLike(value) {
3316
- return isRecord5(value) && ("resourceHTML" in value || "resourceUri" in value || "resourceURI" in value);
3925
+ return isRecord(value) && ("resourceHTML" in value || "resourceUri" in value || "resourceURI" in value);
3317
3926
  }
3318
3927
  var AgentSession = class _AgentSession {
3319
3928
  sessionId;
@@ -3328,14 +3937,39 @@ var AgentSession = class _AgentSession {
3328
3937
  patchFlushCancel = null;
3329
3938
  joined = false;
3330
3939
  joinPending = null;
3940
+ // bootstrap 与每次 reconnect 共用一条订阅生命周期队列。它们都会重置 join
3941
+ // 状态并临时设置 replay source,禁止并发,否则较早操作的 finally 会清掉
3942
+ // 较晚操作仍在使用的来源上下文。
3943
+ subscriptionQueue = Promise.resolve();
3944
+ replaceQueue = Promise.resolve();
3945
+ timelineEpoch = 0;
3946
+ pagingGeneration = 0;
3947
+ liveRevision = 0;
3948
+ modeRevision = 0;
3949
+ liveRevisions = /* @__PURE__ */ new Map();
3950
+ pendingOptimisticTurnIds = /* @__PURE__ */ new Set();
3951
+ /** Consecutive supplements are persisted as one authoritative user turn. */
3952
+ pendingAppendTurnId = null;
3953
+ olderPending = null;
3954
+ replaceInFlight = 0;
3331
3955
  // 回放控制请求排成一条队列。连着点"5x"再点"退出回放"时,并发发出去的两个
3332
3956
  // 请求响应可能乱序回来,晚到的旧值会把已经退出的会话又显示成回放中;用
3333
3957
  // 序号丢弃过期响应又会在失败时留下中间态。串行执行就没有这些情况:每个
3334
3958
  // 响应都是当时最新的,失败也只影响自己。
3335
3959
  replayQueue = Promise.resolve();
3960
+ // Historical snapshots and live commands share one delivery barrier. A
3961
+ // repair closes the barrier before joining, snapshots the raw command log
3962
+ // after the join, then releases buffered commands in socket arrival order.
3963
+ commandRestoreQueue = Promise.resolve();
3964
+ commandRecoveryGeneration = 0;
3965
+ commandRestoreRetryAttempt = 0;
3966
+ commandRestoreRetryHandle = null;
3967
+ bufferedCommandDeliveries = null;
3336
3968
  pendingReplayMessage = null;
3337
3969
  pendingReplayMode;
3970
+ pendingReplayKbIds;
3338
3971
  recentChatEndAt = 0;
3972
+ subscriptionReplaySource = null;
3339
3973
  disposed = false;
3340
3974
  getAppContext = null;
3341
3975
  sendPending = false;
@@ -3359,11 +3993,11 @@ var AgentSession = class _AgentSession {
3359
3993
  const session = new _AgentSession(
3360
3994
  sessionId,
3361
3995
  {
3362
- streamTokens: false,
3363
3996
  emitEvent: readonlyError,
3364
3997
  emitWithAck: async () => readonlyError(),
3365
3998
  ensureConnected: readonlyError,
3366
- fetchTurns: async () => turns,
3999
+ fetchTurnsPage: async () => ({ turns, nextBefore: null, currentMode: "executing" }),
4000
+ fetchCommands: async () => [],
3367
4001
  // 静态历史没有回放身份可言,给个空详情即可,不要抛错——_bootstrap
3368
4002
  // 不会跑到这里,但保持只读语义一致。
3369
4003
  fetchSessionInfo: async () => ({ replay_state: null }),
@@ -3387,6 +4021,42 @@ var AgentSession = class _AgentSession {
3387
4021
  getState() {
3388
4022
  return this.state;
3389
4023
  }
4024
+ get hasOlderHistory() {
4025
+ return this.state.nextBefore !== null;
4026
+ }
4027
+ /** Load the page immediately preceding the current display window. */
4028
+ loadOlderHistory(options = {}) {
4029
+ if (this.disposed) return Promise.resolve();
4030
+ if (this.olderPending) return this.olderPending;
4031
+ if (this.replaceInFlight > 0) {
4032
+ return this.replaceQueue.then(() => this.loadOlderHistory(options));
4033
+ }
4034
+ const before = this.state.nextBefore;
4035
+ if (!before) return Promise.resolve();
4036
+ const epoch = this.timelineEpoch;
4037
+ const generation = this.pagingGeneration;
4038
+ this.update((state) => ({ ...state, loadingOlder: true }));
4039
+ const request = this.runtime.fetchTurnsPage(this.sessionId, before).then((page) => {
4040
+ if (epoch !== this.timelineEpoch || generation !== this.pagingGeneration || this.state.nextBefore !== before) return;
4041
+ options.beforeCommit?.();
4042
+ this.update((state) => ({
4043
+ ...withTurns(state, prependOlder(state.turns, page.turns)),
4044
+ nextBefore: page.nextBefore,
4045
+ loadingOlder: false
4046
+ }));
4047
+ }).catch((error) => {
4048
+ if (isCursorInvalidError(error) && epoch === this.timelineEpoch && generation === this.pagingGeneration && this.state.nextBefore === before) {
4049
+ this.timelineEpoch += 1;
4050
+ return this.replaceFromHistory();
4051
+ }
4052
+ }).finally(() => {
4053
+ if (this.olderPending !== request) return;
4054
+ this.olderPending = null;
4055
+ this.update((state) => ({ ...state, loadingOlder: false }));
4056
+ });
4057
+ this.olderPending = request;
4058
+ return request;
4059
+ }
3390
4060
  /** 订阅状态变化。返回取消订阅函数。 */
3391
4061
  subscribe(listener) {
3392
4062
  this.listeners.add(listener);
@@ -3452,6 +4122,22 @@ var AgentSession = class _AgentSession {
3452
4122
  this.sendPending = false;
3453
4123
  }
3454
4124
  }
4125
+ /** Queue a plain-text prompt while a turn is streaming. */
4126
+ async queue(content) {
4127
+ const text = content.trim();
4128
+ if (!text) return false;
4129
+ this.runtime.ensureConnected();
4130
+ this.runtime.emitEvent("chat:append", { session_id: this.sessionId, message: text });
4131
+ return true;
4132
+ }
4133
+ async listQueue() {
4134
+ const response = await this.runtime.emitWithAck("chat:queue:list", { session_id: this.sessionId }, 1e4);
4135
+ return Array.isArray(response?.items) ? response.items : [];
4136
+ }
4137
+ async cancelQueued(id) {
4138
+ const response = await this.runtime.emitWithAck("chat:queue:cancel", { session_id: this.sessionId, id }, 1e4);
4139
+ return response?.cancelled === true;
4140
+ }
3455
4141
  async sendUnlocked(content, options) {
3456
4142
  await this.ensureIdleBeforeSend();
3457
4143
  if (this.state.isStreaming || this.state.activeCompaction || this.manualCompactionPending) {
@@ -3464,7 +4150,7 @@ var AgentSession = class _AgentSession {
3464
4150
  if (this.getAppContext) {
3465
4151
  try {
3466
4152
  const rawContext = await this.getAppContext();
3467
- if (!isRecord5(rawContext)) {
4153
+ if (!isRecord(rawContext)) {
3468
4154
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
3469
4155
  }
3470
4156
  const serialized = JSON.stringify(rawContext);
@@ -3472,7 +4158,7 @@ var AgentSession = class _AgentSession {
3472
4158
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE\u53EF\u5E8F\u5217\u5316\u7684 JSON \u5BF9\u8C61");
3473
4159
  }
3474
4160
  const normalized = JSON.parse(serialized);
3475
- if (!isRecord5(normalized)) {
4161
+ if (!isRecord(normalized)) {
3476
4162
  throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
3477
4163
  }
3478
4164
  appContext = normalized;
@@ -3484,15 +4170,15 @@ var AgentSession = class _AgentSession {
3484
4170
  }
3485
4171
  const askUserAnswer = options.askUserAnswer;
3486
4172
  let optimisticTurnId = null;
4173
+ const clientRequestId = createClientRequestId();
3487
4174
  let optimisticAskAnswer;
3488
4175
  let previousAskAnswer;
3489
- if (!(askUserAnswer && typeof askUserAnswer.tool_call_id === "string")) {
3490
- this.update((state) => {
3491
- const next = addUserMessage(state, content);
3492
- optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
3493
- return next;
3494
- });
3495
- }
4176
+ this.update((state) => {
4177
+ const next = addUserMessage(state, content, clientRequestId);
4178
+ optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
4179
+ return next;
4180
+ });
4181
+ if (optimisticTurnId) this.markOptimisticTurnPending(optimisticTurnId);
3496
4182
  this.update((s) => ({ ...s, isStreaming: true }));
3497
4183
  if (askUserAnswer && typeof askUserAnswer.tool_call_id === "string") {
3498
4184
  const { tool_call_id, ...rest } = askUserAnswer;
@@ -3506,17 +4192,20 @@ var AgentSession = class _AgentSession {
3506
4192
  if (!options.replayDecision) {
3507
4193
  this.pendingReplayMessage = content;
3508
4194
  this.pendingReplayMode = options.mode;
4195
+ this.pendingReplayKbIds = options.kbIds;
3509
4196
  }
3510
4197
  const payload = {
3511
4198
  session_id: this.sessionId,
3512
4199
  message: content,
4200
+ client_request_id: clientRequestId,
3513
4201
  mode: options.mode,
3514
4202
  askuser_answer: askUserAnswer,
3515
4203
  model: options.model,
3516
4204
  thinking_override: options.thinkingOverride,
3517
4205
  whatif: options.whatif,
3518
4206
  replay_decision: options.replayDecision,
3519
- app_context: appContext
4207
+ app_context: appContext,
4208
+ kb_ids: options.kbIds && options.kbIds.length > 0 ? options.kbIds : void 0
3520
4209
  };
3521
4210
  try {
3522
4211
  await this.ensureJoined();
@@ -3552,6 +4241,7 @@ var AgentSession = class _AgentSession {
3552
4241
  rollbackOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer, message) {
3553
4242
  this.pendingReplayMessage = null;
3554
4243
  this.pendingReplayMode = void 0;
4244
+ this.pendingReplayKbIds = void 0;
3555
4245
  this.update((current) => {
3556
4246
  let next = optimisticTurnId ? withTurns(
3557
4247
  current,
@@ -3569,16 +4259,51 @@ var AgentSession = class _AgentSession {
3569
4259
  }
3570
4260
  return addErrorMessage({ ...next, isStreaming: false }, message);
3571
4261
  });
4262
+ if (optimisticTurnId) {
4263
+ this.pendingOptimisticTurnIds.delete(optimisticTurnId);
4264
+ this.liveRevisions.delete(optimisticTurnId);
4265
+ if (this.pendingAppendTurnId === optimisticTurnId) this.pendingAppendTurnId = null;
4266
+ }
3572
4267
  }
3573
4268
  /** 智能体运行期间追加补充说明(不打断当前回复)。 */
3574
4269
  append(text) {
3575
4270
  this.runtime.ensureConnected();
3576
- this.runtime.emitEvent("chat:append", { session_id: this.sessionId, message: text });
3577
- this.update((s) => addUserMessage(s, text));
4271
+ const existingAppendId = this.pendingAppendTurnId;
4272
+ let optimisticTurnId = existingAppendId;
4273
+ this.update((state) => {
4274
+ const existing = existingAppendId ? state.turns.find((turn) => turn.turn_id === existingAppendId) : void 0;
4275
+ if (existing && existing.role === "user") {
4276
+ const blocks = existing.blocks.map(
4277
+ (block, index) => index === 0 && block.type === "text" ? { ...block, content: `${block.content}
4278
+ ${text}` } : block
4279
+ );
4280
+ return withTurns(state, state.turns.map(
4281
+ (turn) => turn.turn_id === existingAppendId ? { ...turn, blocks } : turn
4282
+ ));
4283
+ }
4284
+ const next = addUserMessage(state, text, createClientRequestId());
4285
+ optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
4286
+ return next;
4287
+ });
4288
+ if (optimisticTurnId) {
4289
+ this.pendingAppendTurnId = optimisticTurnId;
4290
+ if (!this.pendingOptimisticTurnIds.has(optimisticTurnId)) {
4291
+ this.markOptimisticTurnPending(optimisticTurnId);
4292
+ }
4293
+ }
4294
+ this.runtime.emitEvent("chat:append", {
4295
+ session_id: this.sessionId,
4296
+ message: text,
4297
+ ...optimisticTurnId ? { client_request_id: optimisticTurnId.slice("local-user-".length) } : {}
4298
+ });
3578
4299
  }
3579
4300
  /** 停止当前回复。 */
3580
4301
  stop() {
3581
- return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) => response ?? {});
4302
+ return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) => {
4303
+ const result = response ?? {};
4304
+ if (result.status !== "accepted") this.manualCompactionPending = false;
4305
+ return result;
4306
+ });
3582
4307
  }
3583
4308
  /** 手动触发上下文压缩。 */
3584
4309
  compact() {
@@ -3598,8 +4323,10 @@ var AgentSession = class _AgentSession {
3598
4323
  trigger: "manual"
3599
4324
  }
3600
4325
  }));
3601
- return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {}).finally(() => {
4326
+ return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {}).catch((error) => {
3602
4327
  this.manualCompactionPending = false;
4328
+ throw error;
4329
+ }).finally(() => {
3603
4330
  if (this.state.activeCompaction?.compaction_id === "pending-manual-compaction") {
3604
4331
  this.update((state) => ({ ...state, activeCompaction: null }));
3605
4332
  }
@@ -3638,10 +4365,20 @@ var AgentSession = class _AgentSession {
3638
4365
  if (this.disposed) return;
3639
4366
  if (!this.runtime.onDispose(this)) return;
3640
4367
  this.disposed = true;
4368
+ this.timelineEpoch += 1;
4369
+ this.invalidateOlderPaging();
4370
+ this.state = { ...this.state, nextBefore: null, loadingOlder: false };
3641
4371
  this.cancelPatchFlush();
3642
4372
  this.listeners.clear();
3643
4373
  this.emitter.clear();
3644
4374
  this.commands.clear();
4375
+ this.commandRecoveryGeneration += 1;
4376
+ this.commandRestoreRetryAttempt = 0;
4377
+ this.bufferedCommandDeliveries = null;
4378
+ if (this.commandRestoreRetryHandle !== null) {
4379
+ globalThis.clearTimeout(this.commandRestoreRetryHandle);
4380
+ this.commandRestoreRetryHandle = null;
4381
+ }
3645
4382
  }
3646
4383
  get isDisposed() {
3647
4384
  return this.disposed;
@@ -3653,27 +4390,55 @@ var AgentSession = class _AgentSession {
3653
4390
  this.update((s) => ({ ...s, connection }));
3654
4391
  }
3655
4392
  /** @internal socket 重连后由 Hub 调用:重新加入房间并补数。 */
3656
- async _rejoinAfterReconnect() {
3657
- const turnsBeforeRejoin = this.state.turns;
3658
- this.joined = false;
3659
- this.joinPending = null;
3660
- try {
3661
- await this.ensureJoined();
3662
- await Promise.all([
3663
- this.syncTurnsFromHistory({
3664
- clearOnEmpty: true,
3665
- preserveChangesSince: turnsBeforeRejoin
3666
- }),
3667
- this.syncReplayFromSession()
3668
- ]);
3669
- } catch {
3670
- }
4393
+ _rejoinAfterReconnect() {
4394
+ this.timelineEpoch += 1;
4395
+ this.invalidateOlderPaging();
4396
+ this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
4397
+ const reconnectLiveRevision = this.liveRevision;
4398
+ const reconnectModeRevision = this.modeRevision;
4399
+ const commandRecoveryGeneration = this.beginCommandRecovery();
4400
+ return this.enqueueSubscription(async () => {
4401
+ this.joined = false;
4402
+ this.joinPending = null;
4403
+ try {
4404
+ this.subscriptionReplaySource = "reconnect_replay";
4405
+ try {
4406
+ await this.ensureJoined(reconnectLiveRevision);
4407
+ } finally {
4408
+ this.subscriptionReplaySource = null;
4409
+ this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
4410
+ }
4411
+ await Promise.all([
4412
+ this.replaceFromHistory({
4413
+ liveRevisionAtStart: reconnectLiveRevision,
4414
+ modeRevisionAtStart: reconnectModeRevision
4415
+ }),
4416
+ this.syncReplayFromSession()
4417
+ ]);
4418
+ } catch {
4419
+ }
4420
+ });
3671
4421
  }
3672
4422
  /** @internal 首次连接:加载历史 + 加入房间。 */
3673
- async _bootstrap() {
3674
- await this.syncTurnsFromHistory({ suppressErrors: false });
3675
- await this.ensureJoined();
3676
- await this.syncReplayFromSession();
4423
+ _bootstrap() {
4424
+ const commandRecoveryGeneration = this.beginCommandRecovery();
4425
+ return this.enqueueSubscription(async () => {
4426
+ await this.replaceFromHistory({ suppressErrors: false });
4427
+ this.subscriptionReplaySource = "bootstrap_replay";
4428
+ try {
4429
+ await this.ensureJoined();
4430
+ } finally {
4431
+ this.subscriptionReplaySource = null;
4432
+ }
4433
+ this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
4434
+ await this.syncReplayFromSession();
4435
+ });
4436
+ }
4437
+ /** 订阅生命周期串行执行;一次失败只由自己的调用方观察,不截断后续重连。 */
4438
+ enqueueSubscription(operation) {
4439
+ const run = this.subscriptionQueue.then(operation);
4440
+ this.subscriptionQueue = run.catch(() => void 0);
4441
+ return run;
3677
4442
  }
3678
4443
  /** 改回放是 owner 专属(服务端 PATCH 走 _get_owned_session)。 */
3679
4444
  get canControlReplay() {
@@ -3730,10 +4495,16 @@ var AgentSession = class _AgentSession {
3730
4495
  this.applyTurn(turn);
3731
4496
  }
3732
4497
  /** @internal 历史回放通道(event_format: raw)。 */
3733
- _handleTurnEvents(events) {
4498
+ _handleTurnEvents(events, replayed = false) {
3734
4499
  if (!events.length) return;
4500
+ if (events.some(
4501
+ (event) => event.type === "compaction:end" || event.type === "compaction:error" || event.type === "compaction:abort"
4502
+ )) {
4503
+ this.manualCompactionPending = false;
4504
+ }
4505
+ this.dispatchRawCommands(events);
3735
4506
  const terminalChatEndStatus = extractTerminalChatEndStatus(events);
3736
- if (!terminalChatEndStatus && hasChatRunEvent2(events)) {
4507
+ if (!terminalChatEndStatus && hasChatRunEvent(events) && this.state.status !== "waiting_for_input") {
3737
4508
  this.markRunningIfIdle();
3738
4509
  }
3739
4510
  if (this.state.turns.length) {
@@ -3742,7 +4513,10 @@ var AgentSession = class _AgentSession {
3742
4513
  const updates = this.projectionBuilder.processBatch(events);
3743
4514
  for (const update of updates) {
3744
4515
  if (update.kind === "upsert") {
3745
- this.applyTurn(update.turn);
4516
+ this.applyTurn(
4517
+ update.turn,
4518
+ replayed ? this.subscriptionReplaySource ?? "bootstrap_replay" : "live"
4519
+ );
3746
4520
  } else if (update.kind === "workspace_changed") {
3747
4521
  this.emitter.emit("workspaceChanged", {});
3748
4522
  }
@@ -3797,16 +4571,26 @@ var AgentSession = class _AgentSession {
3797
4571
  }
3798
4572
  /** @internal */
3799
4573
  _handleRewind(checkpointId) {
3800
- this.update((s) => ({ ...s, mode: null, errorMessage: null }));
3801
- void this.syncTurnsFromHistory({ clearOnEmpty: true });
4574
+ this.timelineEpoch += 1;
4575
+ this.invalidateOlderPaging();
4576
+ this.update((s) => ({
4577
+ ...s,
4578
+ mode: null,
4579
+ errorMessage: null,
4580
+ nextBefore: null,
4581
+ loadingOlder: false
4582
+ }));
4583
+ void this.replaceFromHistory();
3802
4584
  this.emitter.emit("rewind", { checkpointId });
3803
4585
  }
3804
4586
  /** @internal */
3805
4587
  _handleReplayMismatch(data) {
3806
4588
  const message = this.pendingReplayMessage ?? data.actual_message ?? "";
3807
4589
  const mode = this.pendingReplayMode;
4590
+ const kbIds = this.pendingReplayKbIds;
3808
4591
  this.pendingReplayMessage = null;
3809
4592
  this.pendingReplayMode = void 0;
4593
+ this.pendingReplayKbIds = void 0;
3810
4594
  const respond = (decision) => {
3811
4595
  void this.runtime.emitWithAck(
3812
4596
  "chat:send",
@@ -3814,7 +4598,8 @@ var AgentSession = class _AgentSession {
3814
4598
  session_id: this.sessionId,
3815
4599
  message,
3816
4600
  mode,
3817
- replay_decision: decision
4601
+ replay_decision: decision,
4602
+ kb_ids: kbIds && kbIds.length > 0 ? kbIds : void 0
3818
4603
  },
3819
4604
  3e4
3820
4605
  ).then((response) => {
@@ -3855,9 +4640,9 @@ var AgentSession = class _AgentSession {
3855
4640
  detail,
3856
4641
  status,
3857
4642
  loopId,
3858
- metadata: isRecord5(notification.metadata) ? notification.metadata : void 0
4643
+ metadata: isRecord(notification.metadata) ? notification.metadata : void 0
3859
4644
  });
3860
- if (notificationType === "bg:started" && isRecord5(notification.metadata)) {
4645
+ if (notificationType === "bg:started" && isRecord(notification.metadata)) {
3861
4646
  const taskId = typeof notification.metadata.task_id === "string" ? notification.metadata.task_id : "";
3862
4647
  if (taskId) {
3863
4648
  this.emitter.emit("backgroundTask", {
@@ -3879,12 +4664,12 @@ var AgentSession = class _AgentSession {
3879
4664
  this.emitter.emit("artifact", event);
3880
4665
  }
3881
4666
  /** @internal 加入房间的应答里带 is_running,据此校准流式状态。 */
3882
- _applyRunningState(isRunning) {
4667
+ _applyRunningState(isRunning, liveRevisionAtJoin = this.liveRevision) {
3883
4668
  if (isRunning) {
3884
4669
  this.update((s) => ({ ...s, status: "running", isStreaming: true }));
3885
4670
  } else if (this.state.isStreaming) {
3886
4671
  this.update((s) => ({ ...s, isStreaming: false }));
3887
- void this.syncTurnsFromHistory();
4672
+ void this.replaceFromHistory({ liveRevisionAtStart: liveRevisionAtJoin });
3888
4673
  }
3889
4674
  }
3890
4675
  // ---------------------------------------------------------------- 内部实现
@@ -3905,14 +4690,36 @@ var AgentSession = class _AgentSession {
3905
4690
  completeChat(status) {
3906
4691
  this.finishStreaming(status);
3907
4692
  this.recentChatEndAt = Date.now();
3908
- void this.syncTurnsFromHistory();
4693
+ void this.replaceFromHistory();
3909
4694
  this.emitter.emit("chatEnd", { status });
3910
4695
  }
3911
- applyTurn(turn) {
4696
+ markTurnFresh(turnId) {
4697
+ this.liveRevision += 1;
4698
+ this.liveRevisions.set(turnId, this.liveRevision);
4699
+ }
4700
+ markOptimisticTurnPending(turnId) {
4701
+ this.pendingOptimisticTurnIds.add(turnId);
4702
+ this.markTurnFresh(turnId);
4703
+ }
4704
+ pruneOptimisticLiveRevisions() {
4705
+ const retained = new Set(
4706
+ this.state.turns.map((turn) => turn.turn_id).filter((turnId) => turnId.startsWith("local-user-"))
4707
+ );
4708
+ for (const turnId of this.liveRevisions.keys()) {
4709
+ if (turnId.startsWith("local-user-") && !retained.has(turnId)) {
4710
+ this.pendingOptimisticTurnIds.delete(turnId);
4711
+ this.liveRevisions.delete(turnId);
4712
+ }
4713
+ }
4714
+ }
4715
+ applyTurn(turn, source = "live") {
3912
4716
  this.flushPatches();
4717
+ this.markTurnFresh(turn.turn_id);
4718
+ if (extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
3913
4719
  const previous = this.state;
3914
4720
  this.update((s) => upsertTurn(s, turn));
3915
- this.emitTurnDerivedEvents(previous, turn);
4721
+ this.pruneOptimisticLiveRevisions();
4722
+ this.emitTurnDerivedEvents(previous, turn, source);
3916
4723
  }
3917
4724
  schedulePatchFlush() {
3918
4725
  if (this.patchFlushHandle != null) return;
@@ -3945,11 +4752,15 @@ var AgentSession = class _AgentSession {
3945
4752
  this.pendingTurnPatches.clear();
3946
4753
  for (const patch of patches) {
3947
4754
  const previous = this.state;
3948
- this.update((s) => applyTurnPatch(s, patch));
4755
+ const next = applyTurnPatch(previous, patch);
4756
+ if (next === previous) continue;
4757
+ this.liveRevision += 1;
4758
+ this.liveRevisions.set(patch.turn_id, this.liveRevision);
3949
4759
  const turn = patch.data.turn;
3950
- if (turn) {
3951
- this.emitTurnDerivedEvents(previous, turn);
3952
- }
4760
+ if (turn && extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
4761
+ this.setState(next);
4762
+ this.pruneOptimisticLiveRevisions();
4763
+ if (turn) this.emitTurnDerivedEvents(previous, turn);
3953
4764
  }
3954
4765
  if (this.patchFlushCancel) {
3955
4766
  this.patchFlushCancel();
@@ -3958,7 +4769,7 @@ var AgentSession = class _AgentSession {
3958
4769
  }
3959
4770
  }
3960
4771
  /** turn 数据落地后统一派生语义事件(指令 / 工具调用 / 消息 / 模式 / 预览 / OOM)。 */
3961
- emitTurnDerivedEvents(previous, turn) {
4772
+ emitTurnDerivedEvents(previous, turn, source = "live") {
3962
4773
  const mode = extractModeFromBlocks(turn.blocks);
3963
4774
  if (mode && previous.mode !== mode) {
3964
4775
  this.emitter.emit("modeChange", { mode });
@@ -3987,7 +4798,7 @@ var AgentSession = class _AgentSession {
3987
4798
  title: ui.title ?? "\u5DE5\u5177\u9884\u89C8"
3988
4799
  });
3989
4800
  }
3990
- if (block.type === "system_notification" && isRecord5(block.content)) {
4801
+ if (block.type === "system_notification" && isRecord(block.content)) {
3991
4802
  this._handleSystemNotification(turn.loop_id, block.content);
3992
4803
  }
3993
4804
  }
@@ -4007,7 +4818,7 @@ var AgentSession = class _AgentSession {
4007
4818
  const finished = toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled";
4008
4819
  if (finished && !this.finishedToolCallIds.has(toolCall.id)) {
4009
4820
  this.finishedToolCallIds.add(toolCall.id);
4010
- this.emitter.emit("toolResult", { toolCall: info, turn });
4821
+ this.emitter.emit("toolResult", { toolCall: info, turn, source });
4011
4822
  if (isOomResult(toolCall.result)) {
4012
4823
  this.notifySandboxOom(toolCall.id);
4013
4824
  }
@@ -4024,17 +4835,36 @@ var AgentSession = class _AgentSession {
4024
4835
  dispatchCommand(turn, block) {
4025
4836
  if (!isToolBridgeContent(block.content)) return;
4026
4837
  const { action, payload } = block.content;
4027
- let payloadKey = "";
4028
- try {
4029
- payloadKey = JSON.stringify(payload ?? null);
4030
- } catch {
4031
- payloadKey = String(payload ?? "");
4838
+ const deliveryKey = block.delivery_id ? commandDeliveryKey(block.delivery_id) : `legacy-tool-command:${turn.turn_id}:${block.tool_call_id ?? ""}:${action}:${JSON.stringify(payload)}`;
4839
+ this.deliverCommand(deliveryKey, action, payload, block.tool_call_id ?? void 0);
4840
+ }
4841
+ dispatchRawCommands(events) {
4842
+ for (const event of events) {
4843
+ if (event.type !== "loop:tool_bridge") continue;
4844
+ const deliveryId = String(event.payload.delivery_id ?? "");
4845
+ const bridge = event.payload.bridge;
4846
+ if (!deliveryId || !isToolBridgeContent(bridge)) continue;
4847
+ this.deliverCommand(
4848
+ commandDeliveryKey(deliveryId),
4849
+ bridge.action,
4850
+ bridge.payload,
4851
+ String(event.payload.tool_call_id ?? "") || void 0
4852
+ );
4032
4853
  }
4033
- const deliveryKey = [block.tool_call_id ?? turn.id, action, payloadKey].join("\0");
4854
+ }
4855
+ deliverCommand(deliveryKey, action, payload, toolCallId) {
4856
+ if (this.bufferedCommandDeliveries) {
4857
+ this.bufferedCommandDeliveries.push({ deliveryKey, action, payload, toolCallId });
4858
+ return;
4859
+ }
4860
+ this.deliverCommandNow({ deliveryKey, action, payload, toolCallId });
4861
+ }
4862
+ deliverCommandNow(command) {
4863
+ const { deliveryKey, action, payload, toolCallId } = command;
4034
4864
  if (this.deliveredCommandKeys.has(deliveryKey)) return;
4035
4865
  this.deliveredCommandKeys.add(deliveryKey);
4036
- const meta = { toolCallId: block.tool_call_id ?? void 0 };
4037
- this.emitter.emit("command", { action, payload, toolCallId: meta.toolCallId });
4866
+ const meta = { toolCallId };
4867
+ this.emitter.emit("command", { action, payload, toolCallId });
4038
4868
  this.commands.dispatch(action, payload, meta);
4039
4869
  }
4040
4870
  notifySandboxOom(key) {
@@ -4043,49 +4873,146 @@ var AgentSession = class _AgentSession {
4043
4873
  this.update((s) => addErrorMessage(s, OOM_MESSAGE));
4044
4874
  this.emitter.emit("sandboxOom", { toolCallId: key });
4045
4875
  }
4046
- async syncTurnsFromHistory(options = {}) {
4047
- const { clearOnEmpty = false, suppressErrors = true, preserveChangesSince } = options;
4048
- const turnsAtRequestStart = this.state.turns;
4049
- try {
4050
- const turns = await this.runtime.fetchTurns(this.sessionId);
4051
- if (turns.length === 0 && !clearOnEmpty) return;
4052
- this.update((state) => {
4053
- const mergeBaseline = preserveChangesSince ?? turnsAtRequestStart;
4054
- if (state.turns === mergeBaseline) return withTurns(state, turns);
4055
- const historyById = new Map(turns.map((turn) => [turn.turn_id, turn]));
4056
- let merged = withTurns(state, turns);
4057
- for (const current of state.turns) {
4058
- const fromHistory = historyById.get(current.turn_id);
4059
- if (!fromHistory || current.sequence > fromHistory.sequence) {
4060
- merged = upsertTurn(merged, current);
4061
- }
4876
+ replaceFromHistory(options = {}) {
4877
+ if (this.disposed) return Promise.resolve();
4878
+ const operationEpoch = this.timelineEpoch;
4879
+ const liveRevisionAtStart = options.liveRevisionAtStart ?? this.liveRevision;
4880
+ const modeRevisionAtStart = options.modeRevisionAtStart ?? this.modeRevision;
4881
+ this.invalidateOlderPaging();
4882
+ this.replaceInFlight += 1;
4883
+ const operation = async () => {
4884
+ let previousNextBefore = null;
4885
+ try {
4886
+ if (this.disposed || operationEpoch !== this.timelineEpoch) return;
4887
+ this.invalidateOlderPaging();
4888
+ previousNextBefore = this.state.nextBefore;
4889
+ this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
4890
+ const page = await this.runtime.fetchTurnsPage(this.sessionId);
4891
+ if (this.disposed || operationEpoch !== this.timelineEpoch) return;
4892
+ if (!this.joined) this.seedToolCallDeliveryBaseline(page.turns);
4893
+ this.update((state) => {
4894
+ const next = withTurns(
4895
+ state,
4896
+ replaceWindow(state.turns, page.turns, liveRevisionAtStart, {
4897
+ current: this.liveRevision,
4898
+ byTurnId: this.liveRevisions
4899
+ })
4900
+ );
4901
+ return {
4902
+ ...next,
4903
+ mode: this.modeRevision === modeRevisionAtStart ? page.currentMode : state.mode,
4904
+ nextBefore: page.nextBefore,
4905
+ loadingOlder: false
4906
+ };
4907
+ });
4908
+ this.pruneOptimisticLiveRevisions();
4909
+ this.projectionBuilder._seedTurns(this.state.turns);
4910
+ } catch (error) {
4911
+ if (!this.disposed && operationEpoch === this.timelineEpoch) {
4912
+ this.update((state) => ({
4913
+ ...state,
4914
+ nextBefore: previousNextBefore,
4915
+ loadingOlder: false
4916
+ }));
4062
4917
  }
4063
- return merged;
4064
- });
4065
- this.replayCommandsFromTurns(turns);
4066
- } catch (error) {
4067
- if (!suppressErrors) throw error;
4918
+ if (options.suppressErrors === false) throw error;
4919
+ } finally {
4920
+ this.replaceInFlight -= 1;
4921
+ }
4922
+ };
4923
+ const run = this.replaceQueue.then(operation);
4924
+ this.replaceQueue = run.catch(() => void 0);
4925
+ return run;
4926
+ }
4927
+ invalidateOlderPaging() {
4928
+ this.pagingGeneration += 1;
4929
+ this.olderPending = null;
4930
+ }
4931
+ beginCommandRecovery() {
4932
+ this.commandRecoveryGeneration += 1;
4933
+ this.commandRestoreRetryAttempt = 0;
4934
+ this.bufferedCommandDeliveries ??= [];
4935
+ if (this.commandRestoreRetryHandle !== null) {
4936
+ globalThis.clearTimeout(this.commandRestoreRetryHandle);
4937
+ this.commandRestoreRetryHandle = null;
4068
4938
  }
4939
+ return this.commandRecoveryGeneration;
4069
4940
  }
4070
- /**
4071
- * 把历史里的指令补投给宿主页面。
4072
- *
4073
- * 重开会话、断线补数时,地图高亮这类"页面当前状态"必须能恢复——
4074
- * 否则刷新一次页面,之前智能体让页面做的事就全丢了。
4075
- * 只回放指令:toolCall / message 这些事件如果按历史全量重放,
4076
- * 监听器会在挂载瞬间收到几百条陈年事件。
4077
- * dispatch 内部按 deliveredCommandKeys 去重,重复调用安全。
4078
- */
4079
- replayCommandsFromTurns(turns) {
4941
+ scheduleHistoricalCommandRestore(generation) {
4942
+ const restore = async () => {
4943
+ if (this.disposed || generation !== this.commandRecoveryGeneration) return;
4944
+ try {
4945
+ const commands = await this.runtime.fetchCommands(this.sessionId);
4946
+ if (this.disposed) return;
4947
+ for (const command of commands) {
4948
+ if (this.disposed) return;
4949
+ this.deliverCommandNow({
4950
+ deliveryKey: commandDeliveryKey(command.deliveryId),
4951
+ action: command.action,
4952
+ payload: command.payload,
4953
+ toolCallId: command.toolCallId
4954
+ });
4955
+ }
4956
+ if (generation !== this.commandRecoveryGeneration) return;
4957
+ this.commandRestoreRetryAttempt = 0;
4958
+ this.flushBufferedCommands(generation);
4959
+ } catch {
4960
+ if (!this.disposed && generation === this.commandRecoveryGeneration && this.commandRestoreRetryHandle === null) {
4961
+ const retryDelay = Math.min(
4962
+ COMMAND_RESTORE_RETRY_BASE_MS * 2 ** this.commandRestoreRetryAttempt,
4963
+ COMMAND_RESTORE_RETRY_MAX_MS
4964
+ );
4965
+ this.commandRestoreRetryAttempt += 1;
4966
+ this.commandRestoreRetryHandle = globalThis.setTimeout(() => {
4967
+ this.commandRestoreRetryHandle = null;
4968
+ if (!this.disposed && generation === this.commandRecoveryGeneration) {
4969
+ this.scheduleHistoricalCommandRestore(generation);
4970
+ }
4971
+ }, retryDelay);
4972
+ }
4973
+ }
4974
+ };
4975
+ const run = this.commandRestoreQueue.then(restore);
4976
+ this.commandRestoreQueue = run.catch(() => void 0);
4977
+ }
4978
+ flushBufferedCommands(generation) {
4979
+ const pending = [...this.bufferedCommandDeliveries ?? []];
4980
+ this.bufferedCommandDeliveries = [];
4981
+ let index = 0;
4982
+ while (index < pending.length) {
4983
+ if (this.disposed) return;
4984
+ if (generation !== this.commandRecoveryGeneration) {
4985
+ const currentGenerationBuffer = this.bufferedCommandDeliveries ?? [];
4986
+ this.bufferedCommandDeliveries = [
4987
+ ...pending.slice(index),
4988
+ ...currentGenerationBuffer
4989
+ ];
4990
+ return;
4991
+ }
4992
+ this.deliverCommandNow(pending[index]);
4993
+ index += 1;
4994
+ const reentrantBuffer = this.bufferedCommandDeliveries;
4995
+ if (index === pending.length && reentrantBuffer && reentrantBuffer.length > 0) {
4996
+ pending.push(...reentrantBuffer);
4997
+ this.bufferedCommandDeliveries = [];
4998
+ }
4999
+ }
5000
+ if (!this.disposed && generation === this.commandRecoveryGeneration) {
5001
+ this.bufferedCommandDeliveries = null;
5002
+ }
5003
+ }
5004
+ /** 首次订阅前的 REST 历史建立语义事件基线,随后同一工具的订阅回放不再投递。 */
5005
+ seedToolCallDeliveryBaseline(turns) {
4080
5006
  for (const turn of turns) {
4081
- for (const block of turn.blocks) {
4082
- if (block.type === "tool_bridge" && isToolBridgeContent(block.content)) {
4083
- this.dispatchCommand(turn, block);
5007
+ for (const toolCall of turn.tool_calls) {
5008
+ this.seenToolCallIds.add(toolCall.id);
5009
+ if (toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled") {
5010
+ this.finishedToolCallIds.add(toolCall.id);
4084
5011
  }
4085
5012
  }
4086
5013
  }
4087
5014
  }
4088
- ensureJoined() {
5015
+ ensureJoined(liveRevisionBoundary = this.liveRevision) {
4089
5016
  if (this.joined) return Promise.resolve();
4090
5017
  if (this.joinPending) return this.joinPending;
4091
5018
  const join = this.runtime.emitWithAck(
@@ -4093,8 +5020,7 @@ var AgentSession = class _AgentSession {
4093
5020
  {
4094
5021
  session_id: this.sessionId,
4095
5022
  event_format: "raw",
4096
- resume: true,
4097
- stream_tokens: this.runtime.streamTokens
5023
+ resume: true
4098
5024
  },
4099
5025
  5e3
4100
5026
  ).then((response) => {
@@ -4104,7 +5030,7 @@ var AgentSession = class _AgentSession {
4104
5030
  );
4105
5031
  }
4106
5032
  this.joined = true;
4107
- this._applyRunningState(Boolean(response?.is_running));
5033
+ this._applyRunningState(Boolean(response?.is_running), liveRevisionBoundary);
4108
5034
  }).finally(() => {
4109
5035
  this.joinPending = null;
4110
5036
  });
@@ -4167,16 +5093,28 @@ var SessionHub = class {
4167
5093
  * 只有最后一份归还才退订房间——否则 StrictMode 双跑、路由抖动这类"连两次
4168
5094
  * 只释放一次"的场景会把仍在用的会话退订掉,之后所有 turn/chat 事件静默丢失。
4169
5095
  */
4170
- async connect(sessionId) {
5096
+ async connect(sessionId, options = {}) {
4171
5097
  const existing = this.sessions.get(sessionId);
4172
5098
  if (existing && !existing.isDisposed) {
4173
5099
  this.hold(sessionId);
5100
+ try {
5101
+ options.setup?.(existing);
5102
+ } catch (error) {
5103
+ existing.dispose();
5104
+ throw error;
5105
+ }
4174
5106
  return this.bootstrapPending.get(sessionId) ?? existing;
4175
5107
  }
4176
5108
  this.ensureBound();
4177
5109
  const session = new AgentSession(sessionId, this.runtimeFor(), this.connection);
4178
5110
  this.sessions.set(sessionId, session);
4179
5111
  this.hold(sessionId);
5112
+ try {
5113
+ options.setup?.(session);
5114
+ } catch (error) {
5115
+ session.dispose();
5116
+ throw error;
5117
+ }
4180
5118
  this.ensureConnected();
4181
5119
  const bootstrap = session._bootstrap().then(() => session);
4182
5120
  this.bootstrapPending.set(sessionId, bootstrap);
@@ -4231,7 +5169,6 @@ var SessionHub = class {
4231
5169
  }
4232
5170
  runtimeFor() {
4233
5171
  return {
4234
- streamTokens: this.client.options?.streamTokens !== false,
4235
5172
  emitEvent: (event, data) => {
4236
5173
  ;
4237
5174
  this.client.socket().emit(event, data);
@@ -4247,7 +5184,8 @@ var SessionHub = class {
4247
5184
  });
4248
5185
  }),
4249
5186
  ensureConnected: () => this.ensureConnected(),
4250
- fetchTurns: (sessionId) => this.client.sessions.getSessionTurns(sessionId),
5187
+ fetchTurnsPage: (sessionId, before) => this.client.sessions.getSessionTurnsPage(sessionId, { before }),
5188
+ fetchCommands: (sessionId) => this.client.sessions.getSessionTurnCommands(sessionId),
4251
5189
  fetchSessionInfo: (sessionId) => this.client.sessions.getSession(sessionId),
4252
5190
  updateReplay: (sessionId, payload) => this.client.sessions.updateReplaySession(sessionId, payload).then((result) => result.replay_state ?? null),
4253
5191
  listDir: (sessionId, dirPath) => this.client.sessions.listDir(sessionId, dirPath),
@@ -4339,9 +5277,25 @@ var SessionHub = class {
4339
5277
  socket.on("turn:end", (data) => {
4340
5278
  for (const session of this.route(data.session_id)) session._handleTurnEnd(data);
4341
5279
  });
4342
- socket.on("turn:events", (data) => {
4343
- for (const session of this.route(data.session_id)) session._handleTurnEvents(data.events ?? []);
4344
- });
5280
+ socket.on(
5281
+ "turn:timing",
5282
+ (data) => {
5283
+ for (const session of this.route(data.session_id)) {
5284
+ session._handleTurnEvents([
5285
+ { type: "llm:first_token:delivered", payload: data }
5286
+ ]);
5287
+ }
5288
+ }
5289
+ );
5290
+ socket.on(
5291
+ "turn:events",
5292
+ (data, acknowledge) => {
5293
+ acknowledgeTurnEvents(acknowledge);
5294
+ for (const session of this.route(data.session_id)) {
5295
+ session._handleTurnEvents(data.events ?? [], data.replayed === true);
5296
+ }
5297
+ }
5298
+ );
4345
5299
  socket.on("chat:start", (data) => {
4346
5300
  for (const session of this.route(data?.session_id)) session._handleChatStart();
4347
5301
  });
@@ -4469,21 +5423,14 @@ function resolveAuthToken(options) {
4469
5423
  return token ? token : null;
4470
5424
  }
4471
5425
 
4472
- // src/version.ts
4473
- var SDK_NAME = "agent-client";
4474
- var SDK_VERSION = true ? "2610.0.0-beta.4" : "1.1.1";
4475
-
4476
5426
  // src/socket.ts
4477
- function withSdkIdentity(auth) {
4478
- return { ...auth ?? {}, sdk: SDK_NAME, sdk_version: SDK_VERSION };
4479
- }
4480
5427
  function createSocket(options) {
4481
5428
  const token = resolveAuthToken(options);
4482
5429
  return io(options.baseUrl, {
4483
5430
  path: options.path ?? "/socket.io",
4484
5431
  withCredentials: true,
4485
5432
  query: token ? { token } : void 0,
4486
- auth: typeof options.token === "function" ? (cb) => cb(withSdkIdentity(buildSocketAuth(options))) : withSdkIdentity(buildSocketAuth(options)),
5433
+ auth: typeof options.token === "function" ? (cb) => cb(buildSocketAuth(options) ?? {}) : buildSocketAuth(options) ?? {},
4487
5434
  autoConnect: false
4488
5435
  });
4489
5436
  }
@@ -4499,6 +5446,7 @@ var BladeClient = class {
4499
5446
  options;
4500
5447
  auth;
4501
5448
  headless;
5449
+ computers;
4502
5450
  models;
4503
5451
  sessions;
4504
5452
  /** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
@@ -4510,6 +5458,7 @@ var BladeClient = class {
4510
5458
  };
4511
5459
  this.auth = new AuthResource(this);
4512
5460
  this.headless = new HeadlessResource(this);
5461
+ this.computers = new ComputersResource(this);
4513
5462
  this.models = new ModelsResource(this);
4514
5463
  this.sessions = new SessionsResource(this);
4515
5464
  this.hub = new SessionHub(this);
@@ -4864,6 +5813,90 @@ function parseXhrHeaders(rawHeaders) {
4864
5813
  return headers;
4865
5814
  }
4866
5815
 
5816
+ // src/platform-endpoints.ts
5817
+ var PLATFORM_SERVICE_NAMES = [
5818
+ "os",
5819
+ "hub",
5820
+ "agent",
5821
+ "oauth",
5822
+ "llmGateway",
5823
+ "gitea",
5824
+ "tile"
5825
+ ];
5826
+ var EMPTY_PLATFORM_ENDPOINTS = Object.freeze({
5827
+ services: Object.freeze({})
5828
+ });
5829
+ function defaultDocumentBaseUrl() {
5830
+ if (typeof document === "undefined") return void 0;
5831
+ if (document.querySelector("base[href]")) return document.baseURI;
5832
+ if (typeof location !== "undefined" && location.origin) return `${location.origin}/`;
5833
+ return document.baseURI;
5834
+ }
5835
+ function normalizeServiceUrl(value) {
5836
+ if (typeof value !== "string" || !value.trim()) return null;
5837
+ if (value.includes("?") || value.includes("#")) return null;
5838
+ try {
5839
+ const url = new URL(value.trim());
5840
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
5841
+ if (url.username || url.password || url.search || url.hash) return null;
5842
+ return url.toString().replace(/\/$/, "");
5843
+ } catch {
5844
+ return null;
5845
+ }
5846
+ }
5847
+ function parsePlatformEndpoints(value) {
5848
+ if (!isRecord(value) || !isRecord(value.services)) return null;
5849
+ const services = {};
5850
+ for (const name of PLATFORM_SERVICE_NAMES) {
5851
+ if (!(name in value.services)) continue;
5852
+ const url = normalizeServiceUrl(value.services[name]);
5853
+ if (!url) return null;
5854
+ if (name === "agent" && new URL(url).pathname !== "/") return null;
5855
+ services[name] = url;
5856
+ }
5857
+ return { services };
5858
+ }
5859
+ async function loadPlatformEndpoints(options = {}) {
5860
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
5861
+ const baseUrl = options.baseUrl ?? defaultDocumentBaseUrl();
5862
+ if (!fetchImpl || !baseUrl) return EMPTY_PLATFORM_ENDPOINTS;
5863
+ const controller = new AbortController();
5864
+ const timeout = globalThis.setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
5865
+ try {
5866
+ const response = await fetchImpl(new URL("config.json", baseUrl), {
5867
+ cache: "no-store",
5868
+ credentials: "same-origin",
5869
+ headers: { Accept: "application/json" },
5870
+ signal: controller.signal
5871
+ });
5872
+ if (!response.ok) return EMPTY_PLATFORM_ENDPOINTS;
5873
+ return parsePlatformEndpoints(await response.json()) ?? EMPTY_PLATFORM_ENDPOINTS;
5874
+ } catch {
5875
+ return EMPTY_PLATFORM_ENDPOINTS;
5876
+ } finally {
5877
+ globalThis.clearTimeout(timeout);
5878
+ }
5879
+ }
5880
+ function resolveServiceUrl(endpoints, name, path = "") {
5881
+ const baseUrl = endpoints.services[name];
5882
+ if (!baseUrl) return null;
5883
+ if (!path) return baseUrl;
5884
+ const pathReference = path.trimStart();
5885
+ if (path.includes("\\") || pathReference.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(pathReference)) {
5886
+ return null;
5887
+ }
5888
+ const base = new URL(`${baseUrl}/`);
5889
+ const target = new URL(path.replace(/^\/+/, ""), base);
5890
+ if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) {
5891
+ return null;
5892
+ }
5893
+ return target.toString();
5894
+ }
5895
+
5896
+ // src/version.ts
5897
+ var SDK_NAME = "agent-client";
5898
+ var SDK_VERSION = true ? "2610.0.0-beta.40" : "1.1.1";
5899
+
4867
5900
  // src/commands/protocol.ts
4868
5901
  function isCommandEnvelope(value) {
4869
5902
  if (typeof value !== "object" || value === null) return false;
@@ -4967,7 +6000,9 @@ export {
4967
6000
  BladeApiError,
4968
6001
  BladeClient,
4969
6002
  ClientProjectionBuilder,
6003
+ ComputersResource,
4970
6004
  DEFAULT_REPLAY_SPEED,
6005
+ EMPTY_PLATFORM_ENDPOINTS,
4971
6006
  LayoutType,
4972
6007
  ModelsResource,
4973
6008
  SDK_NAME,
@@ -4979,21 +6014,40 @@ export {
4979
6014
  Task,
4980
6015
  TaskStatus,
4981
6016
  acceptedPostChatFollowupCompletesLatestRun,
6017
+ acknowledgeTurnEvents,
4982
6018
  buildMessageContent,
6019
+ canToggleComputer,
6020
+ chatErrorForDisplay,
6021
+ computerDaemonVersion,
6022
+ computerOS,
6023
+ computerPlatformLabel,
6024
+ computerState,
4983
6025
  connectEmbedded,
4984
6026
  contentPreview,
6027
+ contextProjectionData,
4985
6028
  createInitialSessionState,
4986
6029
  createSocket,
4987
6030
  extractTextAttachments,
6031
+ getContextDisplayState,
6032
+ getContextGroupDisplayState,
4988
6033
  getFileParts,
4989
6034
  getImageParts,
4990
6035
  getTextContent,
6036
+ groupAdjacentContextRuns,
4991
6037
  groupMessagesByLoop,
6038
+ hasChatRunEvent,
4992
6039
  isCommandEnvelope,
4993
6040
  isHiddenInternalMessage,
4994
6041
  isInboundEnvelope,
4995
6042
  latestPostChatFollowup,
6043
+ loadPlatformEndpoints,
4996
6044
  normalizeMessageContent,
6045
+ prependOlder,
6046
+ reconcileHistoricalUserTurns,
6047
+ reconcileOptimisticUserTurns,
6048
+ replaceWindow,
6049
+ resolveServiceUrl,
6050
+ sortComputers,
4997
6051
  toReplaySnapshot,
4998
6052
  transformSlashCommand
4999
6053
  };