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,15 +38,132 @@ class ConversationKernel {
38
38
  return () => this.listeners.delete(listener);
39
39
  }
40
40
  isRunning(target) {
41
- return !!this.findRuntime(target)?.activePromise;
41
+ const runtime = this.findRuntime(target);
42
+ return !!runtime?.activePromise || runtime?.externalOwner?.active === true;
43
+ }
44
+ conversationOwner(target) {
45
+ return this.findRuntime(target)?.runner || null;
46
+ }
47
+ /** Branch mutations retain the Agent but reload that branch's durable queue. */
48
+ refreshIdleConversation(target) {
49
+ const previous = this.findRuntime(target);
50
+ if (!previous)
51
+ return;
52
+ if (this.isRunning(target) || previous.preparingArchive)
53
+ throw new Error('Cannot refresh a running conversation');
54
+ previous.preparingArchive = true;
55
+ this.finishArchive(target, true, true);
56
+ const restored = this.runtime(previous.target, previous.options, previous.runner);
57
+ restored.queuePaused = previous.queuePaused;
58
+ restored.externalOwner = previous.externalOwner;
59
+ restored.runId = previous.runId;
60
+ this.emitQueueUpdate(restored);
61
+ }
62
+ /** Detach idle owners before config replacement or workspace removal. */
63
+ disposeIdle(workspaceKey) {
64
+ const runtimes = Array.from(this.runtimes.values()).filter(runtime => !workspaceKey || runtime.target.workspaceKey === workspaceKey);
65
+ if (runtimes.some(runtime => runtime.activePromise || runtime.externalOwner || runtime.pendingNextTurn.length || runtime.runner.subagents.hasPendingWork())) {
66
+ throw new Error('Cannot release a conversation with running or queued work');
67
+ }
68
+ for (const runtime of runtimes) {
69
+ runtime.runner.flushWorkspaceConversationState();
70
+ runtime.preparingArchive = true;
71
+ this.finishArchive(runtime.target, true);
72
+ }
73
+ return runtimes.map(runtime => runtime.runtimeKey);
74
+ }
75
+ /** A Flow and its queue share one Agent; no second history writer is created. */
76
+ beginExternalRun(targetInput, options, createOwner, previousQueuePaused) {
77
+ const target = this.normalizeTarget(targetInput);
78
+ const existing = this.findRuntime(target);
79
+ if (existing?.activePromise || existing?.externalOwner?.active)
80
+ throw new Error('Target conversation is already running');
81
+ const runtime = existing || this.runtime(target, options, createOwner());
82
+ runtime.options = { ...options };
83
+ runtime.externalOwner = { active: true, wasPaused: runtime.externalOwner?.wasPaused ?? previousQueuePaused ?? runtime.queuePaused };
84
+ runtime.queuePaused = true;
85
+ runtime.runId ||= (0, crypto_1.randomUUID)();
86
+ runtime.stopRequestedRunId = '';
87
+ this.emitQueueUpdate(runtime);
88
+ return runtime.runner;
89
+ }
90
+ settleExternalRun(target, completed) {
91
+ const runtime = this.findRuntime(target);
92
+ if (!runtime?.externalOwner)
93
+ return;
94
+ const wasPaused = runtime.externalOwner.wasPaused;
95
+ if (completed)
96
+ runtime.externalOwner = undefined;
97
+ else
98
+ runtime.externalOwner.active = false;
99
+ runtime.queuePaused = completed && !runtime.preparingArchive ? wasPaused : true;
100
+ runtime.runner.saveWorkspaceConversationState(true);
101
+ this.emitQueueUpdate(runtime);
102
+ if (!runtime.queuePaused && runtime.pendingNextTurn.length)
103
+ this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
104
+ }
105
+ releaseExternalRun(target, paused) {
106
+ const runtime = this.findRuntime(target);
107
+ if (!runtime)
108
+ return;
109
+ if (runtime.externalOwner?.active)
110
+ throw new Error('Wait for the Flow to stop before releasing its queue');
111
+ runtime.externalOwner = undefined;
112
+ this.setQueuePaused(target, paused);
113
+ }
114
+ async prepareForArchive(target) {
115
+ const runtime = this.findRuntime(target);
116
+ if (!runtime)
117
+ return null;
118
+ if (runtime.runner.subagents.hasPendingWork())
119
+ throw new Error('Cannot archive a conversation with running or queued subagents');
120
+ runtime.preparingArchive = true;
121
+ runtime.queuePaused = true;
122
+ if (runtime.goalContinuationTimer)
123
+ clearTimeout(runtime.goalContinuationTimer);
124
+ runtime.goalContinuationTimer = undefined;
125
+ runtime.runner.abortActiveKernelRun('user_stop');
126
+ if (runtime.activePromise) {
127
+ runtime.stopRequestedRunId = runtime.runId;
128
+ try {
129
+ await runtime.activePromise;
130
+ }
131
+ catch { /* archive owns the terminal outcome */ }
132
+ }
133
+ runtime.runner.flushWorkspaceConversationState();
134
+ return runtime.runner;
135
+ }
136
+ finishArchive(target, completed, preserveOwner = false) {
137
+ const runtime = this.findRuntime(target);
138
+ if (!runtime?.preparingArchive)
139
+ return;
140
+ if (!completed) {
141
+ runtime.preparingArchive = false;
142
+ runtime.queuePaused = true;
143
+ return;
144
+ }
145
+ // Branch refresh reuses this facade immediately; terminal release must
146
+ // detach its shared-manager callbacks as well as the kernel subscriptions.
147
+ if (!preserveOwner)
148
+ runtime.runner.releaseConversationRuntimeBindings();
149
+ runtime.unsubscribe?.();
150
+ runtime.unsubscribePeer?.();
151
+ runtime.unsubscribeRootInboxWake?.();
152
+ runtime.unsubscribeUserMessageStart?.();
153
+ if (runtime.goalContinuationTimer)
154
+ clearTimeout(runtime.goalContinuationTimer);
155
+ this.runtimes.delete(runtime.runtimeKey);
42
156
  }
