newmark-agent 0.5.14 → 0.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/conversation-utility-host.bundle.cjs +3373 -1900
  2. package/dist/conversation-utility-host.js +1 -1
  3. package/dist/core/agent.d.ts +105 -14
  4. package/dist/core/agent.js +658 -104
  5. package/dist/core/agentKernel/agent.d.ts +1 -0
  6. package/dist/core/agentKernel/agent.js +9 -0
  7. package/dist/core/agentKernelDiagnostics.d.ts +4 -6
  8. package/dist/core/agentKernelDiagnostics.js +12 -9
  9. package/dist/core/agentKernelRunner.d.ts +5 -0
  10. package/dist/core/agentKernelRunner.js +236 -85
  11. package/dist/core/autoRouter.js +16 -3
  12. package/dist/core/conversationCommandState.d.ts +17 -0
  13. package/dist/core/conversationCommandState.js +140 -0
  14. package/dist/core/conversationKernel.d.ts +25 -4
  15. package/dist/core/conversationKernel.js +347 -83
  16. package/dist/core/conversationListEvent.d.ts +6 -0
  17. package/dist/core/conversationListEvent.js +14 -0
  18. package/dist/core/electronUtilityAgentClient.d.ts +4 -1
  19. package/dist/core/electronUtilityAgentClient.js +4 -4
  20. package/dist/core/electronUtilityRuntimePool.d.ts +8 -2
  21. package/dist/core/electronUtilityRuntimePool.js +11 -5
  22. package/dist/core/installUpdate.js +41 -29
  23. package/dist/core/providerUsageAccounting.d.ts +47 -0
  24. package/dist/core/providerUsageAccounting.js +71 -0
  25. package/dist/core/requestContextEstimate.d.ts +18 -0
  26. package/dist/core/requestContextEstimate.js +54 -0
  27. package/dist/core/subagent.d.ts +89 -5
  28. package/dist/core/subagent.js +264 -41
  29. package/dist/core/subagentCommunication.d.ts +43 -0
  30. package/dist/core/subagentCommunication.js +167 -0
  31. package/dist/core/types.d.ts +32 -1
  32. package/dist/core/utilityAgentProtocol.d.ts +4 -0
  33. package/dist/core/workEventCoalescer.js +4 -1
  34. package/dist/core/wslAgentClient.d.ts +18 -5
  35. package/dist/core/wslAgentClient.js +79 -27
  36. package/dist/core/wslAgentProtocol.d.ts +7 -0
  37. package/dist/core/wslAgentRuntimePool.d.ts +8 -2
  38. package/dist/core/wslAgentRuntimePool.js +19 -6
  39. package/dist/core/wslRuntimeProcessTree.d.ts +10 -0
  40. package/dist/core/wslRuntimeProcessTree.js +104 -0
  41. package/dist/llm/provider.d.ts +5 -12
  42. package/dist/llm/provider.js +148 -267
  43. package/dist/main.js +582 -356
  44. package/dist/preload.js +3 -3
  45. package/dist/providers/chat-completions.adapter.d.ts +2 -0
  46. package/dist/providers/chat-completions.adapter.js +84 -36
  47. package/dist/providers/provider-adapter.d.ts +2 -0
  48. package/dist/providers/provider-events.d.ts +23 -14
  49. package/dist/providers/provider-events.js +148 -43
  50. package/dist/providers/provider-headers.d.ts +7 -3
  51. package/dist/providers/provider-headers.js +109 -39
  52. package/dist/providers/provider-request-compat.d.ts +6 -0
  53. package/dist/providers/provider-request-compat.js +37 -0
  54. package/dist/providers/responses.adapter.d.ts +2 -0
  55. package/dist/providers/responses.adapter.js +192 -126
  56. package/dist/server.d.ts +9 -2
  57. package/dist/server.js +100 -24
  58. package/dist/tools/index.js +17 -1
  59. package/dist/ui/index.html +2374 -616
  60. package/dist/ui/lucide-sprite.svg +7 -0
  61. package/dist/ui/startup.html +6 -6
  62. package/dist/wsl-agent-host.bundle.cjs +3381 -1906
  63. package/dist/wsl-agent-host.js +9 -4
  64. package/package.json +23 -4
@@ -38,6 +38,9 @@ exports.normalizeIntelligenceTier = normalizeIntelligenceTier;
38
38
  const fs = __importStar(require("fs"));
39
39
  const path = __importStar(require("path"));
40
40
  const crypto = __importStar(require("crypto"));
41
+ const promises_1 = require("node:timers/promises");
42
+ const providerUsageAccounting_1 = require("./providerUsageAccounting");
43
+ const requestContextEstimate_1 = require("./requestContextEstimate");
41
44
  const imageInspect_1 = require("./imageInspect");
42
45
  const conversationAttachments_1 = require("./conversationAttachments");
43
46
  const displayImages_1 = require("./displayImages");
@@ -48,6 +51,7 @@ const index_1 = require("../tools/index");
48
51
  const workspace_1 = require("./workspace");
49
52
  const ssh_1 = require("./ssh");
50
53
  const subagent_1 = require("./subagent");
54
+ const subagentCommunication_1 = require("./subagentCommunication");
51
55
  const compat_1 = require("./compat");
52
56
  const skills_1 = require("./skills");
53
57
  const flow_1 = require("./flow");
