newmark-agent 0.5.14 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/conversation-utility-host.bundle.cjs +4565 -2998
  2. package/dist/conversation-utility-host.js +1 -1
  3. package/dist/core/agent.d.ts +108 -16
  4. package/dist/core/agent.js +698 -195
  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 +250 -107
  11. package/dist/core/autoRouter.js +17 -27
  12. package/dist/core/config.d.ts +2 -0
  13. package/dist/core/config.js +7 -1
  14. package/dist/core/conversationCommandState.d.ts +17 -0
  15. package/dist/core/conversationCommandState.js +140 -0
  16. package/dist/core/conversationKernel.d.ts +25 -4
  17. package/dist/core/conversationKernel.js +360 -89
  18. package/dist/core/conversationListEvent.d.ts +6 -0
  19. package/dist/core/conversationListEvent.js +14 -0
  20. package/dist/core/electronUtilityAgentClient.d.ts +4 -1
  21. package/dist/core/electronUtilityAgentClient.js +4 -4
  22. package/dist/core/electronUtilityRuntimePool.d.ts +8 -2
  23. package/dist/core/electronUtilityRuntimePool.js +11 -5
  24. package/dist/core/installUpdate.js +41 -29
  25. package/dist/core/modelResponseHealth.d.ts +17 -0
  26. package/dist/core/modelResponseHealth.js +193 -0
  27. package/dist/core/providerUsageAccounting.d.ts +47 -0
  28. package/dist/core/providerUsageAccounting.js +71 -0
  29. package/dist/core/requestContextEstimate.d.ts +18 -0
  30. package/dist/core/requestContextEstimate.js +54 -0
  31. package/dist/core/subagent.d.ts +89 -5
  32. package/dist/core/subagent.js +264 -41
  33. package/dist/core/subagentCommunication.d.ts +43 -0
  34. package/dist/core/subagentCommunication.js +167 -0
  35. package/dist/core/types.d.ts +34 -1
  36. package/dist/core/utilityAgentProtocol.d.ts +4 -0
  37. package/dist/core/workEventCoalescer.js +4 -1
  38. package/dist/core/wslAgentClient.d.ts +18 -5
  39. package/dist/core/wslAgentClient.js +79 -27
  40. package/dist/core/wslAgentProtocol.d.ts +7 -0
  41. package/dist/core/wslAgentRuntimePool.d.ts +8 -2
  42. package/dist/core/wslAgentRuntimePool.js +19 -6
  43. package/dist/core/wslRuntimeProcessTree.d.ts +10 -0
  44. package/dist/core/wslRuntimeProcessTree.js +104 -0
  45. package/dist/llm/provider.d.ts +5 -12
  46. package/dist/llm/provider.js +150 -269
  47. package/dist/main.js +582 -356
  48. package/dist/preload.js +3 -3
  49. package/dist/providers/chat-completions.adapter.d.ts +2 -0
  50. package/dist/providers/chat-completions.adapter.js +84 -36
  51. package/dist/providers/provider-adapter.d.ts +2 -0
  52. package/dist/providers/provider-events.d.ts +23 -14
  53. package/dist/providers/provider-events.js +148 -43
  54. package/dist/providers/provider-headers.d.ts +7 -3
  55. package/dist/providers/provider-headers.js +109 -39
  56. package/dist/providers/provider-request-compat.d.ts +6 -0
  57. package/dist/providers/provider-request-compat.js +37 -0
  58. package/dist/providers/responses.adapter.d.ts +2 -0
  59. package/dist/providers/responses.adapter.js +192 -126
  60. package/dist/server.d.ts +9 -2
  61. package/dist/server.js +100 -24
  62. package/dist/tools/index.js +17 -1
  63. package/dist/ui/index.html +2515 -693
  64. package/dist/ui/lucide-sprite.svg +7 -0
  65. package/dist/ui/startup.html +6 -6
  66. package/dist/wsl-agent-host.bundle.cjs +4575 -3006
  67. package/dist/wsl-agent-host.js +9 -4
  68. package/package.json +23 -4
@@ -1,10 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SubagentManager = void 0;
3
+ exports.SubagentManager = exports.SUBAGENT_RECOVERY_PROMPT = void 0;
4
4
  exports.sharedSubagentManager = sharedSubagentManager;
5
5
  exports.releaseSharedSubagentManager = releaseSharedSubagentManager;