43
157
  isAnyRunning() {
44
158
  for (const runtime of this.runtimes.values()) {
45
- if (runtime.activePromise)
159
+ if (runtime.activePromise || runtime.externalOwner?.active)
46
160
  return true;
47
161
  }
48
162
  return false;
49
163
  }
164
+ hasRetainedWork() {
165
+ return Array.from(this.runtimes.values()).some(runtime => runtime.activePromise || runtime.externalOwner || runtime.pendingNextTurn.length || runtime.runner.subagents.hasPendingWork());
166
+ }
50
167
  flushPersistence() {
51
168
  this.host.flushWorkspaceConversationState();
52
169
  for (const runtime of this.runtimes.values())
@@ -81,10 +198,12 @@ class ConversationKernel {
81
198
  const visibleMode = message.visibleMode;
82
199
  return {
83
200
  text,
201
+ ...(message.userMessageId || message.clientMessageId ? { userMessageId: message.userMessageId || message.clientMessageId } : {}),
84
202
  ...(images?.length ? { images } : {}),
85
203
  ...(attachments?.length ? { attachments } : {}),
86
204
  ...(visible ? { visibleUserInput: visible } : {}),
87
205
  ...(visibleMode ? { visibleMode } : {}),
206
+ ...(message.goalObjective ? { goalObjective: message.goalObjective } : {}),
88
207
  // A normal user follow-up must never inherit the identity metadata that
89
208
  // was only needed while it lived in the runtime queue. In particular,
90
209
  // do not let a stale/forwarded hiddenUserInput flag classify it as an
@@ -112,13 +231,15 @@ class ConversationKernel {
112
231
  goalObjective: item.message.goalObjective,
113
232
  runId: item.message.runId,
114
233
  createdAt: String(item.message.createdAt || ''),
234
+ images: item.message.images?.map(image => ({ ...image })),
115
235
  }];
116
236
  });
117
237
  }
118
238
  enqueueNext(target, input) {
119
239
  const runtime = this.findRuntime(target);
120
- if (!runtime?.activePromise || !runtime.runId)
121
- throw new Error('Target conversation is not running');
240
+ if (!runtime)
241
+ throw new Error('Target conversation runtime is unavailable');
242
+ runtime.runId ||= (0, crypto_1.randomUUID)();
122
243
  const id = String(input.id || '').trim().slice(0, 200);
123
244
  const text = String(input.text || '').trim();
124
245
  if (!id || !text)
@@ -147,12 +268,18 @@ class ConversationKernel {
147
268
  clientMessageId: id,
148
269
  runId: runtime.runId,
149
270
  createdAt,
271
+ images: input.images,
272
+ visibleUserInput: text,
273
+ visibleMode: String(input.requestedMode || 'build'),
274
+ goalObjective: input.goalObjective,
150
275
  }]);
151
276
  this.trackQueuedMessage(runtime, prompt, 'followUp');
152
277
  this.emitQueueUpdate(runtime);
278
+ if (!runtime.activePromise && !runtime.externalOwner && !runtime.queuePaused)
279
+ this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
153
280
  return this.queueItems(runtime.target).find(item => item.id === id);
154
281
  }