@@ -222,6 +226,8 @@ class Agent {
222
226
  lastCompression = null;
223
227
  providerUsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
224
228
  lastProviderUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
229
+ providerUsageAccounting = (0, providerUsageAccounting_1.createAccounting)();
230
+ lastRequestContext = null;
225
231
  compressionCache = [];
226
232
  pendingHistoryRemovals = [];
227
233
  branchMailbox = [];
@@ -264,15 +270,25 @@ class Agent {
264
270
  activeAgentKernelRuntime = null;
265
271
  modelSwitchCompressionPromise = null;
266
272
  activePeerAgents = new Map();
273
+ // Bounded caller-owned peer slots retain protocol compatibility learning
274
+ // between mailbox jobs. They never retain the child Agent or go to disk.
275
+ peerProviderCaches = new Map();
267
276
  awaitingAgentKernelRuntime = false;
268
277
  pendingAgentKernelQueue = [];
269
278
  linkedPlanAccess;
270
279
  subagentContextPersist;
280
+ subagentPersistedHistory = [];
281
+ subagentPersistedCompression = null;
271
282
  agentKernelUserMessageStartSubscribers = [];
272
283
  rootInboxWakeSubscribers = [];
284
+ directRootInboxQueuedIds = new Set();
285
+ directRootWakePending = false;
286
+ rootInboxRetiredSubscribers = [];
273
287
  activeWorkRunId = '';
274
288
  loadedWorkspaceConversationKey = '';
275
289
  managedWorkRunIds = new Set();
290
+ /** Public deltas since the last completed response, retained only until its boundary. */
291
+ pendingPublicWorkText = new Map();
276
292
  finalizingWorkRunId = '';
277
293
  agentRunService = null;
278
294
  agentRunByWorkRunId = new Map();
@@ -308,6 +324,7 @@ class Agent {
308
324
  recentChecks: [],
309
325
  };
310
326
  rootInboxListener = (message) => this.deliverRootInboxMessage(message);
327
+ boundSubagentManagerKey = '';
311
328
  agentOnly;
312
329
  runtimeActorId;
313
330
  runtimeLifecycleRole;
@@ -430,6 +447,12 @@ class Agent {
430
447
  this.toolDefinitionCache.clear();
431
448
  this.status = 'idle';
432
449
  }
450
+ /** Selecting the composer mode does not start a Goal or reset a running Flow. */
451
+ selectConversationMode(mode) {
452
+ this.mode = ['build', 'plan', 'chat', 'goal', 'flow'].includes(mode) ? mode : 'build';
453
+ this.invalidateSystemPrompt();
454
+ this.saveWorkspaceConversationState(true);
455
+ }
433
456
  currentConversationFlowSelection() {
434
457
  return this.flow?.name
435
458
  ? { name: this.flow.name, pc: Math.max(0, Math.floor(Number(this.flowPc) || 0)) }
@@ -1036,18 +1059,20 @@ class Agent {
1036
1059
  this.routeAttemptStartedAt = Date.now();
1037
1060
  }
1038
1061
  async waitForPlannedRouteRetry(explicitDelayMs) {
1039
- if (explicitDelayMs !== undefined) {
1040
- if (explicitDelayMs <= 0)
1041
- return;
1042
- await new Promise(resolve => setTimeout(resolve, explicitDelayMs));
1043
- return;
1044
- }
1062
+ // Keep the original Build owner through the entire backoff. Stopping a
1063
+ // healthy request between attempts must not wait out a 60-second timer.
1064
+ const signal = this.activeProcessSignal();
1065
+ throwIfAgentAborted(signal);
1045
1066
  const waitBudgetMs = Math.max(0, Math.min(15_000, this.lastRouteDecision?.retryBudgetMs ?? 5_000));
1046
- const delay = Math.max(0, Math.min(waitBudgetMs, this.lastRouteRetryDelayMs));
1047
- this.lastRouteRetryDelayMs = 0;
1067
+ const delay = explicitDelayMs === undefined
1068
+ ? Math.max(0, Math.min(waitBudgetMs, this.lastRouteRetryDelayMs))
1069
+ : Math.max(0, explicitDelayMs);
1070
+ if (explicitDelayMs === undefined)
1071
+ this.lastRouteRetryDelayMs = 0;
1048
1072
  if (!delay)
1049
1073
  return;
1050
- await new Promise(resolve => setTimeout(resolve, delay));
1074
+ await (0, promises_1.setTimeout)(delay, undefined, { signal });
1075
+ throwIfAgentAborted(signal);
1051
1076
  }
1052
1077
  recordRouteSuccess(latencyMs, throughput) {
1053
1078
  const deployment = this.activeDeployment();
@@ -1411,7 +1436,7 @@ class Agent {
1411
1436
  const instruction = useChinese
1412
1437
  ? `请观察图片并生成一个准确、具体的中文短标题,描述图片实际内容。只输出标题,不要加“图片”“示意图”“标题”等前缀,不要使用 Markdown,最多60个汉字。${captionHint ? `调用方提示:${captionHint}` : ''}`
1413
1438
  : `Inspect the image and return one accurate, concrete short title describing its actual visual content. Output only the title, with no "image", "diagram", or "title" prefix, no Markdown, and at most 100 characters.${captionHint ? ` Caller hint: ${captionHint}` : ''}`;
1414
- descriptionPromise = this.withTimeout(provider.chat(this.activeModelName(), [{ role: 'user', content: [
1439
+ descriptionPromise = this.withTimeout(this.chatWithConversationUsage(provider, this.activeModelName(), [{ role: 'user', content: [
1415
1440
  { type: 'text', text: instruction },
1416
1441
  { type: 'image_url', image_url: { url: hydrated.dataUrl } },
1417
1442
  ] }], useChinese ? '你是视觉图片标题生成器,只返回忠实、简洁的标题。' : 'You generate faithful concise titles for visual images. Return only the title.', 0, 120, signal), 30000)
@@ -1535,9 +1560,16 @@ class Agent {
1535
1560
  mode: String(event.mode || this.modeName()),
1536
1561
  model: String(event.model || this.model),
1537
1562
  timestamp: String(event.timestamp || this.nowLabel()),
1563
+ toolCallId: isToolEvent && event.toolCallId ? String(event.toolCallId) : undefined,
1538
1564
  toolName,
1539
1565
  toolArgs: type === 'tool_call' && event.toolArgs ? this.visibleToolArgs(event.toolArgs) : undefined,
1540
1566
  queue: isToolEvent ? undefined : event.queue,
1567
+ queueItems: isToolEvent ? undefined : event.queueItems?.map(item => ({
1568
+ id: item.id, text: item.text, queueMode: item.queueMode, requestedMode: item.requestedMode,
1569
+ images: item.images?.map(image => ({ ...image })),
1570
+ goalObjective: item.goalObjective, runId: item.runId, createdAt: item.createdAt,
1571
+ })),
1572
+ queuePaused: isToolEvent ? undefined : event.queuePaused,
1541
1573
  workspaceId: target.workspaceId,
1542
1574
  workspaceKey: event.workspaceKey,
1543
1575
  runtimeKey: String(raw.runtimeKey || (0, conversationTarget_1.conversationRuntimeKey)(target)),
@@ -2250,6 +2282,9 @@ class Agent {
2250
2282
  const run = this.workRuns.find(item => item.runId === String(runId || ''));
2251
2283
  if (!run)
2252
2284
  return false;
2285
+ if (status !== 'completed' && (run.status === 'running' || run.status === status || (run.status === 'interrupted' && status === 'force_interrupted'))) {
2286
+ this.persistInterruptedPublicWorkText(run);
2287
+ }
2253
2288
  this.syncAgentRunTerminal(run.runId, status, endedAt);
2254
2289
  this.flushPendingHistoryRemovals();
2255
2290
  if (run.status !== 'running') {
@@ -2346,11 +2381,23 @@ class Agent {
2346
2381
  conversationId: run.target.conversationId,
2347
2382
  });
2348
2383
  }
2384
+ persistInterruptedPublicWorkText(run) {
2385
+ const chunks = this.pendingPublicWorkText.get(run.runId);
2386
+ this.pendingPublicWorkText.delete(run.runId);
2387
+ const content = chunks?.join('');
2388
+ if (!content?.trim())
2389
+ return;
2390
+ // This is an incomplete public reply, not a successful final answer or a
2391
+ // model-history message. A single durable boundary replaces live deltas.
2392
+ this.emitWorkEvent({ type: 'response', content, runId: run.runId, conversationId: run.target.conversationId });
2393
+ }
2349
2394
  emitWorkEvent(input) {
2350
2395
  if (input.type === 'start' && !this.workRuns.some(run => run.runId === this.activeWorkRunId && run.status === 'running')) {
2351
2396
  this.beginConversationWorkRun(input.runId || crypto.randomUUID(), this.currentConversationTarget(input.conversationId));
2352
2397
  }
2353
2398
  const activeRun = this.workRuns.find(run => run.runId === (input.runId || this.activeWorkRunId));
2399
+ if (activeRun && input.type === 'error')
2400
+ this.persistInterruptedPublicWorkText(activeRun);
2354
2401
  const managedTurnBoundary = input.type === 'done'
2355
2402
  && !!activeRun
2356
2403
  && this.managedWorkRunIds.has(activeRun.runId)
@@ -2374,9 +2421,16 @@ class Agent {
2374
2421
  mode: input.mode || this.modeName(),
2375
2422
  model: input.model || this.model,
2376
2423
  timestamp: input.timestamp || this.nowLabel(),
2424
+ toolCallId: isToolEvent ? input.toolCallId : undefined,
2377
2425
  toolName,
2378
2426
  toolArgs: publishedType === 'tool_call' && input.toolArgs ? this.visibleToolArgs(input.toolArgs) : undefined,
2379
2427
  queue: isToolEvent ? undefined : input.queue,
2428
+ queueItems: isToolEvent ? undefined : input.queueItems?.map(item => ({
2429
+ id: item.id, text: item.text, queueMode: item.queueMode, requestedMode: item.requestedMode,
2430
+ images: item.images?.map(image => ({ ...image })),
2431
+ goalObjective: item.goalObjective, runId: item.runId, createdAt: item.createdAt,
2432
+ })),
2433
+ queuePaused: isToolEvent ? undefined : input.queuePaused,
2380
2434
  workspaceId: input.workspaceId || activeRun?.target.workspaceId || this.currentConversationTarget(input.conversationId).workspaceId,
2381
2435
  workspaceKey: input.workspaceKey,
2382
2436
  runtimeKey: input.runtimeKey || activeRun?.runtimeKey,
@@ -2389,6 +2443,14 @@ class Agent {
2389
2443
  fallback: input.fallback,
2390
2444
  };
2391
2445
  if (activeRun && this.isPersistablePublicWorkEvent(event)) {
2446
+ if (event.type === 'text' && activeRun.status === 'running') {
2447
+ const chunks = this.pendingPublicWorkText.get(activeRun.runId) || [];
2448
+ chunks.push(event.content);
2449
+ this.pendingPublicWorkText.set(activeRun.runId, chunks);
2450
+ }
2451
+ else if (event.type === 'response' || event.type === 'final_response' || event.type === 'done' || event.type === 'error') {
2452
+ this.pendingPublicWorkText.delete(activeRun.runId);
2453
+ }
2392
2454
  activeRun.sequence = Number(sequence || activeRun.sequence + 1);
2393
2455
  // Streaming text is delivered live, while the complete sanitized API
2394
2456
  // response is persisted once at message_end. This avoids saving hundreds
@@ -2474,8 +2536,10 @@ class Agent {
2474
2536
  attachAgentKernelRuntime(runtime) {
2475
2537
  this.activeAgentKernelRuntime = runtime;
2476
2538
  this.awaitingAgentKernelRuntime = false;
2477
- if (!runtime)
2539
+ if (!runtime) {
2540
+ this.directRootInboxQueuedIds.clear();
2478
2541
  return;
2542
+ }
2479
2543
  // A user stop can arrive while the Native Kernel is still being loaded or
2480
2544
  // assembling its first context. In that handoff window
2481
2545
  // abortActiveKernelRun() can only abort the outer process signal; make a
@@ -2495,12 +2559,53 @@ class Agent {
2495
2559
  this.agentKernelUserMessageStartSubscribers = this.agentKernelUserMessageStartSubscribers.filter(sub => sub !== fn);
2496
2560
  };
2497
2561
  }
2498
- subscribeRootInboxWake(fn) {
2562
+ subscribeRootInboxWake(fn, retired) {
2499
2563
  this.rootInboxWakeSubscribers.push(fn);
2564
+ if (retired)
2565
+ this.rootInboxRetiredSubscribers.push(retired);
2500
2566
  return () => {
2501
2567
  this.rootInboxWakeSubscribers = this.rootInboxWakeSubscribers.filter(sub => sub !== fn);
2568
+ if (retired)
2569
+ this.rootInboxRetiredSubscribers = this.rootInboxRetiredSubscribers.filter(sub => sub !== retired);
2502
2570
  };
2503
2571
  }
2572
+ /** Called only after receipt-bearing tool history has been saved, or on cold replay. */
2573
+ acknowledgeSubagentSettlementReceipts() {
2574
+ if (this.isSubagentRuntime)
2575
+ return [];
2576
+ const receipts = this.history.flatMap(message => {
2577
+ const receipt = message.subagent_settlement_receipt;
2578
+ return message.role === 'tool' && ['subagent_result', 'subagent_read'].includes(String(message.name || ''))
2579
+ && receipt && typeof receipt.peerId === 'string' && Number.isSafeInteger(receipt.revision) && receipt.revision > 0
2580
+ ? [receipt] : [];
2581
+ });
2582
+ if (!receipts.length)
2583
+ return [];
2584
+ const ids = this.subagents.acknowledgeSettlementResults(receipts);
2585
+ if (!ids.length)
2586
+ return [];
2587
+ const retired = new Set(ids);
2588
+ const matches = (content, hidden, clientMessageId) => {
2589
+ if (!hidden || clientMessageId || typeof content !== 'string')
2590
+ return false;
2591
+ const id = content.match(/^\[Root subagent inbox id=([0-9a-f-]{36})\b/i)?.[1];
2592
+ return !!id && retired.has(id);
2593
+ };
2594
+ this.pendingAgentKernelQueue = this.pendingAgentKernelQueue.filter(item => item.queueMode !== 'followUp'
2595
+ || !matches(item.content, item.hiddenUserInput === true, item.clientMessageId));
2596
+ this.activeAgentKernelRuntime?.removeQueuedMessages?.((raw, mode) => {
2597
+ const message = raw;
2598
+ return mode === 'followUp' && matches(message.content, message.hiddenUserInput === true, message.clientMessageId);
2599
+ });
2600
+ const before = this.continuations.length;
2601
+ this.continuations = this.continuations.filter(item => item.queueMode !== 'followUp'
2602
+ || !matches(item.content, item.hiddenUserInput === true, item.clientMessageId));
2603
+ if (this.continuations.length !== before)
2604
+ this.saveWorkspaceConversationState(true);
2605
+ for (const subscriber of this.rootInboxRetiredSubscribers)
2606
+ subscriber(ids);
2607
+ return ids;
2608
+ }
2504
2609
  notifyAgentKernelUserMessageStart(content, clientMessageId) {
2505
2610
  const text = String(content || '');
2506
2611
  if (!text && !clientMessageId)
@@ -2592,7 +2697,7 @@ class Agent {
2592
2697
  }
2593
2698
  consumeConversationContinuation(match) {
2594
2699
  const index = this.continuations.findIndex(item => match.clientMessageId
2595
- ? item.clientMessageId === match.clientMessageId
2700
+ ? item.clientMessageId === match.clientMessageId && item.queueMode === match.queueMode
2596
2701
  : item.queueMode === match.queueMode && item.content === match.content);
2597
2702
  if (index < 0)
2598
2703
  return false;
@@ -2636,6 +2741,9 @@ class Agent {
2636
2741
  attachments: attachments.length ? attachments : undefined,
2637
2742
  hiddenUserInput: raw.hiddenUserInput === true,
2638
2743
  createdAt: String(raw.createdAt || new Date().toISOString()),
2744
+ visibleUserInput: raw.visibleUserInput,
2745
+ visibleMode: raw.visibleMode,
2746
+ goalObjective: raw.goalObjective,
2639
2747
  });
2640
2748
  }
2641
2749
  return Array.from(deduped.values()).slice(-100);
@@ -2730,9 +2838,9 @@ class Agent {
2730
2838
  this.conversationStateDirty.delete(file);
2731
2839
  this.writeStoredConversationStateNow(pending.state, pending.ws);
2732
2840
  }
2733
- getStoredFlowSuspension(conversationId = this.activeConversationId) {
2734
- const stored = this.readStoredConversationState();
2735
- const stateKey = this.workspaceConversationStateKey(conversationId);
2841
+ getStoredFlowSuspension(conversationId = this.activeConversationId, ws = this.workspace.current) {
2842
+ const stored = this.readStoredConversationState(ws);
2843
+ const stateKey = this.workspaceConversationStateKeyFor(conversationId, ws);
2736
2844
  if (stateKey && stored.flowSuspensions && stored.flowSuspensions[stateKey])
2737
2845
  return stored.flowSuspensions[stateKey];
2738
2846
  const legacy = stored.flowSuspension || null;
@@ -2741,8 +2849,8 @@ class Agent {
2741
2849
  const legacyTargetsThis = !legacy.target || (legacy.target.conversationId === conversationId);
2742
2850
  return legacyTargetsThis ? legacy : null;
2743
2851
  }
2744
- saveStoredFlowSuspension(suspension, conversationId = this.activeConversationId) {
2745
- const stateKey = this.workspaceConversationStateKey(conversationId);
2852
+ saveStoredFlowSuspension(suspension, conversationId = this.activeConversationId, ws = this.workspace.current) {
2853
+ const stateKey = this.workspaceConversationStateKeyFor(conversationId, ws);
2746
2854
  if (!stateKey)
2747
2855
  return;
2748
2856
  // Use the lock-aware latest-state mutator directly. Passing a partial
@@ -2750,7 +2858,7 @@ class Agent {
2750
2858
  // merge the deleted key back from the latest disk snapshot, making Stop,
2751
2859
  // archive, and new-work handoff appear to clear a suspension in memory
2752
2860
  // while it immediately reappears after reload.
2753
- this.mutateStoredConversationState(this.workspace.current, latest => {
2861
+ this.mutateStoredConversationState(ws, latest => {
2754
2862
  const flowSuspensions = { ...(latest.flowSuspensions || {}) };
2755
2863
  if (suspension) {
2756
2864
  flowSuspensions[stateKey] = { ...suspension, updatedAt: suspension.updatedAt || new Date().toISOString() };
@@ -2763,8 +2871,8 @@ class Agent {
2763
2871
  return next;
2764
2872
  });
2765
2873
  }
2766
- clearStoredFlowSuspension(conversationId = this.activeConversationId) {
2767
- this.saveStoredFlowSuspension(null, conversationId);
2874
+ clearStoredFlowSuspension(conversationId = this.activeConversationId, ws = this.workspace.current) {
2875
+ this.saveStoredFlowSuspension(null, conversationId, ws);
2768
2876
  }
2769
2877
  getStoredConversationDraft(conversationId = this.activeConversationId) {
2770
2878
  const key = this.workspaceConversationStateKey(conversationId);
@@ -3070,8 +3178,10 @@ class Agent {
3070
3178
  if (this.isSubagentRuntime)
3071
3179
  return;
3072
3180
  const clean = this.safeConversationId(conversationId || 'default');
3073
- this.subagents.removeRootInboxListener(this.rootInboxListener);
3074
- this.subagents = (0, subagent_1.sharedSubagentManager)(this.subagentManagerKey(clean), {
3181
+ const managerKey = this.subagentManagerKey(clean);
3182
+ if (this.boundSubagentManagerKey !== managerKey)
3183
+ this.subagents.releaseOwnerBinding(this.rootInboxListener);
3184
+ this.subagents = (0, subagent_1.sharedSubagentManager)(managerKey, {
3075
3185
  conversationId: clean,
3076
3186
  rootAgentId: state?.rootAgentId || this.runtimeActorId,
3077
3187
  concurrency: this.subagentConcurrencyLimit(),
@@ -3086,6 +3196,22 @@ class Agent {
3086
3196
  this.saveWorkspaceConversationState();
3087
3197
  },
3088
3198
  });
3199
+ this.boundSubagentManagerKey = managerKey;
3200
+ }
3201
+ /** Release an idle facade; shared peer state remains available to its next owner. */
3202
+ releaseConversationRuntimeBindings() {
3203
+ if (this.activeProcessSignal() || (this.subagents.ownsExecutionBinding(this.rootInboxListener) && this.subagents.hasPendingWork())) {
3204
+ throw new Error('Cannot release a conversation owner with running or queued work');
3205
+ }
3206
+ this.flushWorkspaceConversationState();
3207
+ this.subagents.releaseOwnerBinding(this.rootInboxListener);
3208
+ }
3209
+ assertPeerOwnerCanSwitch(conversationId, workspaceId = this.workspace.current?.id) {
3210
+ const changesTarget = this.safeConversationId(conversationId || 'default') !== this.activeConversationId
3211
+ || workspaceId !== this.workspace.current?.id;
3212
+ if (changesTarget && this.subagents.ownsExecutionBinding(this.rootInboxListener) && this.subagents.hasPendingWork()) {
3213
+ throw new Error('Cannot switch an execution owner with running or queued subagents');
3214
+ }
3089
3215
  }
3090
3216
  persistSubagentState(conversationId, subagentState) {
3091
3217
  if (this.isSubagentRuntime)
@@ -3109,16 +3235,55 @@ class Agent {
3109
3235
  return child.queueActiveKernelMessage(`${marker}\n${message.body}`, 'steer', undefined, undefined, undefined, true);
3110
3236
  }
3111
3237
  deliverRootInboxMessage(message) {
3238
+ if (this.acknowledgeSubagentSettlementReceipts().includes(message.id))
3239
+ return true;
3112
3240
  const marker = `[Root subagent inbox id=${message.id} ${message.kind} from ${message.fromAgentId}]`;
3113
3241
  const prompt = `${marker}\n${message.body}\n\nReview this persisted peer result and summarize or continue the parent task as needed.`;
3242
+ // Stay in the already active kernel so hosted mailbox delivery shares the
3243
+ // same initialized system/tools instead of creating one Build per message.
3244
+ if (this.directRootInboxQueuedIds.has(message.id))
3245
+ return true;
3246
+ if (this.queueActiveKernelMessage(prompt, 'followUp', undefined, undefined, undefined, true)) {
3247
+ this.directRootInboxQueuedIds.add(message.id);
3248
+ return true;
3249
+ }
3114
3250
  for (const sub of this.rootInboxWakeSubscribers) {
3115
3251
  try {
3116
- if (sub(prompt))
3252
+ if (sub(prompt, { wakeup: message.wakeup !== false }))
3117
3253
  return true;
3118
3254
  }
3119
3255
  catch { /* ignore subscriber errors */ }
3120
3256
  }
3121
- return this.queueActiveKernelMessage(prompt, 'followUp', undefined, undefined, undefined, true);
3257
+ if (message.wakeup !== false && !this.rootInboxWakeSubscribers.length && !this.activeProcessSignal()
3258
+ && !this.subagents.isSchedulingPaused() && this.subagents.ownsExecutionBinding(this.rootInboxListener)
3259
+ && !this.directRootWakePending) {
3260
+ this.directRootWakePending = true;
3261
+ queueMicrotask(() => {
3262
+ this.directRootWakePending = false;
3263
+ if (this.rootInboxWakeSubscribers.length || this.activeProcessSignal() || this.subagents.isSchedulingPaused()
3264
+ || !this.subagents.ownsExecutionBinding(this.rootInboxListener))
3265
+ return;
3266
+ const next = this.subagents.readRootInbox().find(item => item.wakeup !== false);
3267
+ if (!next)
3268
+ return;
3269
+ const text = `[Root subagent inbox id=${next.id} ${next.kind} from ${next.fromAgentId}]\n${next.body}`;
3270
+ void this.process({ text, hiddenUserInput: true }).catch(error => {
3271
+ this.recordWorkStatus(`Subagent mailbox wake failed: ${error instanceof Error ? error.message : String(error)}`);
3272
+ });
3273
+ });
3274
+ }
3275
+ return false;
3276
+ }
3277
+ /** A normal activation consumes queued mail without changing its static request prefix. */
3278
+ flushDirectRootInbox() {
3279
+ if (this.isSubagentRuntime || this.rootInboxWakeSubscribers.length)
3280
+ return;
3281
+ for (const message of this.subagents.readRootInbox()) {
3282
+ const marker = `[Root subagent inbox id=${message.id} `;
3283
+ if (this.history.some(item => item.role === 'user' && String(item.content || '').startsWith(marker)))
3284
+ continue;
3285
+ this.deliverRootInboxMessage(message);
3286
+ }
3122
3287
  }
3123
3288
  deliverPeerSettlement(record) {
3124
3289
  const target = record.createdByAgentId === record.id ? this.subagents.rootAgentId : record.createdByAgentId;
@@ -3129,7 +3294,7 @@ class Agent {
3129
3294
  if (delivery.ok)
3130
3295
  return;
3131
3296
  }
3132
- this.subagents.sendRootMessage(record.id, body, 'result');
3297
+ this.subagents.sendRootMessage(record.id, body, 'result', record.settlementRevision);
3133
3298
  }
3134
3299
  recordsForState(state) {
3135
3300
  if (!state)
@@ -3187,7 +3352,8 @@ class Agent {
3187
3352
  // windows on demand ("load earlier messages").
3188
3353
  const windowSize = Math.max(1, Math.floor(Number(options.window) || 0) || 200);
3189
3354
  const totalMessages = fullChatMessages.length;
3190
- const before = Math.max(0, Math.floor(Number(options.before) || 0) || totalMessages);
3355
+ const before = options.before == null ? totalMessages
3356
+ : Math.min(totalMessages, Math.max(0, Math.floor(Number(options.before) || 0)));
3191
3357
  const windowStart = Math.max(0, before - windowSize);
3192
3358
  const chatMessages = fullChatMessages.slice(windowStart, before);
3193
3359
  const workRuns = this.normalizeWorkRuns(isActiveConversation && viewingRuntimeNode ? this.workRuns : (viewedNode?.workRuns || persisted?.workRuns || memory?.workRuns));
@@ -3267,7 +3433,7 @@ class Agent {
3267
3433
  branchIndexDirectory: { ...(tree.nodeIndex || {}) },
3268
3434
  };
3269
3435
  }
3270
- ensureConversationSnapshot(conversationId = this.activeConversationId) {
3436
+ ensureConversationSnapshot(conversationId = this.activeConversationId, options = {}) {
3271
3437
  const clean = this.safeConversationId(conversationId || 'default');
3272
3438
  const stateKey = this.workspaceConversationStateKey(clean);
3273
3439
  if (stateKey) {
@@ -3296,7 +3462,7 @@ class Agent {
3296
3462
  this.writeStoredConversationState(stored);
3297
3463
  }
3298
3464
  }
3299
- return this.getConversationSnapshot(clean);
3465
+ return this.getConversationSnapshot(clean, options);
3300
3466
  }
3301
3467
  rewindConversation(conversationId, messageIndex) {
3302
3468
  const clean = this.safeConversationId(conversationId || 'default');
@@ -3732,24 +3898,69 @@ class Agent {
3732
3898
  'No preamble, no explanation, no Markdown, no quotes, no trailing punctuation.',
3733
3899
  ].join('\n');
3734
3900
  const prompt = `First user input:\n${String(firstUserInput || '').slice(0, 4000)}\n\nConversation title (a few words):`;
3735
- const controller = new AbortController();
3736
- const timer = setTimeout(() => controller.abort(new Error('conversation rename timed out')), 15000);
3737
3901
  try {
3738
3902
  if (signal?.aborted)
3739
3903
  return '';
3740
3904
  const { temperature, reasoningEffort } = provider.intelligenceConfig(intelligence);
3741
- const generated = await provider.chat(modelName, [{ role: 'user', content: prompt }], system, temperature, 64, controller.signal, reasoningEffort);
3905
+ // The title request uses the same transport policy and cancellation owner
3906
+ // as the formal response. A healthy slow model must not be turned into
3907
+ // five failed requests by an unrelated 15-second title deadline.
3908
+ const generated = await this.chatWithConversationUsage(provider, modelName, [{ role: 'user', content: prompt }], system, temperature, 64, signal, reasoningEffort);
3909
+ if (signal?.aborted)
3910
+ return '';
3742
3911
  const title = this.normalizeConversationRenameTitle(generated);
3743
3912
  const source = String(firstUserInput || '').replace(/\s+/g, ' ').trim();
3744
3913
  return title && title !== source ? title : '';
3745
3914
  }
3746
- catch {
3747
- return '';
3748
- }
3749
- finally {
3750
- clearTimeout(timer);
3915
+ catch (error) {
3916
+ if (signal?.aborted)
3917
+ return '';
3918
+ throw error;
3751
3919
  }
3752
3920
  }
3921
+ conversationTitleFailureCause(error, provider) {
3922
+ const message = error instanceof Error ? error.message : '';
3923
+ const status = message.match(/^\[LLM Error:\s*([1-5]\d{2})\]/)?.[1];
3924
+ if (status) {
3925
+ // Extract only a short machine code. Never display the provider's raw
3926
+ // message/body, request URL, headers or echoed credentials in this gate.
3927
+ let code = '';
3928
+ try {
3929
+ const json = JSON.parse(message.slice(message.indexOf('{')));
3930
+ const candidate = typeof json.error?.code === 'string' ? json.error.code : '';
3931
+ if (/^[a-z][a-z0-9_.-]{0,63}$/i.test(candidate)
3932
+ && !(provider.apiKey && candidate.includes(provider.apiKey))
3933
+ && !(provider.baseUrl && candidate.includes(provider.baseUrl)))
3934
+ code = candidate;
3935
+ }
3936
+ catch { /* Non-JSON errors still retain their safe HTTP status. */ }
3937
+ const reason = code === 'get_channel_failed' ? 'no provider channel available' : {
3938
+ '400': 'request rejected', '401': 'authentication failed', '402': 'payment required',
3939
+ '403': 'access denied', '404': 'model or endpoint unavailable', '408': 'request timed out',
3940
+ '429': 'rate limited', '500': 'provider internal error', '502': 'provider gateway error',
3941
+ '503': 'service unavailable', '504': 'provider gateway timed out',
3942
+ }[status] || 'provider request failed';
3943
+ return `HTTP ${status}${code ? ` (${code})` : ''}: ${reason}`;
3944
+ }
3945
+ if (error instanceof Error && error.name === 'TimeoutError')
3946
+ return 'provider request timed out';
3947
+ if (error instanceof SyntaxError)
3948
+ return 'provider returned invalid JSON';
3949
+ return 'provider request failed';
3950
+ }
3951
+ async waitForConversationTitleRetry(delayMs, signal) {
3952
+ if (signal?.aborted || delayMs <= 0)
3953
+ return;
3954
+ await new Promise(resolve => {
3955
+ const finish = () => {
3956
+ clearTimeout(timer);
3957
+ signal?.removeEventListener('abort', finish);
3958
+ resolve();
3959
+ };
3960
+ const timer = setTimeout(finish, delayMs);
3961
+ signal?.addEventListener('abort', finish, { once: true });
3962
+ });
3963
+ }
3753
3964
  /** Legacy completion hook retained as a no-op; naming is pre-response only. */
3754
3965
  maybeAutoRenameConversationFromRun(_run) {
3755
3966
  // Compatibility no-op. Automatic naming is a hard gate on the first
@@ -3765,19 +3976,33 @@ class Agent {
3765
3976
  const retryDelaysMs = [0, 1000, 2000, 4000, 8000];
3766
3977
  if (!messageId || !firstUserInput.trim())
3767
3978
  return false;
3979
+ // Failure state belongs to this title attempt sequence, not the Agent or
3980
+ // another conversation. A later empty title replaces an earlier HTTP error.
3981
+ let lastFailure = 'provider returned an empty or unusable title';
3982
+ const requestTitle = async () => {
3983
+ lastFailure = 'provider returned an empty or unusable title';
3984
+ try {
3985
+ return await this.deriveConversationTitleFromProvider(firstUserInput, provider, modelName, intelligence, signal);
3986
+ }
3987
+ catch (error) {
3988
+ lastFailure = this.conversationTitleFailureCause(error, provider);
3989
+ return '';
3990
+ }
3991
+ };
3992
+ const exhaustedFailure = () => new Error(`Conversation title generation failed; ${lastFailure}. The first Agent request was not started. Retry the first input.`);
3768
3993
  if (!stateKey) {
3769
3994
  // Pure Agent/CLI mode has no workspace conversation file to rename, but
3770
3995
  // still uses the title request as the required first model-availability
3771
3996
  // probe before starting the formal response.
3772
3997
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
3773
- if (await this.deriveConversationTitleFromProvider(firstUserInput, provider, modelName, intelligence, signal))
3998
+ if (await requestTitle())
3774
3999
  return true;
3775
4000
  if (signal?.aborted)
3776
4001
  return false;
3777
4002
  if (attempt < maxAttempts - 1)
3778
- await new Promise(resolve => setTimeout(resolve, retryDelaysMs[attempt]));
4003
+ await this.waitForConversationTitleRetry(retryDelaysMs[attempt], signal);
3779
4004
  }
3780
- return false;
4005
+ throw exhaustedFailure();
3781
4006
  }
3782
4007
  const stored = this.readStoredConversationState(workspace);
3783
4008
  const entry = stored.conversations?.[stateKey];
@@ -3794,7 +4019,7 @@ class Agent {
3794
4019
  if (memory)
3795
4020
  memory.titleRequestMessageId = messageId;
3796
4021
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
3797
- const title = await this.deriveConversationTitleFromProvider(firstUserInput, provider, modelName, intelligence, signal);
4022
+ const title = await requestTitle();
3798
4023
  if (title) {
3799
4024
  const latest = this.readStoredConversationState(workspace);
3800
4025
  const current = latest.conversations?.[stateKey];
@@ -3812,9 +4037,9 @@ class Agent {
3812
4037
  if (signal?.aborted)
3813
4038
  return false;
3814
4039
  if (attempt < maxAttempts - 1)
3815
- await new Promise(resolve => setTimeout(resolve, retryDelaysMs[attempt]));
4040
+ await this.waitForConversationTitleRetry(retryDelaysMs[attempt], signal);
3816
4041
  }
3817
- return false;
4042
+ throw exhaustedFailure();
3818
4043
  }
3819
4044
  firstPersistedUserForTitleGate() {
3820
4045
  const firstUser = this.chatMessages.find(message => message.role === 'user');
@@ -4008,8 +4233,16 @@ class Agent {
4008
4233
  };
4009
4234
  }
4010
4235
  saveWorkspaceConversationState(flush = true) {
4011
- if (this.isSubagentRuntime)
4236
+ if (this.isSubagentRuntime) {
4237
+ if (this.subagentContextPersist && (this.history.length !== this.subagentPersistedHistory.length
4238
+ || this.lastCompression !== this.subagentPersistedCompression
4239
+ || this.history.some((message, index) => message !== this.subagentPersistedHistory[index]))) {
4240
+ this.subagentContextPersist(this.history, this.lastCompression);
4241
+ this.subagentPersistedHistory = this.history.slice();
4242
+ this.subagentPersistedCompression = this.lastCompression;
4243
+ }
4012
4244
  return;
4245
+ }
4013
4246
  const key = this.workspaceConversationKey();
4014
4247
  if (!key)
4015
4248
  return;
@@ -4018,6 +4251,7 @@ class Agent {
4018
4251
  this.workspaceConversations.set(key, {
4019
4252
  chatMessages: [...this.chatMessages],
4020
4253
  history: [...this.history],
4254
+ providerUsage: this.conversationProviderUsage(),
4021
4255
  compressionCache: [...this.compressionCache],
4022
4256
  branchMailbox: [...this.branchMailbox],
4023
4257
  branchCommunication: this.branchCommunicationEnabled,
@@ -4053,6 +4287,7 @@ class Agent {
4053
4287
  title,
4054
4288
  chatMessages: [...this.chatMessages],
4055
4289
  history: [...this.history],
4290
+ providerUsage: this.conversationProviderUsage(),
4056
4291
  compressionCache: [...this.compressionCache],
4057
4292
  branchMailbox: [...this.branchMailbox],
4058
4293
  branchCommunication: this.branchCommunicationEnabled,
@@ -4085,6 +4320,8 @@ class Agent {
4085
4320
  }
4086
4321
  loadWorkspaceConversationState() {
4087
4322
  this.managedWorkRunIds.clear();
4323
+ this.pendingPublicWorkText.clear();
4324
+ this.restoreProviderUsage();
4088
4325
  const key = this.workspaceConversationKey();
4089
4326
  this.loadedWorkspaceConversationKey = key || '';
4090
4327
  if (!key) {
@@ -4109,6 +4346,7 @@ class Agent {
4109
4346
  }
4110
4347
  const saved = this.workspaceConversations.get(key);
4111
4348
  if (saved) {
4349
+ this.restoreProviderUsage(saved.providerUsage);
4112
4350
  this.history = [...saved.history];
4113
4351
  this.compressionCache = saved.compressionCache ? saved.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
4114
4352
  this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
@@ -4136,6 +4374,7 @@ class Agent {
4136
4374
  const stored = this.readStoredConversationState();
4137
4375
  const stateKey = this.workspaceConversationStateKey();
4138
4376
  const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
4377
+ this.restoreProviderUsage(persisted?.providerUsage);
4139
4378
  const repairedHistory = this.repairDanglingToolCalls(persisted?.history ? [...persisted.history] : []);
4140
4379
  this.history = repairedHistory.messages;
4141
4380
  this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
@@ -4182,6 +4421,7 @@ class Agent {
4182
4421
  this.workspaceConversations.set(key, {
4183
4422
  chatMessages: [...this.chatMessages],
4184
4423
  history: [...this.history],
4424
+ providerUsage: this.conversationProviderUsage(),
4185
4425
  compressionCache: [...this.compressionCache],
4186
4426
  branchMailbox: [...this.branchMailbox],
4187
4427
  branchCommunication: this.branchCommunicationEnabled,
@@ -4232,10 +4472,12 @@ class Agent {
4232
4472
  return ws;
4233
4473
  }
4234
4474
  selectWorkspace(id) {
4475
+ this.assertPeerOwnerCanSwitch(this.activeConversationId, id);
4235
4476
  this.saveWorkspaceConversationState();
4236
4477
  return this.applyWorkspaceContext(this.workspace.select(id));
4237
4478
  }
4238
4479
  selectWorkspaceFromStorage(id) {
4480
+ this.assertPeerOwnerCanSwitch(this.activeConversationId, id);
4239
4481
  const selected = this.workspace.select(id);
4240
4482
  if (selected)
4241
4483
  this.config.loadWorkspaceConfig(selected.path);
@@ -4273,6 +4515,7 @@ class Agent {
4273
4515
  }
4274
4516
  setConversation(id) {
4275
4517
  const clean = this.safeConversationId(id || 'default');
4518
+ this.assertPeerOwnerCanSwitch(clean);
4276
4519
  // Conversation runners may bind a target workspace directly before their
4277
4520
  // first setConversation(). Do not save state loaded for another workspace
4278
4521
  // under the new workspace key during that hand-off.
@@ -4285,6 +4528,7 @@ class Agent {
4285
4528
  return this.activeConversationId;
4286
4529
  }
4287
4530
  setConversationFromStorage(id) {
4531
+ this.assertPeerOwnerCanSwitch(id);
4288
4532
  this.activeConversationId = this.safeConversationId(id || 'default');
4289
4533
  const key = this.workspaceConversationKey();
4290
4534
  if (key)
@@ -4294,6 +4538,8 @@ class Agent {
4294
4538
  }
4295
4539
  persistActiveConversationSelection(id, ws = this.workspace.current) {
4296
4540
  const clean = this.safeConversationId(id || 'default');
4541
+ if (ws && this.workspace.current && path.resolve(ws.path) === path.resolve(this.workspace.current.path))
4542
+ this.assertPeerOwnerCanSwitch(clean);
4297
4543
  this.mutateStoredConversationState(ws, latest => ({
4298
4544
  version: 3,
4299
4545
  activeConversationId: clean,
@@ -4319,6 +4565,9 @@ class Agent {
4319
4565
  }
4320
4566
  abortActiveKernelRun(reason = 'unspecified') {
4321
4567
  let aborted = false;
4568
+ if (!this.isSubagentRuntime && (reason === 'user_stop' || reason === 'force_stop')) {
4569
+ this.subagents.broadcastStop(this.runtimeActorId, this.currentWorkRunId() || this.routeTransactionId || 'inactive', reason === 'force_stop');
4570
+ }
4322
4571
  this.subagents.pauseScheduling();
4323
4572
  if (this.activeProcessAbortController && !this.activeProcessAbortController.signal.aborted) {
4324
4573
  const abortError = new Error('Agent run aborted');
@@ -4339,6 +4588,7 @@ class Agent {
4339
4588
  this.pendingAgentKernelQueue = [];
4340
4589
  return aborted;
4341
4590
  }
4591
+ hasRunningSubagents() { return this.activePeerAgents.size > 0; }
4342
4592
  activeProcessSignal() {
4343
4593
  return this.activeProcessAbortController?.signal;
4344
4594
  }
@@ -4575,7 +4825,7 @@ class Agent {
4575
4825
  ].join('\n');
4576
4826
  const maxTokens = Math.max(1024, Math.min(8192, Math.ceil(content.length / 4)) + 512);
4577
4827
  const { temperature } = provider.intelligenceConfig('low');
4578
- const generated = await this.withTimeout(provider.chat(modelName, [{ role: 'user', content: prompt }], system, temperature, maxTokens, signal), 120000);
4828
+ const generated = await this.withTimeout(this.chatWithConversationUsage(provider, modelName, [{ role: 'user', content: prompt }], system, temperature, maxTokens, signal), 120000);
4579
4829
  const summary = String(generated || '').trim();
4580
4830
  if (!summary || /^\[LLM Error(?::|\])/i.test(summary)) {
4581
4831
  return {
@@ -5423,6 +5673,11 @@ class Agent {
5423
5673
  const stored = this.readStoredConversationState(ws);
5424
5674
  stored.conversations = stored.conversations || {};
5425
5675
  const previous = stateKey ? stored.conversations[stateKey] : undefined;
5676
+ const providerUsage = source.conversationProviderUsage ? source.conversationProviderUsage() : source.providerUsageTotals ? {
5677
+ version: 1,
5678
+ totals: this.normalizeProviderUsageCounters(source.providerUsageTotals),
5679
+ last: this.normalizeProviderUsageCounters(source.lastProviderUsage),
5680
+ } : previous?.providerUsage;
5426
5681
  const sourceTitleGate = source.getConversationTitleGateState?.() || source.titleGateState;
5427
5682
  const titleRequestMessageId = sourceTitleGate?.titleRequestMessageId === undefined
5428
5683
  ? String(previous?.titleRequestMessageId || '')
@@ -5434,6 +5689,7 @@ class Agent {
5434
5689
  this.workspaceConversations.set(key, {
5435
5690
  chatMessages: normalizedChatMessages,
5436
5691
  history: [...source.history],
5692
+ providerUsage,
5437
5693
  plan,
5438
5694
  linkedPlan,
5439
5695
  subagentState,
@@ -5459,6 +5715,7 @@ class Agent {
5459
5715
  title,
5460
5716
  chatMessages: normalizedChatMessages,
5461
5717
  history: [...source.history],
5718
+ providerUsage,
5462
5719
  plan,
5463
5720
  linkedPlan,
5464
5721
  subagentState,
@@ -5481,6 +5738,7 @@ class Agent {
5481
5738
  if (this.safeConversationId(this.activeConversationId || 'default') === clean) {
5482
5739
  this.chatMessages = normalizedChatMessages;
5483
5740
  this.history = [...source.history];
5741
+ this.restoreProviderUsage(providerUsage);
5484
5742
  this.conversationPlan = plan;
5485
5743
  this.linkedPlan = linkedPlan;
5486
5744
  if (source.subagents)
@@ -5728,22 +5986,143 @@ class Agent {
5728
5986
  buildBlockTokens: estimate(buildBlockAsciiChars, buildBlockNonAsciiChars, buildBlockStructuralChars, true),
5729
5987
  };
5730
5988
  }
5731
- recordProviderUsage(input) {
5732
- const bounded = (value) => Math.max(0, Math.floor(Number(value) || 0));
5733
- const usage = {
5734
- input: bounded(input.input),
5735
- output: bounded(input.output),
5736
- cacheRead: bounded(input.cacheRead),
5737
- cacheWrite: bounded(input.cacheWrite),
5989
+ normalizeProviderUsageCounters(input) {
5990
+ const bounded = (value) => typeof value === 'number' && Number.isFinite(value)
5991
+ ? Math.max(0, Math.floor(value)) : 0;
5992
+ return { input: bounded(input?.input), output: bounded(input?.output), cacheRead: bounded(input?.cacheRead), cacheWrite: bounded(input?.cacheWrite) };
5993
+ }
5994
+ conversationProviderUsage() {
5995
+ return {
5996
+ version: 1, totals: { ...this.providerUsageTotals }, last: { ...this.lastProviderUsage },
5997
+ accounting: { ...this.providerUsageAccounting },
5998
+ requestContext: this.lastRequestContext ? { ...this.lastRequestContext } : null,
5999
+ };
6000
+ }
6001
+ normalizedConversationUsage(saved) {
6002
+ const totals = this.normalizeProviderUsageCounters(saved?.version === 1 ? saved.totals : undefined);
6003
+ const accounting = (0, providerUsageAccounting_1.createAccounting)(saved?.version === 1 ? totals : undefined);
6004
+ if (saved?.version === 1 && saved.accounting) {
6005
+ for (const key of ['requests', 'inputReportedRequests', 'outputReportedRequests', 'cacheReportedRequests', 'cacheEligibleInputTokens', 'cacheReadTokens']) {
6006
+ const value = saved.accounting[key];
6007
+ accounting[key] = typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
6008
+ }
6009
+ accounting.hasLegacyTotals = saved.accounting.hasLegacyTotals === true;
6010
+ accounting.lastInputTokens = typeof saved.accounting.lastInputTokens === 'number' && Number.isFinite(saved.accounting.lastInputTokens)
6011
+ ? Math.max(0, Math.floor(saved.accounting.lastInputTokens)) : null;
6012
+ }
6013
+ let requestContext = null;
6014
+ const context = saved?.version === 1 ? saved.requestContext : null;
6015
+ if (context && typeof context.requestId === 'string' && typeof context.runId === 'string' && typeof context.at === 'string') {
6016
+ requestContext = { requestId: context.requestId, runId: context.runId, model: String(context.model || ''), at: context.at,
6017
+ estimatedTokens: 0, longHistoryTokens: 0, buildBlockTokens: 0, systemPromptTokens: 0, toolSchemaTokens: 0, messageCount: 0, hasImages: context.hasImages === true };
6018
+ for (const key of ['estimatedTokens', 'longHistoryTokens', 'buildBlockTokens', 'systemPromptTokens', 'toolSchemaTokens', 'messageCount', 'inputTokens', 'cacheReadTokens']) {
6019
+ const value = context[key];
6020
+ if (typeof value === 'number' && Number.isFinite(value))
6021
+ requestContext[key] = Math.max(0, Math.floor(value));
6022
+ }
6023
+ }
6024
+ return { version: 1, totals, last: this.normalizeProviderUsageCounters(saved?.version === 1 ? saved.last : undefined), accounting, requestContext };
6025
+ }
6026
+ restoreProviderUsage(saved) {
6027
+ // Legacy counters remain spend evidence, but cannot prove reporting coverage.
6028
+ const usage = this.normalizedConversationUsage(saved);
6029
+ this.providerUsageTotals = usage.totals;
6030
+ this.lastProviderUsage = usage.last;
6031
+ this.providerUsageAccounting = usage.accounting;
6032
+ this.lastRequestContext = usage.requestContext || null;
6033
+ }
6034
+ mutateRequestUsage(request, mutate) {
6035
+ const activeKey = this.workspaceConversationStateKey();
6036
+ if (request.stateKey === activeKey && request.conversationId === this.activeConversationId) {
6037
+ const usage = { version: 1, totals: this.providerUsageTotals, last: this.lastProviderUsage,
6038
+ accounting: this.providerUsageAccounting, requestContext: this.lastRequestContext };
6039
+ mutate(usage);
6040
+ this.lastProviderUsage = usage.last;
6041
+ this.lastRequestContext = usage.requestContext || null;
6042
+ this.saveWorkspaceConversationState(false);
6043
+ return;
6044
+ }
6045
+ // A late auxiliary response belongs to the captured target, not whichever
6046
+ // conversation the user selected while it was running. Do not revive deletion.
6047
+ if (!request.workspace || !request.stateKey)
6048
+ return;
6049
+ this.mutateStoredConversationState(request.workspace, stored => {
6050
+ const entry = stored.conversations?.[request.stateKey];
6051
+ if (!entry)
6052
+ return stored;
6053
+ const usage = this.normalizedConversationUsage(entry.providerUsage);
6054
+ mutate(usage);
6055
+ entry.providerUsage = usage;
6056
+ entry.updatedAt = new Date(Math.max(Date.now(), (Date.parse(entry.updatedAt || '') || 0) + 1)).toISOString();
6057
+ // A different active conversation can already have a debounced snapshot
6058
+ // of this same store. Update only this entry's usage in that snapshot so
6059
+ // its later flush cannot roll back the late measurement or discard B.
6060
+ const storePath = this.workspaceConversationStorePath(request.workspace);
6061
+ const dirtyEntry = storePath ? this.conversationStateDirty.get(storePath)?.state.conversations?.[request.stateKey] : undefined;
6062
+ if (dirtyEntry)
6063
+ dirtyEntry.providerUsage = structuredClone(usage);
6064
+ const memoryKey = `${request.workspace.isInternal ? 'internal' : 'external'}:${path.resolve(request.workspace.path)}::conversation:${request.conversationId}`;
6065
+ const memory = this.workspaceConversations.get(memoryKey);
6066
+ if (memory) {
6067
+ memory.providerUsage = structuredClone(usage);
6068
+ memory.updatedAt = entry.updatedAt;
6069
+ }
6070
+ return stored;
6071
+ });
6072
+ }
6073
+ beginProviderUsageRequest() {
6074
+ const request = {
6075
+ id: crypto.randomUUID(), conversationId: this.activeConversationId,
6076
+ workspace: this.workspace.current ? { ...this.workspace.current } : null,
6077
+ stateKey: this.workspaceConversationStateKey(),
6078
+ accounting: (0, providerUsageAccounting_1.beginAccountingRequest)((0, providerUsageAccounting_1.createAccounting)()),
6079
+ };
6080
+ this.mutateRequestUsage(request, usage => { request.accounting = (0, providerUsageAccounting_1.beginAccountingRequest)(usage.accounting); });
6081
+ return request;
6082
+ }
6083
+ recordRequestContext(request, messages, system, tools, model) {
6084
+ const context = {
6085
+ ...(0, requestContextEstimate_1.estimateSubmittedContext)(messages, system, tools, this.compressionBuildBlockStart(messages)),
6086
+ requestId: request.id, runId: this.currentWorkRunId(), model, at: new Date().toISOString(),
6087
+ };
6088
+ this.mutateRequestUsage(request, usage => { usage.requestContext = context; });
6089
+ }
6090
+ recordProviderUsage(input, request = this.beginProviderUsageRequest()) {
6091
+ this.mutateRequestUsage(request, usage => {
6092
+ (0, providerUsageAccounting_1.applyAccountingUsage)(usage.accounting, usage.totals, request.accounting, input);
6093
+ usage.last = { ...request.accounting.usage };
6094
+ if (usage.requestContext?.requestId === request.id) {
6095
+ if (request.accounting.reported.input)
6096
+ usage.requestContext.inputTokens = request.accounting.usage.input;
6097
+ if (request.accounting.reported.cacheRead)
6098
+ usage.requestContext.cacheReadTokens = request.accounting.usage.cacheRead;
6099
+ }
6100
+ });
6101
+ }
6102
+ async chatWithConversationUsage(provider, ...args) {
6103
+ // Bind before awaiting. The callback runs for successful responses even if
6104
+ // usage is absent; failed HTTP/model-probe requests are not invented spend.
6105
+ const workspace = this.workspace.current ? { ...this.workspace.current } : null;
6106
+ const conversationId = this.activeConversationId;
6107
+ const stateKey = this.workspaceConversationStateKey();
6108
+ let request;
6109
+ const callback = args[7];
6110
+ args[7] = input => {
6111
+ if (!request) {
6112
+ request = { id: crypto.randomUUID(), workspace, conversationId, stateKey, accounting: (0, providerUsageAccounting_1.beginAccountingRequest)((0, providerUsageAccounting_1.createAccounting)()) };
6113
+ this.mutateRequestUsage(request, usage => { request.accounting = (0, providerUsageAccounting_1.beginAccountingRequest)(usage.accounting); });
6114
+ }
6115
+ this.recordProviderUsage(input, request);
6116
+ callback?.(input);
5738
6117
  };
5739
- this.lastProviderUsage = usage;
5740
- this.providerUsageTotals.input += usage.input;
5741
- this.providerUsageTotals.output += usage.output;
5742
- this.providerUsageTotals.cacheRead += usage.cacheRead;
5743
- this.providerUsageTotals.cacheWrite += usage.cacheWrite;
6118
+ return provider.chat(...args);
5744
6119
  }
5745
6120
  contextWindow(modelName = this.model) {
5746
- const estimatedTokens = this.estimateContextTokens();
6121
+ const activeRequest = this.activeAgentKernelRuntime && this.lastRequestContext?.runId === this.currentWorkRunId()
6122
+ ? this.lastRequestContext : null;
6123
+ const historyEstimate = activeRequest ? null
6124
+ : (0, requestContextEstimate_1.estimateSubmittedContext)(this.history, '', [], this.compressionBuildBlockStart(this.history));
6125
+ const estimatedTokens = activeRequest?.estimatedTokens ?? historyEstimate.estimatedTokens;
5747
6126
  // Display and compression must share one window resolution. Both resolve
5748
6127
  // the auto branch through the active (routed) deployment so the UI ring and
5749
6128
  // the compaction trigger stay on the same maxTokens even after an Auto
@@ -5753,14 +6132,15 @@ class Agent {
5753
6132
  const maxTokens = Math.max(1, Number(model?.max_tokens || 0) || 128000);
5754
6133
  const ratio = estimatedTokens / maxTokens;
5755
6134
  const budget = this.compressionBudget(this.history, modelName);
6135
+ const cache = (0, providerUsageAccounting_1.ratioSummary)(this.providerUsageAccounting);
5756
6136
  return {
5757
6137
  estimatedTokens,
5758
6138
  maxTokens,
5759
6139
  ratio,
5760
6140
  warning: ratio >= 1 ? 'over_limit' : ratio >= 0.85 ? 'near_limit' : 'ok',
5761
6141
  model: modelName,
5762
- buildBlockTokens: budget.buildBlockTokens,
5763
- longHistoryTokens: budget.longHistoryTokens,
6142
+ buildBlockTokens: activeRequest?.buildBlockTokens ?? historyEstimate.buildBlockTokens,
6143
+ longHistoryTokens: activeRequest?.longHistoryTokens ?? historyEstimate.longHistoryTokens,
5764
6144
  buildBlockTriggerTokens: budget.buildBlockTriggerTokens,
5765
6145
  longHistoryTriggerTokens: budget.longHistoryTriggerTokens,
5766
6146
  buildBlockRetentionTokens: budget.buildBlockRetentionTokens,
@@ -5775,9 +6155,20 @@ class Agent {
5775
6155
  providerOutputTokens: this.providerUsageTotals.output,
5776
6156
  providerCacheReadTokens: this.providerUsageTotals.cacheRead,
5777
6157
  providerCacheWriteTokens: this.providerUsageTotals.cacheWrite,
5778
- providerCacheReadRatio: this.providerUsageTotals.input > 0
5779
- ? Math.min(1, this.providerUsageTotals.cacheRead / this.providerUsageTotals.input)
5780
- : 0,
6158
+ providerCacheReadRatio: cache.totalRatio,
6159
+ providerKnownCacheReadRatio: cache.knownRatio,
6160
+ providerCacheEligibleInputTokens: cache.denominator,
6161
+ providerUsageRequests: this.providerUsageAccounting.requests,
6162
+ providerUsageInputReportedRequests: this.providerUsageAccounting.inputReportedRequests,
6163
+ providerUsageOutputReportedRequests: this.providerUsageAccounting.outputReportedRequests,
6164
+ providerUsageCacheReportedRequests: this.providerUsageAccounting.cacheReportedRequests,
6165
+ providerUsageHasLegacyTotals: this.providerUsageAccounting.hasLegacyTotals,
6166
+ providerLastInputTokens: this.providerUsageAccounting.lastInputTokens,
6167
+ requestContext: this.lastRequestContext ? { ...this.lastRequestContext } : null,
6168
+ contextEstimateSource: activeRequest ? 'active_request' : 'history',
6169
+ contextEstimateHasImages: activeRequest?.hasImages ?? historyEstimate.hasImages,
6170
+ systemPromptTokens: activeRequest?.systemPromptTokens ?? 0,
6171
+ toolSchemaTokens: activeRequest?.toolSchemaTokens ?? 0,
5781
6172
  };
5782
6173
  }
5783
6174
  resolveWindowModel(modelName) {
@@ -5827,16 +6218,17 @@ class Agent {
5827
6218
  }
5828
6219
  compressionBuildBlockStart(messages) {
5829
6220
  const activeRunId = this.currentWorkRunId();
6221
+ // No active Build means every retained message is historical, including
6222
+ // legacy assistant/tool tails whose old persistence omitted run metadata.
6223
+ if (!activeRunId)
6224
+ return messages.length;
5830
6225
  if (activeRunId) {
5831
6226
  const index = messages.findIndex(message => String(message.run_id || message.runId || '') === activeRunId);
5832
6227
  if (index >= 0)
5833
6228
  return index;
5834
6229
  }
5835
- // dev-0.5.6: 空闲对话(无活动 run)时不得把全部历史算进当前 Build Block,
5836
- // 否则上下文显示窗口的长期历史恒为 0。回退语义:
5837
- // - 存在带 run_id 的消息:boundary 取最后一个 run 的起点之后(该 run 及其
5838
- // 之前的历史属于长期历史,其后无归属的消息属于当前未命名区块);
5839
- // - 完全没有 run_id:全部历史都是长期历史(不存在当前 Build Block)。
6230
+ // Legacy active transcripts may lack the current run tag. Retain the
6231
+ // historical tagged prefix; ordinary new inputs now carry their run_id.
5840
6232
  let lastRunBoundary = -1;
5841
6233
  for (let index = 0; index < messages.length; index += 1) {
5842
6234
  if (String(messages[index]?.run_id || messages[index]?.runId || ''))
@@ -6189,6 +6581,7 @@ class Agent {
6189
6581
  title: this.titleFromMessages(messages, clean),
6190
6582
  chatMessages: messages,
6191
6583
  history: sourceHistory,
6584
+ providerUsage: memory?.providerUsage,
6192
6585
  plan: memory?.plan,
6193
6586
  linkedPlan: memory?.linkedPlan,
6194
6587
  subagentState: memory?.subagentState,
@@ -6286,6 +6679,7 @@ class Agent {
6286
6679
  title: this.titleFromMessages(messages, clean),
6287
6680
  chatMessages: messages,
6288
6681
  history: sourceHistory,
6682
+ providerUsage: memory?.providerUsage,
6289
6683
  plan: memory?.plan,
6290
6684
  linkedPlan: memory?.linkedPlan,
6291
6685
  subagentState: memory?.subagentState,
@@ -7223,7 +7617,7 @@ class Agent {
7223
7617
  try {
7224
7618
  const provider = this.engineModel();
7225
7619
  if (provider)
7226
- corrected = String(await provider.chat(this.activeModelName(), [{ role: 'user', content: prompt }], 'You are a text-only OCR correction assistant. Be conservative and explicit about uncertainty.', 0.05, 3000, signal) || '').trim();
7620
+ corrected = String(await this.chatWithConversationUsage(provider, this.activeModelName(), [{ role: 'user', content: prompt }], 'You are a text-only OCR correction assistant. Be conservative and explicit about uncertainty.', 0.05, 3000, signal) || '').trim();
7227
7621
  }
7228
7622
  catch { }
7229
7623
  return JSON.stringify({
@@ -7236,17 +7630,57 @@ class Agent {
7236
7630
  uncertainty: corrected ? 'preserved' : 'raw_ocr_only',
7237
7631
  }, null, 2);
7238
7632
  }
7239
- engineModel() {
7633
+ engineModel(cache) {
7240
7634
  if (this.forcedProvider) {
7241
7635
  const active = this.activeDeployment();
7242
7636
  if (!this.forcedProviderDeployment
7243
- || (active && deploymentIdentity(active) === this.forcedProviderDeployment))
7637
+ || (active && deploymentIdentity(active) === this.forcedProviderDeployment)) {
7638
+ if (cache) {
7639
+ delete cache.key;
7640
+ delete cache.provider;
7641
+ }
7244
7642
  return this.forcedProvider;
7643
+ }
7245
7644
  }
7246
7645
  const m = this.activeModelConfig();
7247
- if (!m)
7646
+ if (!m) {
7647
+ if (cache) {
7648
+ delete cache.key;
7649
+ delete cache.provider;
7650
+ }
7248
7651
  return null;
7249
- return new provider_1.LLMProvider(m.provider, m.provider_url, m.api_key, m.provider_protocol, this.config.openAIApiMode(), this.config.contextFlag('provider_adapters_v2'), undefined, this.modelThinkingTierMaps(m), this.providerProxyConfig());
7652
+ }
7653
+ return this.modelProvider(m, cache);
7654
+ }
7655
+ modelProvider(m, cache) {
7656
+ const apiMode = this.config.openAIApiMode();
7657
+ const adapters = this.config.contextFlag('provider_adapters_v2');
7658
+ const thinkingMaps = this.modelThinkingTierMaps(m);
7659
+ const proxy = this.providerProxyConfig();
7660
+ const create = () => new provider_1.LLMProvider(m.provider, m.provider_url, m.api_key, m.provider_protocol, apiMode, adapters, undefined, thinkingMaps, proxy);
7661
+ if (!cache)
7662
+ return create();
7663
+ // Match the provider's configured-proxy/environment precedence. A changed
7664
+ // effective proxy must not reuse a dispatcher created for its predecessor.
7665
+ const effectiveProxyUrl = proxy.enabled === false ? '' : (proxy.url.trim()
7666
+ || process.env.HTTPS_PROXY || process.env.HTTP_PROXY
7667
+ || process.env.https_proxy || process.env.http_proxy || '');
7668
+ const orderedMaps = Object.keys(thinkingMaps || {}).sort().map(model => [
7669
+ model,
7670
+ Object.keys(thinkingMaps[model]).sort().map(tier => [tier, thinkingMaps[model][tier]]),
7671
+ ]);
7672
+ const key = crypto.createHash('sha256').update(JSON.stringify([
7673
+ m.provider_id, m.name, m.logical_model_group_id || '',
7674
+ m.provider, m.provider_url, m.api_key, m.provider_protocol,
7675
+ apiMode, adapters, orderedMaps,
7676
+ proxy.enabled, proxy.url, proxy.auth, effectiveProxyUrl,
7677
+ ])).digest('hex');
7678
+ if (cache.key === key && cache.provider)
7679
+ return cache.provider;
7680
+ const provider = create();
7681
+ cache.key = key;
7682
+ cache.provider = provider;
7683
+ return provider;
7250
7684
  }
7251
7685
  /**
7252
7686
  * dev-0.4.3 模型原生思考强度档位映射表(模型名 → thinking_tier_map)。
@@ -7586,6 +8020,7 @@ class Agent {
7586
8020
  if (!explicitFixedModel)
7587
8021
  this.ensureUsableModelSelection();
7588
8022
  let clientMessageId = String(inputEnvelope?.clientMessageId || '').trim();
8023
+ const userMessageId = String(inputEnvelope?.userMessageId || '').trim();
7589
8024
  const inputRunId = String(inputEnvelope?.runId || this.activeWorkRunId || '').trim();
7590
8025
  let rawImages = typeof input === 'string' ? [] : (Array.isArray(input.images) ? input.images : []);
7591
8026
  // dev-0.4.3 Guide 批量续接:同一 Build block 内连续到达的多个 Guide
@@ -7692,7 +8127,7 @@ class Agent {
7692
8127
  const historyContent = images.length
7693
8128
  ? [{ type: 'text', text }, ...images.map(image => ({ type: 'image_url', image_url: { url: image.dataUrl } }))]
7694
8129
  : text;
7695
- if (clientMessageId) {
8130
+ if (clientMessageId && !userMessageId) {
7696
8131
  this.persistGuideMessage(clientMessageId, displayText, inputRunId, historyContent, attachments, String(inputEnvelope?.guideId || ''));
7697
8132
  if (inputRunId) {
7698
8133
  this.recordGuideReceipt({
@@ -7709,19 +8144,30 @@ class Agent {
7709
8144
  }
7710
8145
  }
7711
8146
  else if (!hiddenUserInput) {
7712
- const messageId = crypto.randomUUID();
7713
- this.chatMessages.push({
8147
+ const messageId = userMessageId || crypto.randomUUID();
8148
+ const persistedUser = userMessageId ? this.chatMessages.find(message => message.role === 'user' && message.messageId === userMessageId) : undefined;
8149
+ const userMessage = {
7714
8150
  messageId,
7715
8151
  branchNodeId: this.currentBranchNodeId(),
7716
8152
  role: 'user',
7717
8153
  content: displayText,
7718
8154
  mode: String(inputEnvelope?.visibleMode || this.modeName()),
7719
8155
  model: this.model,
7720
- timestamp: now,
8156
+ timestamp: persistedUser?.timestamp || now,
7721
8157
  attachments: attachments.length ? attachments : undefined,
7722
8158
  runId: this.currentWorkRunId() || undefined,
7723
- });
7724
- this.history.push({ role: 'user', content: historyContent });
8159
+ };
8160
+ if (persistedUser)
8161
+ Object.assign(persistedUser, userMessage);
8162
+ else
8163
+ this.chatMessages.push(userMessage);
8164
+ const persistedHistory = userMessageId ? this.history.find(message => message.role === 'user' && message.user_message_id === userMessageId) : undefined;
8165
+ if (persistedHistory) {
8166
+ persistedHistory.content = historyContent;
8167
+ persistedHistory.run_id = this.currentWorkRunId() || undefined;
8168
+ }
8169
+ else
8170
+ this.history.push({ role: 'user', content: historyContent, run_id: this.currentWorkRunId() || undefined, ...(userMessageId ? { user_message_id: userMessageId } : {}) });
7725
8171
  }
7726
8172
  else {
7727
8173
  this.history.push({
@@ -7734,10 +8180,16 @@ class Agent {
7734
8180
  }
7735
8181
  if (!hiddenUserInput)
7736
8182
  this.recordWorkRunPrimaryPrompt(displayText);
8183
+ const submittedHistoryMessage = userMessageId
8184
+ ? this.history.find(message => message.role === 'user' && message.user_message_id === userMessageId)
8185
+ : this.history.at(-1);
8186
+ const submittedChatMessage = userMessageId
8187
+ ? this.chatMessages.find(message => message.role === 'user' && message.messageId === userMessageId)
8188
+ : [...this.chatMessages].reverse().find(message => message.role === 'user');
7737
8189
  this.saveWorkspaceConversationState(true);
7738
8190
  let firstResponseGateMessageId = '';
7739
8191
  let firstResponseTitleInput = null;
7740
- if (!hiddenUserInput && !clientMessageId && !this.firstAgentResponseStarted) {
8192
+ if (!hiddenUserInput && (!clientMessageId || !!userMessageId) && !this.firstAgentResponseStarted) {
7741
8193
  const firstInput = this.firstPersistedUserForTitleGate();
7742
8194
  if (!firstInput)
7743
8195
  throw new Error('Conversation title generation failed; the first persisted user input is unavailable.');
@@ -7786,6 +8238,14 @@ class Agent {
7786
8238
  // Agent provider request.
7787
8239
  this.notifyAgentKernelUserMessageStart(text, clientMessageId || undefined);
7788
8240
  this.emitWorkEvent({ type: 'start', content: 'Preparing request.' });
8241
+ // Standalone Agent runs acquire their run ID at start, after the title
8242
+ // gate. Bind the captured input now, without moving or bypassing that gate.
8243
+ const startedRunId = this.currentWorkRunId();
8244
+ if (startedRunId && submittedHistoryMessage?.role === 'user')
8245
+ submittedHistoryMessage.run_id = startedRunId;
8246
+ if (startedRunId && !hiddenUserInput && submittedChatMessage)
8247
+ submittedChatMessage.runId = startedRunId;
8248
+ this.saveWorkspaceConversationState(false);
7789
8249
  // Use external opencode CLI engine
7790
8250
  if (this.engine === 'opencode') {
7791
8251
  if (images.length)
@@ -7996,6 +8456,8 @@ class Agent {
7996
8456
  const accepted = await this.handleSubagentContinueEnvelope(args);
7997
8457
  if (!accepted.ok || !accepted.data?.id)
7998
8458
  return accepted.output;
8459
+ if (!['queued', 'working'].includes(accepted.data.status))
8460
+ return accepted.output;
7999
8461
  const settled = await this.waitForSubagentSettlement(accepted.data.id);
8000
8462
  return `${accepted.output}\n${settled?.result || settled?.error || ''}`.trim();
8001
8463
  }
@@ -8005,22 +8467,36 @@ class Agent {
8005
8467
  async handleSubagentContinueEnvelope(args) {
8006
8468
  try {
8007
8469
  const params = JSON.parse(args);
8008
- const name = params.name || params.id || '';
8009
- const prompt = params.message || params.prompt || '';
8470
+ if (!params || typeof params !== 'object' || Array.isArray(params))
8471
+ throw new Error('Arguments must be an object.');
8472
+ if (params.wakeup !== undefined && typeof params.wakeup !== 'boolean')
8473
+ throw new Error('wakeup must be a boolean.');
8474
+ if (params.kind !== undefined && !['directive', 'question', 'result', 'handoff'].includes(params.kind))
8475
+ throw new Error('Invalid message kind.');
8476
+ const name = String(params.id || params.name || '').trim();
8477
+ const selected = (0, subagentCommunication_1.selectSubagentCommunication)(this.history, params);
8478
+ const wakeup = params.wakeup === true;
8479
+ if (name === 'root' || name === this.subagents.rootAgentId) {
8480
+ const delivery = this.subagents.sendRootMessage(this.runtimeActorId, selected.body, params.kind || 'result', undefined, wakeup);
8481
+ return { ok: delivery.ok, output: delivery.ok
8482
+ ? `[Root mailbox message persisted] ${delivery.message?.id} wakeup=${wakeup}`
8483
+ : `[Subagent] ${delivery.error}`, error: delivery.error,
8484
+ metadata: { kind: 'subagent-send', wakeup, selection: selected.selection } };
8485
+ }
8010
8486
  const sa = this.subagents.get(name);
8011
8487
  if (!sa)
8012
8488
  return { ok: false, output: `[Subagent] Not found: ${name}`, error: `Not found: ${name}` };
8013
- if (!prompt)
8014
- return this.subagents.toToolResult(sa.id, '[Subagent] Prompt required.', false);
8015
- sa.messages.push({ role: 'user', content: String(prompt), hidden_user_input: true });
8016
- const delivery = this.subagents.sendMessage(this.runtimeActorId, sa.id, String(prompt), params.kind || 'directive', {
8489
+ const delivery = this.subagents.sendMessage(this.runtimeActorId, sa.id, selected.body, params.kind || 'directive', {
8017
8490
  correlationId: params.correlation_id,
8018
8491
  replyTo: params.reply_to,
8019
- });
8020
- return this.subagents.toToolResult(sa.id, delivery.ok ? `[Subagent message persisted] ${delivery.message?.id}` : `[Subagent] ${delivery.error}`, delivery.ok);
8492
+ }, wakeup);
8493
+ const result = this.subagents.toToolResult(sa.id, delivery.ok ? `[Subagent message persisted] ${delivery.message?.id} wakeup=${wakeup}` : `[Subagent] ${delivery.error}`, delivery.ok);
8494
+ result.metadata = { ...result.metadata, wakeup, selection: selected.selection };
8495
+ return result;
8021
8496
  }
8022
- catch {
8023
- return { ok: false, output: '[Subagent] Invalid continue arguments.', error: 'Invalid continue arguments.' };
8497
+ catch (error) {
8498
+ const detail = error instanceof Error ? error.message : 'Invalid continue arguments.';
8499
+ return { ok: false, output: `[Subagent] ${detail}`, error: detail };
8024
8500
  }
8025
8501
  }
8026
8502
  handleSubagentResult(args) {
@@ -8038,7 +8514,11 @@ class Agent {
8038
8514
  // assistant/user 消息即足以让主 Agent 判断上下文,完整历史按需走
8039
8515
  // subagent_read 的 max_chars 分页读取。
8040
8516
  const transcript = this.subagents.boundedResultTranscript(sa.id);
8041
- return this.subagents.toToolResult(sa.id, `get.subagent("${sa.name}", id="${sa.id}")\nStatus: ${sa.status}\nModel: ${sa.model}\nMode: ${sa.agentMode}\n\nResult:\n${sa.result || ''}\n\nRecent Conversation (bounded):\n${transcript}`, true);
8517
+ const result = this.subagents.toToolResult(sa.id, `get.subagent("${sa.name}", id="${sa.id}")\nStatus: ${sa.status}\nModel: ${sa.model}\nMode: ${sa.agentMode}\n\nResult:\n${sa.result || ''}\n\nRecent Conversation (bounded):\n${transcript}`, true);
8518
+ const receipt = this.subagentSettlementReceipt(sa.id);
8519
+ if (receipt)
8520
+ result.metadata = { ...result.metadata, settlementReceipt: receipt };
8521
+ return result;
8042
8522
  }
8043
8523
  catch {
8044
8524
  return { ok: false, output: '[Subagent] Invalid result arguments.', error: 'Invalid result arguments.' };
@@ -8053,7 +8533,9 @@ class Agent {
8053
8533
  const read = this.subagents.read(this.runtimeActorId, target, Number(params.max_chars || 16000));
8054
8534
  if (!read.ok)
8055
8535
  return { ok: false, output: `[Subagent] ${read.error}`, error: read.error, metadata: { kind: 'subagent-read' } };
8056
- return { ok: true, output: JSON.stringify(read.snapshot, null, 2), data: read.snapshot, metadata: { kind: 'subagent-read' } };
8536
+ const peer = this.subagents.get(target);
8537
+ const receipt = peer && read.snapshot?.peer.result === peer.result ? this.subagentSettlementReceipt(peer.id) : undefined;
8538
+ return { ok: true, output: JSON.stringify(read.snapshot, null, 2), data: read.snapshot, metadata: { kind: 'subagent-read', ...(receipt ? { settlementReceipt: receipt } : {}) } };
8057
8539
  }
8058
8540
  catch {
8059
8541
  return { ok: false, output: '[Subagent] Invalid read arguments.', error: 'Invalid read arguments.' };
@@ -8065,7 +8547,7 @@ class Agent {
8065
8547
  status = String(JSON.parse(args || '{}').status || '');
8066
8548
  }
8067
8549
  catch { }
8068
- const subagents = this.subagents.listAll().filter(record => !status || record.status === status).map(record => this.subagents.toRecord(record.id));
8550
+ const subagents = this.subagents.listSummaries(status);
8069
8551
  return { ok: true, output: JSON.stringify({ conversationId: this.activeConversationId, subagents }, null, 2), metadata: { kind: 'subagent-list' } };
8070
8552
  }
8071
8553
  handleSubagentClose(args) {
@@ -8082,6 +8564,8 @@ class Agent {
8082
8564
  if (actorId === this.subagents.rootAgentId)
8083
8565
  this.activePeerAgents.get(sa.id)?.abortActiveKernelRun();
8084
8566
  const closed = this.subagents.close(sa.id, actorId);
8567
+ if (closed)
8568
+ this.peerProviderCaches.delete(sa.id);
8085
8569
  return this.subagents.toToolResult(sa.id, closed ? `[Subagent '${sa.name}' closed]` : '[Subagent] Close denied.', closed);
8086
8570
  }
8087
8571
  catch {
@@ -8395,7 +8879,7 @@ class Agent {
8395
8879
  }, null, 2);
8396
8880
  try {
8397
8881
  const cfg = provider.intelligenceConfig(this.intelligence);
8398
- const response = await this.withTimeout(provider.chat(this.activeModelName(), [{ role: 'user', content: prompt }], system, Math.min(cfg.temperature, 0.2), Math.min(cfg.maxTokens, 3000), signal), 120000);
8882
+ const response = await this.withTimeout(this.chatWithConversationUsage(provider, this.activeModelName(), [{ role: 'user', content: prompt }], system, Math.min(cfg.temperature, 0.2), Math.min(cfg.maxTokens, 3000), signal), 120000);
8399
8883
  const parsed = this.extractMemoryLabJson(response);
8400
8884
  if (!parsed)
8401
8885
  return deterministic;
@@ -8443,7 +8927,7 @@ class Agent {
8443
8927
  ].join('\n');
8444
8928
  try {
8445
8929
  const cfg = provider.intelligenceConfig(this.intelligence);
8446
- await this.withTimeout(provider.chat(this.activeModelName(), [{ role: 'user', content: JSON.stringify({ index: read.index }, null, 2) }], system, Math.min(cfg.temperature, 0.2), Math.min(cfg.maxTokens, 1200), signal), 120000);
8930
+ await this.withTimeout(this.chatWithConversationUsage(provider, this.activeModelName(), [{ role: 'user', content: JSON.stringify({ index: read.index }, null, 2) }], system, Math.min(cfg.temperature, 0.2), Math.min(cfg.maxTokens, 1200), signal), 120000);
8447
8931
  }
8448
8932
  catch {
8449
8933
  throwIfAgentAborted(signal); /* deterministic reindex still runs */
@@ -8554,10 +9038,18 @@ class Agent {
8554
9038
  }
8555
9039
  const model = assignedModel?.name || (requestedModel === 'auto' ? this.activeModelName() : requestedModel);
8556
9040
  const activeModel = this.activeModelConfig();
8557
- const activeProvider = this.engineModel();
8558
- const assignedProvider = assignedModel && assignedModel.provider_id !== activeModel?.provider_id
8559
- ? new provider_1.LLMProvider(assignedModel.provider, assignedModel.provider_url, assignedModel.api_key, assignedModel.provider_protocol, this.config.openAIApiMode(), this.config.contextFlag('provider_adapters_v2'), undefined, this.modelThinkingTierMaps(assignedModel), this.providerProxyConfig())
8560
- : activeProvider;
9041
+ const peerProviderCache = this.peerProviderCaches.get(id) || {};
9042
+ this.peerProviderCaches.delete(id);
9043
+ this.peerProviderCaches.set(id, peerProviderCache);
9044
+ for (const retainedId of this.peerProviderCaches.keys()) {
9045
+ if (this.peerProviderCaches.size <= 32)
9046
+ break;
9047
+ if (!this.activePeerAgents.has(retainedId) && retainedId !== id)
9048
+ this.peerProviderCaches.delete(retainedId);
9049
+ }
9050
+ const assignedProvider = assignedModel && (assignedModel.provider_id !== activeModel?.provider_id || assignedModel.name !== activeModel?.name)
9051
+ ? this.modelProvider(assignedModel, peerProviderCache)
9052
+ : this.engineModel(peerProviderCache);
8561
9053
  if (!assignedProvider || !model) {
8562
9054
  throw new Error('No LLM configured. Add provider in Settings > Models.');
8563
9055
  }
@@ -8601,7 +9093,7 @@ class Agent {
8601
9093
  child.config.set('models', 'auto_switch', false);
8602
9094
  child.config.set('skills', 'auto_download', 'disabled');
8603
9095
  child.subagents = this.subagents;
8604
- child.subagentContextPersist = (history, compression) => this.subagents.replaceContext(sa.id, history, compression);
9096
+ child.subagentContextPersist = (history, compression, inputCommitted) => this.subagents.replaceContext(sa.id, history, compression, inputCommitted);
8605
9097
  const requestedFlowName = String(flowName || sa.flowName || '').trim();
8606
9098
  if (sa.agentMode === 'flow' && requestedFlowName) {
8607
9099
  const flowDir = path.join(this.rootPath, 'Flow');
@@ -8617,6 +9109,7 @@ class Agent {
8617
9109
  && message.content.startsWith("Peer agent '")));
8618
9110
  const latestPersisted = persistedMessages.at(-1);
8619
9111
  if (latestPersisted?.role === 'user'
9112
+ && !(reason === 'resume' && prompt === subagent_1.SUBAGENT_RECOVERY_PROMPT)
8620
9113
  && (reason === 'spawn' || reason === 'resume' || prompt.includes(latestPersisted.content))) {
8621
9114
  persistedMessages.pop();
8622
9115
  }
@@ -8644,6 +9137,18 @@ class Agent {
8644
9137
  entry.vision_image_path = message.vision_image_path;
8645
9138
  return entry;
8646
9139
  });
9140
+ const compression = sa.metadata?.contextCompression;
9141
+ if (compression && typeof compression.at === 'string' && typeof compression.summary === 'string') {
9142
+ child.lastCompression = {
9143
+ at: compression.at, summary: compression.summary,
9144
+ originalMessages: Number(compression.originalMessages) || 0,
9145
+ compressedMessages: Number(compression.compressedMessages) || 0,
9146
+ originalChars: Number(compression.originalChars) || 0,
9147
+ compressedChars: Number(compression.compressedChars) || compression.summary.length,
9148
+ compressedTokens: Number(compression.compressedTokens) || 0,
9149
+ model: String(compression.model || model), fallback: !!compression.fallback,
9150
+ };
9151
+ }
8647
9152
  child.subscribeAgentKernelUserMessageStart(content => {
8648
9153
  const match = String(content || '').match(/^\[Peer mailbox id=([0-9a-f-]{36})\b/i);
8649
9154
  if (match)
@@ -8694,6 +9199,8 @@ class Agent {
8694
9199
  if (unrelayPeerEvents)
8695
9200
  unrelayPeerEvents();
8696
9201
  this.activePeerAgents.delete(sa.id);
9202
+ if (this.subagents.get(sa.id)?.status === 'closed')
9203
+ this.peerProviderCaches.delete(sa.id);
8697
9204
  }
8698
9205
  }
8699
9206
  persistPeerTranscript(peerId, child) {
@@ -9151,7 +9658,7 @@ class Agent {
9151
9658
  const modelName = String(compressionModel || this.activeModelName()).trim();
9152
9659
  if (!modelName)
9153
9660
  return { summary: fallbackSummary, model: 'local-fallback', fallback: true };
9154
- const generated = await this.withTimeout(provider.chat(modelName, [...prefixMessages, { role: 'user', content: prompt }], system, temperature, budget.summaryTokens, signal), 120000);
9661
+ const generated = await this.withTimeout(this.chatWithConversationUsage(provider, modelName, [...prefixMessages, { role: 'user', content: prompt }], system, temperature, budget.summaryTokens, signal), 120000);
9155
9662
  const generatedText = String(generated || '').trim();
9156
9663
  if (!generatedText || /^\[LLM Error(?::|\])/i.test(generatedText) || /^LLM Error:/i.test(generatedText)) {
9157
9664
  return { summary: fallbackSummary, model: 'local-fallback', fallback: true };
@@ -9472,12 +9979,18 @@ class Agent {
9472
9979
  ].join('\n'));
9473
9980
  }
9474
9981
  if (this.intelligence === 'ultra') {
9475
- parts.push([
9982
+ parts.push(this.isSubagentRuntime ? [
9983
+ '[Ultra Intelligence – Specialist Role]',
9984
+ 'Own the delegated task and complete its implementation or investigation yourself. Preserve the assigned file and responsibility boundaries. You may delegate a concrete independent subtask only when it saves time while you continue useful work; never pass your whole assignment to another peer or create a chain of coordinators.',
9985
+ 'Reuse an existing peer for follow-up work. Send concise, addressed mailbox messages only for new evidence, dependencies, blockers, handoffs or actionable corrections; avoid repeated status checks and unchanged progress messages. Include exact references and verification evidence in your result.',
9986
+ ].join('\n') : [
9476
9987
  '[Ultra Intelligence – Orchestrator Role]',
9477
- 'You are the lead orchestrator. Actively decompose complex tasks into parallel sub-tasks. Use the `SubAgent` tool to create specialized SubAgents for each distinct sub-task. Use `task_create` only for the conversation checklist; it never creates a SubAgent. Coordinate the SubAgent team: assign clear responsibilities, merge results, resolve conflicts, and produce a unified final output. SubAgents are your team; delegate aggressively and manage them as a manager, not just a tool caller.',
9478
- 'Do not attempt to do all the work yourself. Use SubAgents for parallel investigation, verification, implementation, and review.',
9988
+ 'You are the lead orchestrator. Decompose complex work into concrete independent sub-tasks and create specialized peers with `SubAgent`. Give each peer a clear objective, file or responsibility boundary, dependencies, deliverable and acceptance check. Retain useful work locally while peers investigate, implement or review; sequence dependent work and prevent overlapping edits. Merge evidence, resolve conflicts and own the final verification.',
9989
+ 'Use task_create only for the conversation checklist; it never creates a SubAgent. Use the available shared concurrency when independent work exists, but do not create peers merely to fill slots. Reuse existing peers for follow-up work. Prefer concise addressed mailbox updates for new evidence, dependencies, blockers and corrections; avoid redundant broadcasts, repeated status checks and unchanged progress messages.',
9479
9990
  ].join('\n'));
9480
9991
  }
9992
+ if (this.shouldExposeToolInterface())
9993
+ parts.push('Peer communication: subagent_send defaults to wakeup=false and the last visible text-history entry. An active receiver processes either wakeup value normally; use wakeup=true when an inactive peer or root must resume work. Choose concise sender-written summaries or explicit ranges of text/tool history, and forward only evidence relevant to the recipient. Use id="root" for the main agent. Do not retransmit unchanged history or rewrite earlier context to deliver a message.');
9481
9994
  parts.push(this.buildModePrompt());
9482
9995
  const value = this.contextV2.orchestrator.assemble({
9483
9996
  // Keep the complete base prompt in one stable section. The linked_plan
@@ -9535,6 +10048,47 @@ class Agent {
9535
10048
  currentUserInput: '',
9536
10049
  });
9537
10050
  }
10051
+ subagentSettlementReceipt(id) {
10052
+ const peer = this.subagents.get(id);
10053
+ return !this.isSubagentRuntime && peer && typeof peer.result === 'string'
10054
+ && Number.isSafeInteger(peer.settlementRevision) && Number(peer.settlementRevision) > 0
10055
+ ? { peerId: peer.id, revision: peer.settlementRevision } : undefined;
10056
+ }
10057
+ /** Peer jobs share a durable request prefix; credentials only enter its hash. */
10058
+ peerRequestCacheIdentity(systemPrompt, catalog) {
10059
+ if (!this.isSubagentRuntime)
10060
+ return undefined;
10061
+ return crypto.createHash('sha256').update(JSON.stringify({
10062
+ systemPrompt, catalog, model: this.activeModelConfig(),
10063
+ apiMode: this.config.openAIApiMode(), adapters: this.config.contextFlag('provider_adapters_v2'),
10064
+ proxy: this.providerProxyConfig(), intelligence: this.intelligence,
10065
+ compression: this.lastCompression?.at || '',
10066
+ })).digest('hex');
10067
+ }
10068
+ readPeerRequestCache(identity) {
10069
+ if (!identity)
10070
+ return undefined;
10071
+ const saved = this.subagents.get(this.runtimeActorId)?.metadata?.requestCache;
10072
+ if (saved?.version !== 1 || saved.identity !== identity || typeof saved.taskFocus !== 'string'
10073
+ || !Array.isArray(saved.initialTools) || !saved.initialTools.every(name => typeof name === 'string')
10074
+ || !Array.isArray(saved.provisionedTools) || !saved.provisionedTools.every(name => typeof name === 'string'))
10075
+ return undefined;
10076
+ return { ...saved, initialTools: saved.initialTools.slice(), provisionedTools: saved.provisionedTools.slice() };
10077
+ }
10078
+ persistPeerRequestCache(cache) {
10079
+ if (!this.isSubagentRuntime)
10080
+ return;
10081
+ const previous = this.subagents.get(this.runtimeActorId)?.metadata?.requestCache;
10082
+ if (JSON.stringify(previous) !== JSON.stringify(cache)) {
10083
+ this.subagents.patchMetadata(this.runtimeActorId, { requestCache: cache });
10084
+ }
10085
+ }
10086
+ checkpointPeerInput() {
10087
+ // Called at the first real provider boundary, after the input has entered
10088
+ // working history. Commit context and job progress in one durable write.
10089
+ if (this.isSubagentRuntime)
10090
+ this.subagentContextPersist?.(this.history, this.lastCompression, true);
10091
+ }
9538
10092
  cachedToolDefinitions() {
9539
10093
  const identity = JSON.stringify({
9540
10094
  mode: this.mode,