6
6
  const crypto_1 = require("crypto");
7
+ exports.SUBAGENT_RECOVERY_PROMPT = '[Peer runtime recovery] Continue the unfinished work from the persisted working history. Preserve completed tool results and follow the latest instruction; do not repeat completed work.';
7
8
  function now() { return new Date().toISOString(); }
9
+ function cloneMailboxMessage(message) {
10
+ return { ...message, ...(message.control ? { control: { ...message.control } } : {}) };
11
+ }
8
12
  function natureSlug(value) {
9
13
  const normalized = String(value || 'subagent')
10
14
  .normalize('NFKD')
@@ -15,7 +19,14 @@ function natureSlug(value) {
15
19
  return normalized || 'subagent';
16
20
  }
17
21
  function cloneRecord(record) {
18
- return { ...record, messages: record.messages.map(message => ({ ...message })), metadata: record.metadata ? { ...record.metadata } : undefined };
22
+ return {
23
+ ...record,
24
+ messages: record.messages.map(message => ({
25
+ ...message,
26
+ tool_calls: message.tool_calls?.map(call => ({ ...call, function: { ...call.function } })),
27
+ })),
28
+ metadata: record.metadata ? structuredClone(record.metadata) : undefined,
29
+ };
19
30
  }
20
31
  function truncateText(value, maxChars) {
21
32
  const text = String(value || '');
@@ -33,6 +44,7 @@ class SubagentManager {
33
44
  rootInbox = [];
34
45
  pending = [];
35
46
  running = new Set();
47
+ activeJobs = new Map();
36
48
  schedulingPaused = false;
37
49
  nextSequence = 1;
38
50
  concurrency;
@@ -41,18 +53,25 @@ class SubagentManager {
41
53
  persist;
42
54
  onMailboxMessage;
43
55
  rootInboxListeners = new Set();
56
+ bindingOwner;
57
+ ownerBindings = new Map();
44
58
  onSettled;
45
59
  changedQueued = false;
46
60
  settledWaiters = new Map();
47
61
  conversationId;
48
62
  rootAgentId;
49
63
  hasRecords() { return this.subs.size > 0 || this.mailbox.length > 0 || this.rootInbox.length > 0; }
64
+ hasPendingWork() {
65
+ return this.running.size > 0 || this.pending.length > 0
66
+ || Array.from(this.subs.values()).some(record => record.status === 'queued' || record.status === 'working');
67
+ }
50
68
  reset() {
51
69
  this.subs.clear();
52
70
  this.mailbox = [];
53
71
  this.rootInbox = [];
54
72
  this.pending = [];
55
73
  this.running.clear();
74
+ this.activeJobs.clear();
56
75
  this.settledWaiters.clear();
57
76
  this.nextSequence = 1;
58
77
  this.schedulingPaused = false;
@@ -64,13 +83,17 @@ class SubagentManager {
64
83
  this.onChange = options.onChange;
65
84
  this.persist = options.persist;
66
85
  this.onMailboxMessage = options.onMailboxMessage;
67
- if (options.onRootInboxMessage)
86
+ if (options.onRootInboxMessage) {
68
87
  this.rootInboxListeners.add(options.onRootInboxMessage);
88
+ this.bindingOwner = options.onRootInboxMessage;
89
+ }
69
90
  this.onSettled = options.onSettled;
91
+ this.rememberOwnerBinding();
70
92
  const state = options.state;
71
93
  this.rootAgentId = String(state?.rootAgentId || options.rootAgentId || (0, crypto_1.randomUUID)());
72
94
  this.nextSequence = Math.max(1, Number(state?.nextSequence || 1));
73
95
  this.schedulingPaused = state?.schedulingPaused === true;
96
+ const savedJobs = new Map((state?.jobs || []).map(job => [job.id, job]));
74
97
  for (const raw of state?.records || []) {
75
98
  const record = cloneRecord(raw);
76
99
  record.messages = record.messages.map(message => message.role === 'user'
@@ -80,13 +103,24 @@ class SubagentManager {
80
103
  record.status = 'queued';
81
104
  this.subs.set(record.id, record);
82
105
  if (record.status === 'queued') {
83
- const sequence = Number(record.queueSequence || this.nextSequence++);
106
+ const savedJob = savedJobs.get(record.id);
107
+ const sequence = Number(savedJob?.sequence || record.queueSequence || this.nextSequence++);
108
+ this.nextSequence = Math.max(this.nextSequence, sequence + 1);
84
109
  record.queueSequence = sequence;
85
- this.pending.push({ id: record.id, prompt: record.messages.filter(message => message.role === 'user').at(-1)?.content || record.prompt, flowName: record.flowName || '', sequence, reason: 'resume' });
110
+ this.pending.push(savedJob
111
+ ? { ...savedJob, sequence }
112
+ : { id: record.id, prompt: record.messages.filter(message => message.role === 'user').at(-1)?.content || record.prompt, flowName: record.flowName || '', sequence, reason: 'resume' });
86
113
  }
87
114
  }
88
- this.mailbox = (state?.mailbox || []).map(message => ({ ...message }));
89
- this.rootInbox = (state?.rootInbox || []).map(message => ({ ...message }));
115
+ // Historical messages always woke their recipient. Migrate only an absent
116
+ // field; an explicitly stored false must remain passive after a restart.
117
+ this.mailbox = (state?.mailbox || []).map(message => ({ ...cloneMailboxMessage(message),
118
+ wakeup: message.wakeup === undefined ? true : message.wakeup === true,
119
+ acceptedWhileActive: message.acceptedWhileActive === true,
120
+ }));
121
+ this.rootInbox = (state?.rootInbox || []).map(message => ({ ...message,
122
+ wakeup: message.wakeup === undefined ? true : message.wakeup === true,
123
+ }));
90
124
  for (const record of this.subs.values())
91
125
  this.queuePersistedUnread(record);
92
126
  this.pending.sort((a, b) => a.sequence - b.sequence);
@@ -95,28 +129,79 @@ class SubagentManager {
95
129
  bind(options) {
96
130
  if (options.concurrency !== undefined)
97
131
  this.setConcurrencyLimit(options.concurrency);
98
- if (options.executor)
99
- this.executor = options.executor;
100
- if (options.onChange)
101
- this.onChange = options.onChange;
102
- if (options.persist)
103
- this.persist = options.persist;
104
- if (options.onMailboxMessage)
105
- this.onMailboxMessage = options.onMailboxMessage;
106
132
  if (options.onRootInboxMessage) {
107
133
  const listener = options.onRootInboxMessage;
134
+ const previous = this.ownerBindings.get(listener);
135
+ const binding = {
136
+ executor: options.executor || previous?.executor,
137
+ onChange: options.onChange || previous?.onChange,
138
+ persist: options.persist || previous?.persist,
139
+ onMailboxMessage: options.onMailboxMessage || previous?.onMailboxMessage,
140
+ onSettled: options.onSettled || previous?.onSettled,
141
+ };
142
+ this.ownerBindings.delete(listener);
143
+ this.ownerBindings.set(listener, binding);
144
+ // A snapshot/foreground facade must not steal jobs or their settlement
145
+ // callbacks from the owner that is currently executing the queue.
146
+ if (!this.bindingOwner || this.bindingOwner === listener || !this.hasPendingWork()) {
147
+ this.bindingOwner = listener;
148
+ Object.assign(this, binding);
149
+ }
108
150
  const added = !this.rootInboxListeners.has(listener);
109
151
  this.rootInboxListeners.add(listener);
110
152
  if (added)
111
153
  queueMicrotask(() => this.replayRootInbox(listener));
112
154
  }
113
- if (options.onSettled)
114
- this.onSettled = options.onSettled;
155
+ else {
156
+ if (options.executor)
157
+ this.executor = options.executor;
158
+ if (options.onChange)
159
+ this.onChange = options.onChange;
160
+ if (options.persist)
161
+ this.persist = options.persist;
162
+ if (options.onMailboxMessage)
163
+ this.onMailboxMessage = options.onMailboxMessage;
164
+ if (options.onSettled)
165
+ this.onSettled = options.onSettled;
166
+ this.rememberOwnerBinding();
167
+ }
115
168
  this.pump();
116
169
  }
170
+ ownsExecutionBinding(listener) {
171
+ return this.bindingOwner === listener;
172
+ }
173
+ rememberOwnerBinding() {
174
+ if (!this.bindingOwner)
175
+ return;
176
+ this.ownerBindings.delete(this.bindingOwner);
177
+ this.ownerBindings.set(this.bindingOwner, {
178
+ executor: this.executor, onChange: this.onChange, persist: this.persist,
179
+ onMailboxMessage: this.onMailboxMessage, onSettled: this.onSettled,
180
+ });
181
+ }
117
182
  removeRootInboxListener(listener) {
118
183
  this.rootInboxListeners.delete(listener);
119
184
  }
185
+ /** Detach one facade without clearing a newer owner's callbacks or live jobs. */
186
+ releaseOwnerBinding(listener) {
187
+ if (this.bindingOwner === listener && this.hasPendingWork())
188
+ return false;
189
+ this.rootInboxListeners.delete(listener);
190
+ this.ownerBindings.delete(listener);
191
+ if (this.bindingOwner !== listener)
192
+ return true;
193
+ // A temporary facade may have rebound this shared manager while another
194
+ // idle facade remained usable. Restore that owner instead of stranding
195
+ // its next peer in the durable queue with no executor.
196
+ const previous = Array.from(this.ownerBindings.entries()).at(-1);
197
+ this.bindingOwner = previous?.[0];
198
+ this.executor = previous?.[1].executor;
199
+ this.onChange = previous?.[1].onChange;
200
+ this.persist = previous?.[1].persist;
201
+ this.onMailboxMessage = previous?.[1].onMailboxMessage;
202
+ this.onSettled = previous?.[1].onSettled;
203
+ return true;
204
+ }
120
205
  create(name, prompt, model, inputMode, agentMode = 'build', createdByAgentId = this.rootAgentId, flowName = '', goalObjective = '', flowPc = 0, buildRunId = '', intelligenceTier = '') {
121
206
  const id = (0, crypto_1.randomUUID)();
122
207
  const shortId = id.replace(/-/g, '').slice(0, 8);
@@ -169,18 +254,44 @@ class SubagentManager {
169
254
  // convenience lookup only; the id/shortId/displayName paths stay exact.
170
255
  return [...this.subs.values()].find(item => item.name === id || item.displayName === id || item.shortId === id || item.natureSlug === natureSlug(id));
171
256
  }
172
- send(id, prompt) {
173
- const target = this.get(id);
174
- if (!target || target.status === 'closed')
175
- return false;
176
- target.messages.push({ role: 'user', content: prompt, hidden_user_input: true });
177
- target.error = undefined;
178
- target.completedAt = undefined;
179
- target.updatedAt = now();
180
- this.enqueue(target, prompt, target.flowName || '', 'mailbox');
181
- return true;
257
+ send(id, prompt, wakeup = false) {
258
+ // The compatibility entry point shares the durable mailbox and its
259
+ // single-executor guarantee; a direct enqueue could overlap this peer.
260
+ return this.sendMessage(this.rootAgentId, id, prompt, 'directive', {}, wakeup).ok;
261
+ }
262
+ broadcastStop(fromAgentId, runId, force = false) {
263
+ // Set the gate before publishing any control receipt, and commit the gate
264
+ // plus the entire batch once. No per-message callback can dispatch work.
265
+ const wasPaused = this.schedulingPaused;
266
+ this.schedulingPaused = true;
267
+ const ids = [];
268
+ const peerIds = [];
269
+ const stamp = now();
270
+ const stopRunId = String(runId || '');
271
+ const alreadySent = new Set(this.mailbox.filter(message => message.control?.action === 'stop'
272
+ && message.control.runId === stopRunId && message.control.force === (force === true)).map(message => message.toAgentId));
273
+ for (const record of this.subs.values()) {
274
+ if (record.status === 'closed' || alreadySent.has(record.id))
275
+ continue;
276
+ const message = {
277
+ id: (0, crypto_1.randomUUID)(), conversationId: this.conversationId, sequence: this.nextSequence++,
278
+ fromAgentId, toAgentId: record.id, kind: 'directive',
279
+ body: force ? '[Runtime control] Force stop this conversation work.' : '[Runtime control] Stop this conversation work.',
280
+ wakeup: false, acceptedWhileActive: false,
281
+ control: { action: 'stop', runId: stopRunId, force: force === true },
282
+ createdAt: stamp, readAt: stamp,
283
+ };
284
+ this.mailbox.push(message);
285
+ ids.push(message.id);
286
+ peerIds.push(record.id);
287
+ }
288
+ if (!wasPaused || ids.length) {
289
+ this.persistNow();
290
+ this.changed();
291
+ }
292
+ return { count: ids.length, ids, peerIds };
182
293
  }
183
- sendMessage(fromAgentId, toAgentId, body, kind = 'directive', details = {}) {
294
+ sendMessage(fromAgentId, toAgentId, body, kind = 'directive', details = {}, wakeup = false) {
184
295
  if (!body.trim())
185
296
  return { ok: false, error: 'Message body is required.' };
186
297
  if (fromAgentId === toAgentId)
@@ -188,6 +299,8 @@ class SubagentManager {
188
299
  const target = this.get(toAgentId);
189
300
  if (!target)
190
301
  return { ok: false, error: `Peer agent not found: ${toAgentId}` };
302
+ if (fromAgentId === target.id)
303
+ return { ok: false, error: 'Peer agents cannot message themselves.' };
191
304
  if (target.status === 'closed')
192
305
  return { ok: false, error: `Peer agent is closed: ${target.qualifiedName}` };
193
306
  const message = {
@@ -198,6 +311,8 @@ class SubagentManager {
198
311
  toAgentId: target.id,
199
312
  kind,
200
313
  body: truncateText(body, 32000),
314
+ wakeup: wakeup === true,
315
+ acceptedWhileActive: target.status === 'working' || target.status === 'queued',
201
316
  correlationId: details.correlationId,
202
317
  replyTo: details.replyTo,
203
318
  createdAt: now(),
@@ -213,7 +328,7 @@ class SubagentManager {
213
328
  this.changed();
214
329
  return { ok: true, message: { ...message } };
215
330
  }
216
- sendRootMessage(fromAgentId, body, kind = 'result') {
331
+ sendRootMessage(fromAgentId, body, kind = 'result', settlementRevision, wakeup = false) {
217
332
  if (!body.trim())
218
333
  return { ok: false, error: 'Message body is required.' };
219
334
  if (fromAgentId === this.rootAgentId)
@@ -226,7 +341,11 @@ class SubagentManager {
226
341
  toAgentId: this.rootAgentId,
227
342
  kind,
228
343
  body: truncateText(body, 32000),
344
+ wakeup: wakeup === true,
229
345
  createdAt: now(),
346
+ ...(kind === 'result' && Number.isSafeInteger(settlementRevision) && Number(settlementRevision) > 0
347
+ ? { source: 'automatic-settlement', settlementRevision }
348
+ : {}),
230
349
  };
231
350
  this.rootInbox.push(message);
232
351
  this.persistNow();
@@ -240,6 +359,31 @@ class SubagentManager {
240
359
  .sort((a, b) => a.sequence - b.sequence)
241
360
  .map(message => ({ ...message }));
242
361
  }
362
+ acknowledgeSettlementResults(receipts) {
363
+ const observed = new Set(receipts.filter(receipt => typeof receipt.peerId === 'string'
364
+ && Number.isSafeInteger(receipt.revision) && receipt.revision > 0)
365
+ .map(receipt => `${receipt.peerId}:${receipt.revision}`));
366
+ const ids = [];
367
+ const stamp = now();
368
+ let changed = false;
369
+ for (const message of this.rootInbox) {
370
+ if (message.source !== 'automatic-settlement' || message.kind !== 'result'
371
+ || !observed.has(`${message.fromAgentId}:${message.settlementRevision}`))
372
+ continue;
373
+ // Include previously acknowledged IDs so cold-restored continuation
374
+ // copies can be retired without changing already-consumed history.
375
+ ids.push(message.id);
376
+ if (!message.readAt) {
377
+ message.readAt = stamp;
378
+ changed = true;
379
+ }
380
+ }
381
+ if (changed) {
382
+ this.persistNow();
383
+ this.changed();
384
+ }
385
+ return ids;
386
+ }
243
387
  acknowledgeRootInbox(messageId) {
244
388
  const message = this.rootInbox.find(item => item.id === messageId && !item.readAt);
245
389
  if (!message)
@@ -253,7 +397,7 @@ class SubagentManager {
253
397
  const messages = this.mailbox.filter(message => message.toAgentId === agentId && !message.readAt);
254
398
  if (messages.length)
255
399
  this.markMailboxRead(messages);
256
- return messages.map(message => ({ ...message }));
400
+ return messages.map(cloneMailboxMessage);
257
401
  }
258
402
  acknowledgeMailbox(agentId, messageId) {
259
403
  const message = this.mailbox.find(item => item.id === messageId && item.toAgentId === agentId && !item.readAt);
@@ -306,6 +450,8 @@ class SubagentManager {
306
450
  toAgentId: message.toAgentId,
307
451
  kind: message.kind,
308
452
  body: truncateText(message.body, 2000),
453
+ wakeup: message.wakeup,
454
+ acceptedWhileActive: message.acceptedWhileActive,
309
455
  createdAt: message.createdAt,
310
456
  readAt: message.readAt,
311
457
  }));
@@ -371,7 +517,7 @@ class SubagentManager {
371
517
  if (record)
372
518
  record.messages.push({ role: 'assistant', content });
373
519
  }
374
- replaceContext(id, history, compression) {
520
+ replaceContext(id, history, compression, inputCommitted = false) {
375
521
  const record = this.get(id);
376
522
  if (!record)
377
523
  return;
@@ -388,7 +534,7 @@ class SubagentManager {
388
534
  if (message.name)
389
535
  stored.name = String(message.name);
390
536
  if (Array.isArray(message.tool_calls))
391
- stored.tool_calls = message.tool_calls;
537
+ stored.tool_calls = structuredClone(message.tool_calls);
392
538
  if (message.hidden_user_input)
393
539
  stored.hidden_user_input = true;
394
540
  if (message.goal_continuation)
@@ -406,6 +552,19 @@ class SubagentManager {
406
552
  contextCompression: compression ? { ...compression } : null,
407
553
  };
408
554
  record.updatedAt = now();
555
+ const activeJob = this.activeJobs.get(record.id);
556
+ if (activeJob && inputCommitted)
557
+ activeJob.inputCommitted = true;
558
+ this.persistNow();
559
+ this.changed();
560
+ }
561
+ patchMetadata(id, patch) {
562
+ const record = this.get(id);
563
+ if (!record)
564
+ return;
565
+ record.metadata = { ...(record.metadata || {}), ...structuredClone(patch) };
566
+ record.updatedAt = now();
567
+ this.persistNow();
409
568
  this.changed();
410
569
  }
411
570
  complete(id, result) {
@@ -414,6 +573,7 @@ class SubagentManager {
414
573
  return;
415
574
  this.pending = this.pending.filter(job => job.id !== record.id);
416
575
  record.result = result;
576
+ record.settlementRevision = Math.max(0, Math.floor(Number(record.settlementRevision) || 0)) + 1;
417
577
  record.status = 'completed';
418
578
  record.error = undefined;
419
579
  record.completedAt = now();
@@ -430,6 +590,7 @@ class SubagentManager {
430
590
  return;
431
591
  this.pending = this.pending.filter(job => job.id !== record.id);
432
592
  record.result = `[Subagent Error] ${error}`;
593
+ record.settlementRevision = Math.max(0, Math.floor(Number(record.settlementRevision) || 0)) + 1;
433
594
  record.status = 'error';
434
595
  record.error = error;
435
596
  record.completedAt = now();
@@ -508,6 +669,33 @@ class SubagentManager {
508
669
  }
509
670
  listActive() { return this.listAll().filter(item => item.status !== 'closed'); }
510
671
  listAll() { return [...this.subs.values()].map(cloneRecord); }
672
+ /** Model-facing list: detailed transcripts and request cache remain private. */
673
+ listSummaries(status = '') {
674
+ const counts = new Map();
675
+ for (const message of this.mailbox) {
676
+ const count = counts.get(message.toAgentId) || { total: 0, unread: 0 };
677
+ count.total++;
678
+ if (!message.readAt)
679
+ count.unread++;
680
+ counts.set(message.toAgentId, count);
681
+ }
682
+ return [...this.subs.values()].filter(record => !status || record.status === status).map(record => ({
683
+ id: record.id, shortId: record.shortId, natureSlug: record.natureSlug,
684
+ displayName: record.displayName, qualifiedName: record.qualifiedName, name: record.name,
685
+ conversationId: record.conversationId, createdByAgentId: record.createdByAgentId,
686
+ buildRunId: record.buildRunId, intelligenceTier: record.intelligenceTier,
687
+ prompt: truncateText(record.prompt, 1200), model: record.model, inputMode: record.inputMode,
688
+ agentMode: record.agentMode, mode: record.agentMode, status: record.status, active: record.status !== 'closed',
689
+ goalObjective: record.goalObjective ? truncateText(record.goalObjective, 800) : undefined,
690
+ flowName: record.flowName, flowPc: record.flowPc, queueSequence: record.queueSequence,
691
+ resultAvailable: record.result !== null, resultChars: record.result?.length || 0,
692
+ settlementRevision: record.settlementRevision, messageCount: record.messages.length,
693
+ error: record.error ? truncateText(record.error, 512) : undefined,
694
+ createdAt: record.createdAt, startedAt: record.startedAt, updatedAt: record.updatedAt,
695
+ completedAt: record.completedAt, closedAt: record.closedAt,
696
+ mailbox: counts.get(record.id) || { total: 0, unread: 0 },
697
+ }));
698
+ }
511
699
  activeCountForBuild(buildRunId) {
512
700
  const target = String(buildRunId || '').trim();
513
701
  if (!target)
@@ -552,8 +740,17 @@ class SubagentManager {
552
740
  rootAgentId: this.rootAgentId,
553
741
  nextSequence: this.nextSequence,
554
742
  schedulingPaused: this.schedulingPaused,
743
+ jobs: [
744
+ ...Array.from(this.activeJobs.values(), job => ({
745
+ ...job, reason: 'resume', inputCommitted: false,
746
+ prompt: job.inputCommitted
747
+ ? exports.SUBAGENT_RECOVERY_PROMPT
748
+ : job.prompt,
749
+ })),
750
+ ...this.pending.map(job => ({ ...job })),
751
+ ].sort((a, b) => a.sequence - b.sequence),
555
752
  records: this.listAll().map(record => record.status === 'working' ? { ...record, status: 'queued' } : record),
556
- mailbox: this.mailbox.map(message => ({ ...message })),
753
+ mailbox: this.mailbox.map(cloneMailboxMessage),
557
754
  rootInbox: this.rootInbox.map(message => ({ ...message })),
558
755
  };
559
756
  }
@@ -564,6 +761,7 @@ class SubagentManager {
564
761
  existing.prompt = `${existing.prompt}\n\n${prompt}`;
565
762
  record.status = 'queued';
566
763
  record.updatedAt = now();
764
+ this.persistNow();
567
765
  this.changed();
568
766
  this.pump();
569
767
  return;
@@ -574,6 +772,7 @@ class SubagentManager {
574
772
  record.updatedAt = now();
575
773
  this.pending.push({ id: record.id, prompt, flowName, sequence, reason });
576
774
  this.pending.sort((a, b) => a.sequence - b.sequence);
775
+ this.persistNow();
577
776
  this.changed();
578
777
  this.pump();
579
778
  }
@@ -586,7 +785,8 @@ class SubagentManager {
586
785
  if (record.status === 'closed')
587
786
  return;
588
787
  const unread = this.unreadMailbox(record.id);
589
- if (!unread.length)
788
+ const activating = unread.find(message => message.wakeup || message.acceptedWhileActive);
789
+ if (!activating)
590
790
  return;
591
791
  if (record.status === 'queued') {
592
792
  const existing = this.pending.find(job => job.id === record.id);
@@ -598,17 +798,19 @@ class SubagentManager {
598
798
  return;
599
799
  }
600
800
  record.status = 'queued';
601
- const sequence = Number(record.queueSequence || unread[0].sequence || this.nextSequence++);
801
+ const sequence = Number(record.queueSequence || activating.sequence || this.nextSequence++);
602
802
  record.queueSequence = sequence;
603
- this.pending.push({ id: record.id, prompt: this.mailboxPrompt(unread), flowName: record.flowName || '', sequence, reason: 'mailbox' });
803
+ this.pending.push({ id: record.id, prompt: '', flowName: record.flowName || '', sequence, reason: 'mailbox' });
604
804
  }
605
805
  enqueueUnreadMailbox(record) {
606
806
  if (record.status === 'closed' || record.status === 'queued' || this.running.has(record.id))
607
807
  return;
608
808
  const unread = this.unreadMailbox(record.id);
609
- if (!unread.length)
809
+ if (!unread.some(message => message.wakeup || message.acceptedWhileActive))
610
810
  return;
611
- this.enqueue(record, this.mailboxPrompt(unread), record.flowName || '', 'mailbox');
811
+ // Store the mailbox once. pump() builds the current batch at dispatch so
812
+ // later arrivals coalesce without duplicating the initial wake message.
813
+ this.enqueue(record, '', record.flowName || '', 'mailbox');
612
814
  }
613
815
  markMailboxRead(messages) {
614
816
  const stamp = now();
@@ -655,24 +857,45 @@ class SubagentManager {
655
857
  const record = this.subs.get(job.id);
656
858
  if (!record || record.status === 'closed')
657
859
  continue;
658
- const unread = this.consumeMailbox(record.id);
659
- const prompt = unread.length ? `${job.prompt}\n\n${this.mailboxPrompt(unread)}` : job.prompt;
860
+ const unread = this.unreadMailbox(record.id);
861
+ const prompt = [job.prompt, unread.length ? this.mailboxPrompt(unread) : ''].filter(Boolean).join('\n\n');
660
862
  record.status = 'working';
661
863
  record.queueSequence = undefined;
662
864
  record.startedAt = record.startedAt || now();
663
865
  record.updatedAt = now();
664
866
  this.running.add(record.id);
867
+ const activeJob = { ...job, prompt };
868
+ this.activeJobs.set(record.id, activeJob);
869
+ // A read receipt must never reach disk without the precise dispatched
870
+ // input. Cold restore can now recover a job even before child startup.
871
+ if (unread.length)
872
+ this.markMailboxRead(unread);
873
+ else
874
+ this.persistNow();
665
875
  this.changed();
666
- void this.executor({ record: cloneRecord(record), prompt, flowName: job.flowName, reason: job.reason })
876
+ let execution;
877
+ try {
878
+ execution = Promise.resolve(this.executor({ record: cloneRecord(record), prompt, flowName: job.flowName, reason: job.reason }));
879
+ }
880
+ catch (error) {
881
+ execution = Promise.reject(error);
882
+ }
883
+ void execution
667
884
  .then(result => {
885
+ if (this.activeJobs.get(record.id) !== activeJob)
886
+ return;
668
887
  this.complete(record.id, result || '[Subagent] Completed with empty response.');
669
888
  this.running.delete(record.id);
889
+ this.activeJobs.delete(record.id);
670
890
  this.enqueueUnreadMailbox(record);
671
891
  this.pump();
672
892
  })
673
893
  .catch(error => {
894
+ if (this.activeJobs.get(record.id) !== activeJob)
895
+ return;
674
896
  this.fail(record.id, error instanceof Error ? error.message : String(error));
675
897
  this.running.delete(record.id);
898
+ this.activeJobs.delete(record.id);
676
899
  this.enqueueUnreadMailbox(record);
677
900
  this.pump();
678
901
  });
@@ -0,0 +1,43 @@
1
+ /** Sender-owned, explicit content selection for peer communication.
2
+ * Selection never calls a model, mutates history, or serializes runtime state.
3
+ */
4
+ export type SubagentCommunicationContentKind = 'summary' | 'text_history' | 'tool_history';
5
+ export interface SubagentCommunicationHistoryMessage {
6
+ role?: unknown;
7
+ content?: unknown;
8
+ tool_calls?: unknown;
9
+ tool_call_id?: unknown;
10
+ name?: unknown;
11
+ hidden_user_input?: unknown;
12
+ }
13
+ export interface SubagentCommunicationParams {
14
+ message?: unknown;
15
+ prompt?: unknown;
16
+ content?: unknown;
17
+ }
18
+ export interface SubagentCommunicationRange {
19
+ start: number;
20
+ end: number;
21
+ total: number;
22
+ }
23
+ export interface SubagentCommunicationSelection {
24
+ body: string;
25
+ selection: {
26
+ source: 'explicit' | 'default_history' | 'content';
27
+ segments: Array<{
28
+ kind: 'message' | SubagentCommunicationContentKind;
29
+ range?: SubagentCommunicationRange;
30
+ units: number;
31
+ }>;
32
+ bodyChars: number;
33
+ };
34
+ }
35
+ export type SubagentCommunicationErrorCode = 'ambiguous_content' | 'invalid_content' | 'invalid_range' | 'empty_history' | 'budget_exceeded';
36
+ export declare class SubagentCommunicationSelectionError extends Error {
37
+ readonly code: SubagentCommunicationErrorCode;
38
+ constructor(code: SubagentCommunicationErrorCode, message: string);
39
+ }
40
+ export declare function selectSubagentCommunication(history: readonly SubagentCommunicationHistoryMessage[], params: SubagentCommunicationParams, options?: {
41
+ maxBodyChars?: number;
42
+ }): SubagentCommunicationSelection;
43
+ //# sourceMappingURL=subagentCommunication.d.ts.map