155
- updateQueueItem(target, idInput, textInput) {
282
+ updateQueueItem(target, idInput, textInput, input = {}) {
156
283
  const runtime = this.findRuntime(target);
157
284
  if (!runtime)
158
285
  throw new Error('Target conversation runtime is unavailable');
@@ -163,17 +290,26 @@ class ConversationKernel {
163
290
  const pending = runtime.pendingNextTurn.find(item => typeof item.message !== 'string' && item.message.clientMessageId === id);
164
291
  if (!pending || typeof pending.message === 'string')
165
292
  throw new Error('Queue item is no longer editable');
166
- const oldPrompt = pending.message.text;
293
+ const previous = runtime.runner.conversationContinuations().find(item => item.clientMessageId === id);
167
294
  pending.message.text = `[Next queued while current turn is running]\n${text}`;
168
295
  pending.message.visibleUserInput = text;
169
- runtime.runner.consumeConversationContinuation({ content: oldPrompt, queueMode: pending.queueMode, clientMessageId: id });
296
+ if (input.requestedMode !== undefined)
297
+ pending.message.visibleMode = input.requestedMode;
298
+ if (input.goalObjective !== undefined)
299
+ pending.message.goalObjective = input.goalObjective;
300
+ if (input.images !== undefined)
301
+ pending.message.images = input.images.map(image => ({ ...image }));
170
302
  runtime.runner.retainConversationContinuations([{
303
+ ...previous,
171
304
  content: pending.message.text,
172
305
  queueMode: pending.queueMode,
173
306
  clientMessageId: id,
174
307
  runId: pending.message.runId,
308
+ images: pending.message.images,
309
+ visibleUserInput: text,
310
+ visibleMode: pending.message.visibleMode,
311
+ goalObjective: pending.message.goalObjective,
175
312
  }]);
176
- this.replaceTrackedQueuedMessage(runtime, oldPrompt, pending.message.text, pending.queueMode);
177
313
  this.emitQueueUpdate(runtime);
178
314
  return this.queueItems(runtime.target).find(item => item.id === id);
179
315
  }
@@ -188,7 +324,6 @@ class ConversationKernel {
188
324
  const [removed] = runtime.pendingNextTurn.splice(index, 1);
189
325
  if (typeof removed.message !== 'string') {
190
326
  runtime.runner.consumeConversationContinuation({ content: removed.message.text, queueMode: removed.queueMode, clientMessageId: id });
191
- this.consumeQueuedMessage(runtime, removed.message.text);
192
327
  }
193
328
  this.emitQueueUpdate(runtime);
194
329
  return true;
@@ -237,6 +372,8 @@ class ConversationKernel {
237
372
  const runtime = this.findRuntime(target);
238
373
  if (!runtime)
239
374
  throw new Error('Target conversation runtime is unavailable');
375
+ if (!paused && runtime.externalOwner)
376
+ throw new Error('Release the Flow owner before resuming its queue');
240
377
  runtime.queuePaused = paused;
241
378
  this.emitQueueUpdate(runtime);
242
379
  if (!paused && !runtime.activePromise && runtime.pendingNextTurn.length) {
@@ -245,9 +382,12 @@ class ConversationKernel {
245
382
  return runtime.queuePaused;
246
383
  }
247
384
  queueAction(target, action, input = {}) {
248
- const runtime = this.findRuntime(target);
249
- if (!runtime)
250
- throw new Error('Target conversation runtime is unavailable');
385
+ const normalized = this.normalizeTarget(target);
386
+ const runtime = this.findRuntime(normalized) || this.runtime(normalized, {
387
+ mode: this.host.mode, model: this.host.model, intelligence: this.host.intelligence,
388
+ inputMode: this.host.inputMode, engine: this.host.engine,
389
+ });
390
+ runtime.runId ||= (0, crypto_1.randomUUID)();
251
391
  let receipt;
252
392
  if (action === 'enqueue') {
253
393
  this.enqueueNext(runtime.target, {
@@ -260,7 +400,7 @@ class ConversationKernel {
260
400
  });
261
401
  }
262
402
  else if (action === 'update') {
263
- this.updateQueueItem(runtime.target, String(input.id || ''), String(input.text || ''));
403
+ this.updateQueueItem(runtime.target, String(input.id || ''), String(input.text || ''), input);
264
404
  }
265
405
  else if (action === 'delete') {
266
406
  if (!this.deleteQueueItem(runtime.target, String(input.id || '')))
@@ -272,7 +412,12 @@ class ConversationKernel {
272
412
  else if (action === 'toggle_pause') {
273
413
  this.setQueuePaused(runtime.target, !runtime.queuePaused);
274
414
  }
415
+ else if (action === 'set_pause') {
416
+ this.setQueuePaused(runtime.target, input.paused === true);
417
+ }
275
418
  else if (action === 'guide') {
419
+ if (input.text !== undefined)
420
+ this.updateQueueItem(runtime.target, String(input.id || ''), input.text, input);
276
421
  const item = this.queueItems(runtime.target).find(entry => entry.id === String(input.id || ''));
277
422
  if (!item)
278
423
  throw new Error('Queue item was not found');
@@ -284,6 +429,7 @@ class ConversationKernel {
284
429
  deliveryMode: 'steer',
285
430
  text: item.goalObjective ? `Goal for the current Build:\n${item.goalObjective}` : item.text,
286
431
  goalObjective: item.goalObjective,
432
+ images: runtime.pendingNextTurn.find(entry => typeof entry.message !== 'string' && entry.message.clientMessageId === item.id)?.message?.images,
287
433
  createdAt: item.createdAt || new Date().toISOString(),
288
434
  });
289
435
  if (receipt.status === 'rejected')
@@ -323,11 +469,18 @@ class ConversationKernel {
323
469
  options: question.options.map(option => ({ ...option })),
324
470
  })) : undefined;
325
471
  }
326
- snapshot(target) {
472
+ snapshot(target, options = {}) {
327
473
  const normalized = this.normalizeTarget(target);
328
- const runtime = this.findRuntime(normalized);
474
+ let runtime = this.findRuntime(normalized);
329
475
  const runner = runtime?.runner || this.createRunner(normalized);
330
- const conversationSnapshot = runner.getConversationSnapshot(normalized.conversationId);
476
+ if (!runtime && runner.conversationContinuations().length) {
477
+ runtime = this.runtime(normalized, { mode: runner.mode, model: runner.model, intelligence: runner.intelligence, inputMode: runner.inputMode, engine: runner.engine }, runner);
478
+ // Recovered user input remains visible and manageable until its owner
479
+ // explicitly restores the persisted queue policy or resumes the queue.
480
+ runtime.queuePaused = true;
481
+ runtime.runId ||= (0, crypto_1.randomUUID)();
482
+ }
483
+ const conversationSnapshot = runner.getConversationSnapshot(normalized.conversationId, options);
331
484
  return {
332
485
  ...conversationSnapshot,
333
486
  workRuns: this.bindWorkRunsToRuntimeTarget(conversationSnapshot.workRuns, normalized),
@@ -398,7 +551,7 @@ class ConversationKernel {
398
551
  const canReactivateFinalizingRun = !runtime.activePromise
399
552
  && runtime.guideAcceptanceClosedRunId === runtime.runId
400
553
  && (!requestedRunId || requestedRunId === runtime.runId);
401
- if (!runtime.activePromise && !canReactivateFinalizingRun) {
554
+ if (!runtime.activePromise && !runtime.externalOwner?.active && !canReactivateFinalizingRun) {
402
555
  return { ...base, reason: 'Target conversation is not running' };
403
556
  }
404
557
  let safeImages = [];
@@ -511,6 +664,12 @@ class ConversationKernel {
511
664
  this.activateAcceptedGoal(runtime, envelope.goalObjective);
512
665
  return accepted;
513
666
  }
667
+ if (runtime.externalOwner) {
668
+ const rejected = { ...accepted, status: 'rejected', reason: 'The current Flow Build is not accepting Guide input.', updatedAt: new Date().toISOString() };
669
+ runtime.guideEnvelopes.delete(clientMessageId);
670
+ runtime.guideReceipts.set(clientMessageId, rejected);
671
+ return runtime.runner.recordGuideReceipt(rejected);
672
+ }
514
673
  const deferred = { ...accepted, status: 'deferred', updatedAt: new Date().toISOString() };
515
674
  runtime.guideReceipts.set(clientMessageId, deferred);
516
675
  runtime.runner.recordGuideReceipt(deferred);
@@ -561,7 +720,7 @@ class ConversationKernel {
561
720
  async compressContext(target, options = {}) {
562
721
  const normalized = this.normalizeTarget(target);
563
722
  const runtime = this.findRuntime(normalized);
564
- if (runtime?.activePromise) {
723
+ if (runtime?.activePromise || runtime?.externalOwner?.active) {
565
724
  return { ok: false, error: 'Context compression is unavailable while this conversation is running.' };
566
725
  }
567
726
  const runner = runtime?.runner || this.createRunner(normalized);
@@ -613,8 +772,7 @@ class ConversationKernel {
613
772
  const normalized = this.normalizeTarget(target);
614
773
  const runtime = this.findRuntime(normalized);
615
774
  const runner = runtime?.runner || this.createRunner(normalized);
616
- runner.setMode(mode);
617
- runner.saveWorkspaceConversationState(true);
775
+ runner.selectConversationMode(mode);
618
776
  if (runtime)
619
777
  runtime.options.mode = mode;
620
778
  return runner.mode;
@@ -658,6 +816,18 @@ class ConversationKernel {
658
816
  this.mirrorHostIfTargetActive(runtime);
659
817
  return paused;
660
818
  }
819
+ updateGoal(target, objective) {
820
+ const normalized = this.normalizeTarget(target);
821
+ const runtime = this.findRuntime(normalized);
822
+ if (runtime?.activePromise || runtime?.externalOwner?.active)
823
+ throw new Error('Use Guide to change the Goal of a running conversation');
824
+ const runner = runtime?.runner || this.createRunner(normalized);
825
+ runner.updateGoal(objective);
826
+ runner.selectConversationMode('goal');
827
+ if (runtime)
828
+ runtime.options.mode = 'goal';
829
+ return runner.getConversationSnapshot(normalized.conversationId).goal;
830
+ }
661
831
  clearGoal(target) {
662
832
  const normalized = this.normalizeTarget(target);
663
833
  const runtime = this.findRuntime(normalized);
@@ -688,7 +858,7 @@ class ConversationKernel {
688
858
  runtimeKey: runtime.runtimeKey,
689
859
  runId: runtime.runId,
690
860
  generation: runtime.generation,
691
- running: !!runtime.activePromise,
861
+ running: !!runtime.activePromise || runtime.externalOwner?.active === true,
692
862
  stopRequested: !!runtime.runId && runtime.stopRequestedRunId === runtime.runId,
693
863
  workRuns: this.bindWorkRunsToRuntimeTarget(runtime.runner.getConversationSnapshot(runtime.id).workRuns, runtime.target),
694
864
  };
@@ -697,16 +867,20 @@ class ConversationKernel {
697
867
  const normalized = this.normalizeTarget(target);
698
868
  const runtime = this.findRuntime(target);
699
869
  const runtimeKey = runtime?.runtimeKey || normalized.runtimeKey;
700
- if (runtime && !runtime.activePromise && runtime.runId && runtime.stopRequestedRunId === runtime.runId) {
870
+ // A stale stop cannot pause another generation's peer scheduler or emit
871
+ // control messages before its target identity has been checked.
872
+ if (runtime && expectedRunId && expectedRunId !== runtime.runId) {
873
+ return { action: 'stale', runtimeKey, runId: runtime.runId, generation: runtime.generation, checkpointed: false };
874
+ }
875
+ const peerWork = !!runtime?.runner.subagents.hasPendingWork();
876
+ if (runtime && !runtime.activePromise && !runtime.runner.hasRunningSubagents() && runtime.runId && runtime.stopRequestedRunId === runtime.runId) {
701
877
  this.settleCooperativeStop(runtime, runtime.runId);
702
878
  }
703
- if (!runtime?.activePromise || !runtime.runId) {
879
+ if (!runtime || (!runtime.activePromise && !peerWork) || !runtime.runId) {
704
880
  return { action: 'not_running', runtimeKey, runId: runtime?.runId || undefined, generation: runtime?.generation || undefined, checkpointed: false };
705
881
  }
706
- if (expectedRunId && expectedRunId !== runtime.runId) {
707
- return { action: 'stale', runtimeKey, runId: runtime.runId, generation: runtime.generation, checkpointed: false };
708
- }
709
882
  if (runtime.stopRequestedRunId === runtime.runId && runtime.forceStopArmedRunId === runtime.runId) {
883
+ runtime.runner.abortActiveKernelRun('force_stop');
710
884
  runtime.runner.emitWorkEvent({
711
885
  type: 'status',
712
886
  content: 'Force stopping this conversation run.',
@@ -728,7 +902,7 @@ class ConversationKernel {
728
902
  runtime.runner.abortActiveKernelRun('user_stop');
729
903
  runtime.runner.emitWorkEvent({
730
904
  type: 'status',
731
- content: 'Stop requested. Saving progress and interrupting this conversation.',
905
+ content: 'Stop requested for this conversation and all of its subagents. Saving progress and interrupting execution.',
732
906
  status: 'stopping',
733
907
  runId: runtime.runId,
734
908
  });
@@ -751,6 +925,12 @@ class ConversationKernel {
751
925
  runtime.unsubscribePeer();
752
926
  if (runtime?.unsubscribeRootInboxWake)
753
927
  runtime.unsubscribeRootInboxWake();
928
+ runtime?.unsubscribeUserMessageStart?.();
929
+ if (runtime) {
930
+ runtime.preparingArchive = true;
931
+ if (runtime.goalContinuationTimer)
932
+ clearTimeout(runtime.goalContinuationTimer);
933
+ }
754
934
  if (runtime)
755
935
  this.runtimes.delete(runtime.runtimeKey);
756
936
  const runner = runtime?.runner || this.createRunner(normalized);
@@ -759,6 +939,8 @@ class ConversationKernel {
759
939
  async prompt(message, target, options, queueMode = 'followUp') {
760
940
  const normalized = this.normalizeTarget(target);
761
941
  const active = this.findRuntime(normalized);
942
+ if (active?.preparingArchive)
943
+ throw new Error('This conversation is being archived');
762
944
  if (active?.activePromise) {
763
945
  // A Build block is already running: queue this message. Queued messages
764
946
  // carry no send-time model/mode; the running block keeps its settings and
@@ -819,7 +1001,7 @@ class ConversationKernel {
819
1001
  stopped = true;
820
1002
  this.settleCooperativeStop(runtime, runId);
821
1003
  }
822
- else if (!failed && !runtime.queuePaused && runtime.pendingNextTurn.length > 0) {
1004
+ else if (!failed && !runtime.preparingArchive && !runtime.queuePaused && runtime.pendingNextTurn.length > 0) {
823
1005
  // A renderer/IPC Guide can arrive after the final-drain barrier's
824
1006
  // last check but before this promise settles. Do not leave the
825
1007
  // deferred continuation queued on an idle runtime.
@@ -827,7 +1009,7 @@ class ConversationKernel {
827
1009
  }
828
1010
  }
829
1011
  }
830
- if (!failed && !stopped && runtime.runId === runId)
1012
+ if (!failed && !stopped && !runtime.preparingArchive && runtime.runId === runId)
831
1013
  this.scheduleGoalContinuation(runtime, runId);
832
1014
  if (stopped) {
833
1015
  const settled = this.result(runtime, []);
@@ -928,7 +1110,14 @@ class ConversationKernel {
928
1110
  }
929
1111
  else {
930
1112
  const drained = this.drainQueuedFollowUpMessage(next.message);
931
- lastTokens = await this.runSingle(runtime, drained, next.queueMode);
1113
+ lastTokens = await this.runPendingQueueItem(runtime, next, async () => {
1114
+ if (typeof drained !== 'string' && drained.visibleMode && ['build', 'chat', 'plan', 'goal'].includes(drained.visibleMode)) {
1115
+ runtime.runner.setMode(drained.visibleMode);
1116
+ runtime.options.mode = drained.visibleMode;
1117
+ }
1118
+ this.activateAcceptedGoal(runtime, typeof drained === 'string' ? '' : drained.goalObjective);
1119
+ return this.runSingle(runtime, drained, next.queueMode, typeof next.message === 'string' ? undefined : next.message.clientMessageId);
1120
+ });
932
1121
  if (this.repeatedAutomaticAssistant(runtime, drained, next.queueMode))
933
1122
  break;
934
1123
  }
@@ -1021,32 +1210,28 @@ class ConversationKernel {
1021
1210
  runtime.runner.setModel(pending);
1022
1211
  runtime.options.model = runtime.runner.modelSelectionValue();
1023
1212
  }
1024
- async runSingle(runtime, message, continuationMode) {
1025
- this.syncPendingModel(runtime);
1026
- this.consumeQueuedMessage(runtime, typeof message === 'string' ? message : message.text);
1213
+ async runSingle(runtime, message, continuationMode, continuationClientMessageId) {
1214
+ // Display payloads intentionally omit queue identity. Keep the exact
1215
+ // durable owner separately so equal text cannot consume another item.
1216
+ const clientMessageId = continuationClientMessageId || (typeof message === 'string' ? undefined : message.clientMessageId || message.userMessageId);
1217
+ const durable = clientMessageId
1218
+ ? runtime.runner.conversationContinuations().find(item => item.clientMessageId === clientMessageId)
1219
+ : undefined;
1220
+ const consumedMode = continuationMode || durable?.queueMode;
1221
+ const text = typeof message === 'string' ? message : message.text;
1222
+ let accepted = false;
1223
+ const unsubscribe = durable?.queueMode === 'followUp' && clientMessageId
1224
+ ? runtime.runner.subscribeAgentKernelUserMessageStart((content, acceptedId) => {
1225
+ if (acceptedId ? acceptedId === clientMessageId : content === text)
1226
+ accepted = true;
1227
+ }) : undefined;
1027
1228
  const timeoutMs = this.processTimeoutMs(runtime);
1028
- if (timeoutMs <= 0) {
1029
- let tokens;
1030
- try {
1031
- tokens = await runtime.runner.process(message);
1032
- }
1033
- catch (error) {
1034
- this.consumeFailedAutomaticContinuation(runtime, message, continuationMode);
1035
- throw error;
1036
- }
1037
- if (continuationMode)
1038
- runtime.runner.consumeConversationContinuation({
1039
- content: typeof message === 'string' ? message : message.text,
1040
- queueMode: continuationMode,
1041
- clientMessageId: typeof message === 'string' ? undefined : message.clientMessageId,
1042
- });
1043
- return tokens;
1044
- }
1045
1229
  let timeout;
1046
1230
  try {
1047
- let tokens;
1048
- try {
1049
- tokens = await Promise.race([
1231
+ this.syncPendingModel(runtime);
1232
+ this.consumeQueuedMessage(runtime, text);
1233
+ const tokens = timeoutMs <= 0 ? await runtime.runner.process(message)
1234
+ : await Promise.race([
1050
1235
  runtime.runner.process(message),
1051
1236
  new Promise((_, reject) => {
1052
1237
  timeout = setTimeout(() => {
@@ -1065,22 +1250,27 @@ class ConversationKernel {
1065
1250
  }, timeoutMs);
1066
1251
  }),
1067
1252
  ]);
1253
+ if (unsubscribe && !accepted) {
1254
+ // Workspace/attachment rejection can resolve error tokens without
1255
+ // accepting input. Resolution alone must not remove a queued row.
1256
+ throw new Error('Queued input was not accepted; it remains paused for correction or retry.');
1068
1257
  }
1069
- catch (error) {
1070
- this.consumeFailedAutomaticContinuation(runtime, message, continuationMode);
1071
- throw error;
1072
- }
1073
- if (continuationMode)
1258
+ if (consumedMode)
1074
1259
  runtime.runner.consumeConversationContinuation({
1075
1260
  content: typeof message === 'string' ? message : message.text,
1076
- queueMode: continuationMode,
1077
- clientMessageId: typeof message === 'string' ? undefined : message.clientMessageId,
1261
+ queueMode: consumedMode,
1262
+ clientMessageId,
1078
1263
  });
1079
1264
  return tokens;
1080
1265
  }
1266
+ catch (error) {
1267
+ this.consumeFailedAutomaticContinuation(runtime, message, continuationMode);
1268
+ throw error;
1269
+ }
1081
1270
  finally {
1082
1271
  if (timeout)
1083
1272
  clearTimeout(timeout);
1273
+ unsubscribe?.();
1084
1274
  }
1085
1275
  }
1086
1276
  consumeFailedAutomaticContinuation(runtime, message, continuationMode) {
@@ -1141,13 +1331,18 @@ class ConversationKernel {
1141
1331
  });
1142
1332
  for (const continuation of runner.conversationContinuations()) {
1143
1333
  runtime.pendingNextTurn.push({
1144
- message: continuation.clientMessageId || continuation.images?.length
1334
+ message: continuation.clientMessageId || continuation.images?.length || continuation.hiddenUserInput
1145
1335
  ? {
1146
1336
  text: continuation.content,
1147
1337
  images: continuation.images,
1148
1338
  attachments: continuation.attachments,
1149
1339
  clientMessageId: continuation.clientMessageId,
1150
1340
  runId: continuation.runId,
1341
+ visibleUserInput: continuation.visibleUserInput,
1342
+ visibleMode: continuation.visibleMode,
1343
+ goalObjective: continuation.goalObjective,
1344
+ createdAt: continuation.createdAt,
1345
+ hiddenUserInput: continuation.hiddenUserInput,
1151
1346
  }
1152
1347
  : continuation.content,
1153
1348
  queueMode: continuation.queueMode,
@@ -1155,6 +1350,8 @@ class ConversationKernel {
1155
1350
  this.trackQueuedMessage(runtime, continuation.content, continuation.queueMode);
1156
1351
  }
1157
1352
  runtime.unsubscribe = runner.subscribeWorkEvents(event => {
1353
+ if (runtime.externalOwner?.active && event.runId)
1354
+ runtime.runId = event.runId;
1158
1355
  const routedEvent = {
1159
1356
  ...event,
1160
1357
  conversationId: runtime.target.conversationId,
@@ -1177,7 +1374,7 @@ class ConversationKernel {
1177
1374
  for (const listener of this.listeners)
1178
1375
  listener(event);
1179
1376
  });
1180
- runner.subscribeAgentKernelUserMessageStart((content, clientMessageId) => {
1377
+ runtime.unsubscribeUserMessageStart = runner.subscribeAgentKernelUserMessageStart((content, clientMessageId) => {
1181
1378
  this.consumeQueuedMessage(runtime, content);
1182
1379
  if (!clientMessageId)
1183
1380
  return;
@@ -1194,11 +1391,26 @@ class ConversationKernel {
1194
1391
  runtime.runner.recordGuideReceipt(applied);
1195
1392
  runtime.guideEnvelopes.delete(clientMessageId);
1196
1393
  });
1197
- runtime.unsubscribeRootInboxWake = runner.subscribeRootInboxWake(message => {
1198
- this.enqueueRootInboxWake(runtime, message);
1394
+ runtime.unsubscribeRootInboxWake = runner.subscribeRootInboxWake((message, options) => {
1395
+ this.enqueueRootInboxWake(runtime, message, options?.wakeup !== false);
1199
1396
  return true;
1397
+ }, ids => {
1398
+ const retired = new Set(ids);
1399
+ const before = runtime.pendingNextTurn.length;
1400
+ runtime.pendingNextTurn = runtime.pendingNextTurn.filter(item => {
1401
+ if (item.queueMode !== 'followUp' || typeof item.message === 'string'
1402
+ || !item.message.hiddenUserInput || item.message.clientMessageId)
1403
+ return true;
1404
+ const id = item.message.text.match(/^\[Root subagent inbox id=([0-9a-f-]{36})\b/i)?.[1];
1405
+ return !id || !retired.has(id);
1406
+ });
1407
+ if (runtime.pendingNextTurn.length !== before)
1408
+ this.emitQueueUpdate(runtime);
1200
1409
  });
1201
1410
  this.runtimes.set(target.runtimeKey, runtime);
1411
+ // An already-read inbox entry is not replayed by the mailbox listener.
1412
+ // Reconcile saved result receipts after hydrating its continuation copy.
1413
+ runner.acknowledgeSubagentSettlementReceipts();
1202
1414
  return runtime;
1203
1415
  }
1204
1416
  scheduleGoalContinuation(runtime, completedRunId) {
@@ -1207,7 +1419,7 @@ class ConversationKernel {
1207
1419
  runtime.runner.flushWorkspaceConversationState();
1208
1420
  runtime.goalContinuationTimer = setTimeout(() => {
1209
1421
  runtime.goalContinuationTimer = undefined;
1210
- if (runtime.runId !== completedRunId || runtime.activePromise)
1422
+ if (runtime.preparingArchive || runtime.runId !== completedRunId || runtime.activePromise)
1211
1423
  return;
1212
1424
  this.queueState(runtime);
1213
1425
  if (runtime.pendingNextTurn.length > 0
@@ -1238,19 +1450,64 @@ class ConversationKernel {
1238
1450
  }
1239
1451
  setImmediate(() => {
1240
1452
  runtime.pendingContinuationRunId = undefined;
1241
- if (runtime.runId !== runId || runtime.activePromise || runtime.stopRequestedRunId === runId || runtime.queuePaused)
1453
+ if (runtime.preparingArchive || runtime.runId !== runId || runtime.activePromise || runtime.externalOwner || runtime.stopRequestedRunId === runId || runtime.queuePaused)
1242
1454
  return;
1243
1455
  const next = runtime.pendingNextTurn.shift();
1244
1456
  if (!next)
1245
1457
  return;
1246
- const message = typeof next.message === 'string'
1247
- ? { text: next.message, runId }
1248
- : { ...next.message, runId: next.message.runId || runId };
1249
- void this.prompt(message, runtime.target, runtime.options, next.queueMode).catch(() => {
1458
+ const delivery = next.queueMode === 'followUp' ? this.drainQueuedFollowUpMessage(next.message) : next.message;
1459
+ const message = typeof delivery === 'string'
1460
+ ? { text: delivery, runId }
1461
+ : { ...delivery, runId: delivery.runId || runId };
1462
+ void this.runPendingQueueItem(runtime, next, async () => {
1463
+ if (message.visibleMode && ['build', 'chat', 'plan', 'goal'].includes(message.visibleMode)) {
1464
+ this.setMode(runtime.target, message.visibleMode);
1465
+ }
1466
+ return this.prompt(message, runtime.target, runtime.options, next.queueMode);
1467
+ }).catch(() => {
1250
1468
  // Agent.process and the work-run finalizer already publish the error.
1251
1469
  });
1252
1470
  });
1253
1471
  }
1472
+ async runPendingQueueItem(runtime, next, execute) {
1473
+ const queuedMessage = typeof next.message === 'string' ? undefined : next.message;
1474
+ const id = queuedMessage?.clientMessageId;
1475
+ if (next.queueMode !== 'followUp' || !id)
1476
+ return execute();
1477
+ let accepted = false;
1478
+ const unsubscribe = runtime.runner.subscribeAgentKernelUserMessageStart((content, clientMessageId) => {
1479
+ // Normal in-run dequeues omit display identity, but retain the exact
1480
+ // execution text. Never let an unrelated Guide accept this queue row.
1481
+ if (clientMessageId ? clientMessageId === id : content === queuedMessage.text)
1482
+ accepted = true;
1483
+ });
1484
+ try {
1485
+ return await execute();
1486
+ }
1487
+ catch (error) {
1488
+ // Model/title/setup failures before user-message acceptance must leave
1489
+ // the same row manageable. A provider failure after acceptance must
1490
+ // never replay the already submitted user input after restart/resume.
1491
+ runtime.queuePaused = true;
1492
+ throw error;
1493
+ }
1494
+ finally {
1495
+ unsubscribe();
1496
+ if (accepted)
1497
+ runtime.runner.consumeConversationContinuation({
1498
+ content: queuedMessage.text, clientMessageId: id, queueMode: 'followUp',
1499
+ });
1500
+ else if (runtime.runner.conversationContinuations().some(item => item.clientMessageId === id && item.queueMode === 'followUp')) {
1501
+ // Cooperative Stop can turn a rejected process into a resolved outer
1502
+ // result. Restore on every unaccepted exit, including failed archive;
1503
+ // the pause/archive guards still prevent any automatic execution.
1504
+ if (!runtime.pendingNextTurn.some(item => typeof item.message !== 'string' && item.message.clientMessageId === id))
1505
+ runtime.pendingNextTurn.unshift(next);
1506
+ runtime.queuePaused = true;
1507
+ }
1508
+ this.emitQueueUpdate(runtime);
1509
+ }
1510
+ }
1254
1511
  startGoalDrivenBuild(runtime) {
1255
1512
  if (runtime.goalContinuationTimer) {
1256
1513
  clearTimeout(runtime.goalContinuationTimer);
@@ -1294,17 +1551,22 @@ class ConversationKernel {
1294
1551
  runner.ensureConversationSnapshot(target.conversationId);
1295
1552
  return runner;
1296
1553
  }
1297
- enqueueRootInboxWake(runtime, message) {
1554
+ enqueueRootInboxWake(runtime, message, wakeup = false) {
1555
+ const acceptedWhileActive = !!runtime.activePromise;
1298
1556
  queueMicrotask(() => {
1299
1557
  const rootInboxId = message.match(/^\[Root subagent inbox id=([0-9a-f-]{36})\b/i)?.[1];
1300
1558
  if (rootInboxId && !runtime.runner.subagents.readRootInbox().some(item => item.id === rootInboxId))
1301
1559
  return;
1560
+ if (runtime.queuePaused || runtime.runner.subagents.isSchedulingPaused())
1561
+ return;
1302
1562
  if (runtime.activePromise) {
1303
1563
  if (!runtime.pendingNextTurn.some(item => typeof item.message === 'string' ? item.message === message : item.message.text === message)) {
1304
1564
  runtime.pendingNextTurn.push({ message: { text: message, hiddenUserInput: true }, queueMode: 'followUp' });
1305
1565
  }
1306
1566
  return;
1307
1567
  }
1568
+ if (!wakeup && !acceptedWhileActive)
1569
+ return;
1308
1570
  void this.prompt({ text: message, hiddenUserInput: true }, runtime.target, runtime.options, 'followUp').catch(error => {
1309
1571
  runtime.runner.recordWorkStatus(`Subagent result follow-up failed: ${error instanceof Error ? error.message : String(error)}`);
1310
1572
  });
@@ -1427,6 +1689,9 @@ class ConversationKernel {
1427
1689
  images: queuedMessage.images?.map(image => ({ ...image })),
1428
1690
  attachments: queuedMessage.attachments?.map(attachment => ({ ...attachment })),
1429
1691
  createdAt: queuedMessage.createdAt,
1692
+ visibleUserInput: queuedMessage.visibleUserInput,
1693
+ visibleMode: queuedMessage.visibleMode,
1694
+ goalObjective: queuedMessage.goalObjective,
1430
1695
  }]);
1431
1696
  this.trackQueuedMessage(runtime, prompt, 'followUp');
1432
1697
  runtime.runner.recordWorkStatus(runtime.stopRequestedRunId === runtime.runId
@@ -1475,13 +1740,13 @@ class ConversationKernel {
1475
1740
  }
1476
1741
  trackQueuedMessage(runtime, message, queueMode) {
1477
1742
  this.queueState(runtime);
1478
- const list = queueMode === 'steer' ? runtime.queued.steering : runtime.queued.followUp;
1479
- list.push(message);
1743
+ if (queueMode === 'steer')
1744
+ runtime.queued.steering.push(message);
1480
1745
  }
1481
1746
  consumeQueuedMessage(runtime, message) {
1482
1747
  this.queueState(runtime);
1483
1748
  let changed = false;
1484
- for (const key of ['steering', 'followUp']) {
1749
+ for (const key of ['steering']) {
1485
1750
  const index = runtime.queued[key].indexOf(message);
1486
1751
  if (index >= 0) {
1487
1752
  runtime.queued[key].splice(index, 1);
@@ -1491,13 +1756,6 @@ class ConversationKernel {
1491
1756
  if (changed)
1492
1757
  this.emitQueueUpdate(runtime);
1493
1758
  }
1494
- replaceTrackedQueuedMessage(runtime, oldMessage, nextMessage, queueMode) {
1495
- this.queueState(runtime);
1496
- const list = queueMode === 'steer' ? runtime.queued.steering : runtime.queued.followUp;
1497
- const index = list.indexOf(oldMessage);
1498
- if (index >= 0)
1499
- list[index] = nextMessage;
1500
- }
1501
1759
  emitQueueUpdate(runtime) {
1502
1760
  this.queueState(runtime);
1503
1761
  runtime.runner.emitWorkEvent({
@@ -1524,8 +1782,14 @@ class ConversationKernel {
1524
1782
  runtime.queued = { steering: [], followUp: [] };
1525
1783
  if (!Array.isArray(runtime.queued.steering))
1526
1784
  runtime.queued.steering = [];
1527
- if (!Array.isArray(runtime.queued.followUp))
1528
- runtime.queued.followUp = [];
1785
+ // Follow-up order belongs to the pending entries, not a second text-only
1786
+ // ledger. Repeated text, edits and user-message-start callbacks must not
1787
+ // independently remove/reorder that projection. Internal auto-continuations
1788
+ // without user identity remain outside the visible user queue.
1789
+ runtime.queued.followUp = runtime.pendingNextTurn
1790
+ .filter(item => item.queueMode === 'followUp' && (typeof item.message === 'string'
1791
+ || item.message.hiddenUserInput !== true || !!item.message.clientMessageId))
1792
+ .map(item => typeof item.message === 'string' ? item.message : item.message.text);
1529
1793
  return runtime;
1530
1794
  }
1531
1795
  normalizeTarget(input) {