pilotswarm-sdk 0.5.76 → 0.5.78

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 (80) hide show
  1. package/dist/capability-catalog.d.ts +13 -0
  2. package/dist/capability-catalog.d.ts.map +1 -1
  3. package/dist/capability-catalog.js +43 -4
  4. package/dist/capability-catalog.js.map +1 -1
  5. package/dist/capability-runtime.d.ts +3 -3
  6. package/dist/capability-runtime.js +3 -3
  7. package/dist/capability-runtime.js.map +1 -1
  8. package/dist/managed-session.js +4 -4
  9. package/dist/managed-session.js.map +1 -1
  10. package/dist/orchestration/agents.d.ts.map +1 -1
  11. package/dist/orchestration/agents.js +4 -0
  12. package/dist/orchestration/agents.js.map +1 -1
  13. package/dist/orchestration/index.d.ts +1 -1
  14. package/dist/orchestration/index.js +1 -1
  15. package/dist/orchestration/runtime.d.ts +1 -1
  16. package/dist/orchestration-registry.d.ts.map +1 -1
  17. package/dist/orchestration-registry.js +4 -2
  18. package/dist/orchestration-registry.js.map +1 -1
  19. package/dist/orchestration-version.d.ts +1 -1
  20. package/dist/orchestration-version.js +1 -1
  21. package/dist/orchestration.d.ts +2 -2
  22. package/dist/orchestration.js +1 -1
  23. package/dist/orchestration_1_0_46.d.ts +1 -1
  24. package/dist/orchestration_1_0_47.d.ts +1 -1
  25. package/dist/orchestration_1_0_48.d.ts +1 -1
  26. package/dist/orchestration_1_0_49.d.ts +1 -1
  27. package/dist/orchestration_1_0_50.d.ts +1 -1
  28. package/dist/orchestration_1_0_78/agents.d.ts +66 -0
  29. package/dist/orchestration_1_0_78/agents.d.ts.map +1 -0
  30. package/dist/orchestration_1_0_78/agents.js +894 -0
  31. package/dist/orchestration_1_0_78/agents.js.map +1 -0
  32. package/dist/orchestration_1_0_78/index.d.ts +24 -0
  33. package/dist/orchestration_1_0_78/index.d.ts.map +1 -0
  34. package/dist/orchestration_1_0_78/index.js +13 -0
  35. package/dist/orchestration_1_0_78/index.js.map +1 -0
  36. package/dist/orchestration_1_0_78/lifecycle.d.ts +51 -0
  37. package/dist/orchestration_1_0_78/lifecycle.d.ts.map +1 -0
  38. package/dist/orchestration_1_0_78/lifecycle.js +981 -0
  39. package/dist/orchestration_1_0_78/lifecycle.js.map +1 -0
  40. package/dist/orchestration_1_0_78/queue.d.ts +20 -0
  41. package/dist/orchestration_1_0_78/queue.d.ts.map +1 -0
  42. package/dist/orchestration_1_0_78/queue.js +894 -0
  43. package/dist/orchestration_1_0_78/queue.js.map +1 -0
  44. package/dist/orchestration_1_0_78/runtime.d.ts +31 -0
  45. package/dist/orchestration_1_0_78/runtime.d.ts.map +1 -0
  46. package/dist/orchestration_1_0_78/runtime.js +244 -0
  47. package/dist/orchestration_1_0_78/runtime.js.map +1 -0
  48. package/dist/orchestration_1_0_78/state.d.ts +214 -0
  49. package/dist/orchestration_1_0_78/state.d.ts.map +1 -0
  50. package/dist/orchestration_1_0_78/state.js +178 -0
  51. package/dist/orchestration_1_0_78/state.js.map +1 -0
  52. package/dist/orchestration_1_0_78/turn.d.ts +34 -0
  53. package/dist/orchestration_1_0_78/turn.d.ts.map +1 -0
  54. package/dist/orchestration_1_0_78/turn.js +1433 -0
  55. package/dist/orchestration_1_0_78/turn.js.map +1 -0
  56. package/dist/orchestration_1_0_78/utils.d.ts +39 -0
  57. package/dist/orchestration_1_0_78/utils.d.ts.map +1 -0
  58. package/dist/orchestration_1_0_78/utils.js +322 -0
  59. package/dist/orchestration_1_0_78/utils.js.map +1 -0
  60. package/dist/session-list-timestamps.d.ts +5 -0
  61. package/dist/session-list-timestamps.d.ts.map +1 -0
  62. package/dist/session-list-timestamps.js +16 -0
  63. package/dist/session-list-timestamps.js.map +1 -0
  64. package/dist/session-manager.d.ts +8 -0
  65. package/dist/session-manager.d.ts.map +1 -1
  66. package/dist/session-manager.js +79 -7
  67. package/dist/session-manager.js.map +1 -1
  68. package/dist/session-proxy.d.ts +1 -0
  69. package/dist/session-proxy.d.ts.map +1 -1
  70. package/dist/session-proxy.js +6 -1
  71. package/dist/session-proxy.js.map +1 -1
  72. package/dist/types.d.ts +11 -2
  73. package/dist/types.d.ts.map +1 -1
  74. package/dist/types.js.map +1 -1
  75. package/dist/worker.d.ts +2 -0
  76. package/dist/worker.d.ts.map +1 -1
  77. package/dist/worker.js +21 -2
  78. package/dist/worker.js.map +1 -1
  79. package/package.json +3 -3
  80. package/plugins/system/prompts/base-v2.md +7 -3
@@ -0,0 +1,894 @@
1
+ import { sanitizePromptAttachmentRefs, ATTACHMENTS_MAX_COUNT } from "../types.js";
2
+ import { messageSenderKey } from "../message-sender.js";
3
+ import { applyChildUpdate, maybeResolveAgentWaitCompletion, parseChildUpdate, isAgentWaitSettledStatus, } from "./agents.js";
4
+ import { bufferChildUpdate, drainLeadingQueuedScheduleActions, flushPendingChildDigestIntoPrompt, handleCommand, promptIdsIntersectCancellation, publishStatus, queueFollowup, recordCancelledMessageIds, wrapWithResumeContext, } from "./lifecycle.js";
5
+ import { shouldWakeParentForChildDigest } from "../child-notifications.js";
6
+ import { pendingChildDigestHasError } from "./lifecycle.js";
7
+ import { CHILD_UPDATE_BATCH_MS, CHILD_DIGEST_COALESCE_MS, childUpdateBatchMs, FIFO_BUCKET_COUNT, MAX_BUCKET_BYTES, MAX_DRAIN_PER_TURN, MAX_PREDISPATCH_SWEEP, NON_BLOCKING_TIMER_MS, PREDISPATCH_CANCEL_SWEEP_MS, touchRecentClientMessageIds, } from "./state.js";
8
+ import { handleTurnResult, processPrompt, processTimer } from "./turn.js";
9
+ import { validClientMessageIds, noteMessageSender, applySenderAttribution, maybeQueueSharedPreamble } from "./utils.js";
10
+ // ─── KV FIFO bucket primitives ──────────────────────────────
11
+ function fifoBucketKey(index) {
12
+ return `fifo.${index}`;
13
+ }
14
+ function readFifoBucket(ctx, index) {
15
+ const raw = ctx.getValue(fifoBucketKey(index));
16
+ if (!raw)
17
+ return [];
18
+ try {
19
+ return JSON.parse(raw);
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ }
25
+ function writeFifoBucket(ctx, index, items) {
26
+ if (items.length === 0) {
27
+ ctx.clearValue(fifoBucketKey(index));
28
+ }
29
+ else {
30
+ ctx.setValue(fifoBucketKey(index), JSON.stringify(items));
31
+ }
32
+ }
33
+ /**
34
+ * Put an item back at the HEAD of the FIFO.
35
+ *
36
+ * decide() peeks by popping. When the popped item turns out not to be
37
+ * mergeable it has to go back — and appending it put it BEHIND everything
38
+ * still queued: a FIFO of [kickoff, "do X", "cancel X"] dispatched the
39
+ * kickoff and left ["cancel X", "do X"], so the next turn read the two in
40
+ * the wrong order. The item was just popped from the head of the first
41
+ * non-empty bucket, so the head of that bucket is exactly where it belongs.
42
+ * If it no longer fits there (it always should — the bucket only shrank),
43
+ * fall back to append rather than lose it.
44
+ */
45
+ export function prependToFifo(runtime, item) {
46
+ const { ctx } = runtime;
47
+ let headIdx = 0;
48
+ for (let i = 0; i < FIFO_BUCKET_COUNT; i++) {
49
+ if (readFifoBucket(ctx, i).length > 0) {
50
+ headIdx = i;
51
+ break;
52
+ }
53
+ }
54
+ const bucket = readFifoBucket(ctx, headIdx);
55
+ bucket.unshift(item);
56
+ if (JSON.stringify(bucket).length <= MAX_BUCKET_BYTES) {
57
+ writeFifoBucket(ctx, headIdx, bucket);
58
+ return;
59
+ }
60
+ ctx.traceWarn?.(`[fifo] prepend did not fit bucket ${headIdx}; appending instead`);
61
+ appendToFifo(runtime, [item]);
62
+ }
63
+ export function appendToFifo(runtime, newItems) {
64
+ const { ctx } = runtime;
65
+ let writeBucketIdx = 0;
66
+ for (let i = FIFO_BUCKET_COUNT - 1; i >= 0; i--) {
67
+ if (readFifoBucket(ctx, i).length > 0) {
68
+ writeBucketIdx = i;
69
+ break;
70
+ }
71
+ }
72
+ for (const item of newItems) {
73
+ const bucket = readFifoBucket(ctx, writeBucketIdx);
74
+ bucket.push(item);
75
+ const serialized = JSON.stringify(bucket);
76
+ if (serialized.length > MAX_BUCKET_BYTES) {
77
+ bucket.pop();
78
+ writeFifoBucket(ctx, writeBucketIdx, bucket);
79
+ writeBucketIdx++;
80
+ if (writeBucketIdx >= FIFO_BUCKET_COUNT) {
81
+ ctx.traceInfo(`[fifo] overflow — ${newItems.length} item(s) may rely on carry-forward`);
82
+ return;
83
+ }
84
+ writeFifoBucket(ctx, writeBucketIdx, [item]);
85
+ }
86
+ else {
87
+ writeFifoBucket(ctx, writeBucketIdx, bucket);
88
+ }
89
+ }
90
+ }
91
+ function popFifoItem(runtime) {
92
+ const { ctx } = runtime;
93
+ for (let i = 0; i < FIFO_BUCKET_COUNT; i++) {
94
+ const items = readFifoBucket(ctx, i);
95
+ if (items.length > 0) {
96
+ const [first, ...rest] = items;
97
+ writeFifoBucket(ctx, i, rest);
98
+ return first;
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ function popFirstFifoItemMatching(runtime, predicate) {
104
+ const { ctx } = runtime;
105
+ for (let i = 0; i < FIFO_BUCKET_COUNT; i++) {
106
+ const items = readFifoBucket(ctx, i);
107
+ const index = items.findIndex(predicate);
108
+ if (index >= 0) {
109
+ const [item] = items.splice(index, 1);
110
+ writeFifoBucket(ctx, i, items);
111
+ return item;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ function popNextDispatchFifoItem(runtime) {
117
+ const interactive = popFirstFifoItemMatching(runtime, (item) => item?.kind === "prompt" || item?.kind === "answer");
118
+ if (interactive) {
119
+ runtime.ctx.traceInfo(`[fifo] dispatching interactive ${interactive.kind} before queued timers`);
120
+ return interactive;
121
+ }
122
+ return popFifoItem(runtime);
123
+ }
124
+ function hasFifoItems(runtime) {
125
+ const { ctx } = runtime;
126
+ for (let i = 0; i < FIFO_BUCKET_COUNT; i++) {
127
+ if (readFifoBucket(ctx, i).length > 0)
128
+ return true;
129
+ }
130
+ return false;
131
+ }
132
+ function appendPromptStashToFifo(runtime, stash) {
133
+ appendToFifo(runtime, stash);
134
+ for (const item of stash) {
135
+ if (item?.kind !== "prompt")
136
+ continue;
137
+ const ids = validClientMessageIds(item.clientMessageIds);
138
+ touchRecentClientMessageIds(runtime.state, ids);
139
+ }
140
+ }
141
+ function duplicateClientMessageIds(runtime, ids, pendingIds) {
142
+ if (ids.length === 0)
143
+ return [];
144
+ const recent = new Set(runtime.state.recentClientMessageIds);
145
+ return ids.filter((id) => recent.has(id) || pendingIds.has(id));
146
+ }
147
+ function* recordDuplicatePrompt(runtime, ids, duplicateIds, source) {
148
+ const recent = new Set(runtime.state.recentClientMessageIds);
149
+ touchRecentClientMessageIds(runtime.state, duplicateIds.filter((id) => recent.has(id)));
150
+ runtime.ctx.traceInfo(`[${source}] suppressing duplicate prompt (duplicateIds=${duplicateIds.join(",")})`);
151
+ yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
152
+ eventType: "session.message_duplicate_suppressed",
153
+ data: {
154
+ clientMessageIds: ids,
155
+ duplicateClientMessageIds: duplicateIds,
156
+ windowSize: 20,
157
+ source,
158
+ },
159
+ }]);
160
+ }
161
+ // ─── Timer race candidate selection ─────────────────────────
162
+ /** Timer types whose expiry runs a turn — the only ones a child digest can ride into. */
163
+ const TURN_RUNNING_TIMER_TYPES = new Set(["wait", "cron", "cron_at"]);
164
+ function nextTimerCandidate(activeTimer, pendingChildDigest, now, opts = {}) {
165
+ const candidates = [];
166
+ if (activeTimer) {
167
+ candidates.push({
168
+ kind: "active",
169
+ remainingMs: Math.max(0, activeTimer.deadlineMs - now),
170
+ timer: activeTimer,
171
+ });
172
+ }
173
+ if (pendingChildDigest && !pendingChildDigest.ready && pendingChildDigest.updates.length > 0) {
174
+ // ≥1.0.71: if the parent's own wait/cron fires within the coalesce
175
+ // window anyway, the digest waits for that turn and rides into its
176
+ // prompt (processTimer flushes it) instead of waking the parent on
177
+ // its own. A child failure or cancellation still wakes at once.
178
+ // Idle / agent-poll / input-grace timers do not run a turn on expiry,
179
+ // so they never count as "will wake anyway".
180
+ const holdForTimer = Boolean(activeTimer
181
+ && TURN_RUNNING_TIMER_TYPES.has(activeTimer.type)
182
+ && activeTimer.deadlineMs - now <= CHILD_DIGEST_COALESCE_MS
183
+ && !opts.digestHasError);
184
+ if (!holdForTimer) {
185
+ const batchMs = opts.batchMs ?? CHILD_UPDATE_BATCH_MS;
186
+ candidates.push({
187
+ kind: "child-digest",
188
+ remainingMs: Math.max(0, pendingChildDigest.startedAtMs + batchMs - now),
189
+ });
190
+ }
191
+ }
192
+ if (candidates.length === 0)
193
+ return null;
194
+ candidates.sort((left, right) => left.remainingMs - right.remainingMs);
195
+ return candidates[0];
196
+ }
197
+ // ─── drain: greedily move queue events + timer fires into KV FIFO ──
198
+ function hasReadyPendingChildDigest(runtime) {
199
+ const digest = runtime.state.pendingChildDigest;
200
+ return Boolean(digest?.ready && digest.updates.length > 0);
201
+ }
202
+ function needsBlockingDequeue(runtime) {
203
+ const { state } = runtime;
204
+ return (state.legacyPendingMessage === undefined &&
205
+ !state.activeTimer &&
206
+ !hasReadyPendingChildDigest(runtime) &&
207
+ state.pendingToolActions.length === 0 &&
208
+ !state.pendingPrompt &&
209
+ !hasFifoItems(runtime));
210
+ }
211
+ export function* drain(runtime) {
212
+ const { ctx, state } = runtime;
213
+ const stash = [];
214
+ const seenChildUpdates = new Set();
215
+ const cancelledThisDrain = new Set();
216
+ const pendingClientMessageIds = new Set();
217
+ for (let i = 0; i < MAX_DRAIN_PER_TURN; i++) {
218
+ let msg = null;
219
+ if (state.legacyPendingMessage !== undefined) {
220
+ msg = state.legacyPendingMessage;
221
+ state.legacyPendingMessage = undefined;
222
+ }
223
+ else if (state.regen) {
224
+ // Session regeneration: while a pipeline is pending, drain does a
225
+ // SINGLE non-blocking sweep for a pre-empting control cmd
226
+ // (cancel_regen / cancel / delete) and then yields to the run loop
227
+ // to advance the next stage. It must never park on the session's
228
+ // armed idle/affinity/cron timer (up to 30 min) or on a blocking
229
+ // dequeue — either would stall the flip indefinitely.
230
+ const msgTask = ctx.dequeueEvent("messages");
231
+ const timerTask = ctx.scheduleTimer(NON_BLOCKING_TIMER_MS);
232
+ const race = yield ctx.race(msgTask, timerTask);
233
+ if (race.index === 1)
234
+ break;
235
+ msg = typeof race.value === "string" ? JSON.parse(race.value) : race.value;
236
+ }
237
+ else if (state.activeTimer || (state.pendingChildDigest && !state.pendingChildDigest.ready)) {
238
+ const now = yield ctx.utcNow();
239
+ const candidate = nextTimerCandidate(state.activeTimer, state.pendingChildDigest, now, {
240
+ batchMs: childUpdateBatchMs(state.subAgents?.length ?? 0),
241
+ digestHasError: pendingChildDigestHasError(runtime),
242
+ });
243
+ if (!candidate)
244
+ continue;
245
+ if (candidate.remainingMs === 0) {
246
+ if (candidate.kind === "active" && candidate.timer) {
247
+ stash.push({ kind: "timer", timer: { ...candidate.timer }, firedAtMs: now });
248
+ state.activeTimer = null;
249
+ }
250
+ else if (state.pendingChildDigest && state.pendingChildDigest.updates.length > 0) {
251
+ state.pendingChildDigest.ready = true;
252
+ break;
253
+ }
254
+ continue;
255
+ }
256
+ const msgTask = ctx.dequeueEvent("messages");
257
+ const timerTask = ctx.scheduleTimer(candidate.remainingMs);
258
+ const race = yield ctx.race(msgTask, timerTask);
259
+ if (race.index === 1) {
260
+ if (candidate.kind === "active" && candidate.timer) {
261
+ const firedAt = yield ctx.utcNow();
262
+ stash.push({ kind: "timer", timer: { ...candidate.timer }, firedAtMs: firedAt });
263
+ state.activeTimer = null;
264
+ }
265
+ else if (state.pendingChildDigest && state.pendingChildDigest.updates.length > 0) {
266
+ state.pendingChildDigest.ready = true;
267
+ break;
268
+ }
269
+ continue;
270
+ }
271
+ msg = typeof race.value === "string" ? JSON.parse(race.value) : race.value;
272
+ }
273
+ else if (!state.regen && needsBlockingDequeue(runtime)) {
274
+ if (i > 0)
275
+ break;
276
+ if (state.pendingInputQuestion) {
277
+ publishStatus(runtime, "input_required");
278
+ }
279
+ else if (state.blockedError) {
280
+ publishStatus(runtime, "error", {
281
+ error: state.blockedError.message,
282
+ retriesExhausted: true,
283
+ ...(state.blockedError.authFailure ? { authFailure: true } : {}),
284
+ });
285
+ }
286
+ else {
287
+ publishStatus(runtime, "idle");
288
+ }
289
+ const rawMsg = yield ctx.dequeueEvent("messages");
290
+ msg = typeof rawMsg === "string" ? JSON.parse(rawMsg) : rawMsg;
291
+ }
292
+ else {
293
+ const msgTask = ctx.dequeueEvent("messages");
294
+ const timerTask = ctx.scheduleTimer(NON_BLOCKING_TIMER_MS);
295
+ const race = yield ctx.race(msgTask, timerTask);
296
+ if (race.index === 1)
297
+ break;
298
+ msg = typeof race.value === "string" ? JSON.parse(race.value) : race.value;
299
+ }
300
+ if (!msg)
301
+ continue;
302
+ if (msg && Array.isArray(msg.cancelPending) && msg.cancelPending.length > 0) {
303
+ const validCancelIds = [];
304
+ for (const id of msg.cancelPending) {
305
+ if (typeof id === "string" && id) {
306
+ validCancelIds.push(id);
307
+ cancelledThisDrain.add(id);
308
+ state.cancelledMessageIds.add(id);
309
+ }
310
+ }
311
+ if (validCancelIds.length > 0) {
312
+ ctx.traceInfo(`[drain] received cancel tombstone (ids=${validCancelIds.join(",")})`);
313
+ }
314
+ for (let s = stash.length - 1; s >= 0; s--) {
315
+ const item = stash[s];
316
+ if (item?.kind !== "prompt")
317
+ continue;
318
+ const ids = Array.isArray(item.clientMessageIds) ? item.clientMessageIds : [];
319
+ if (ids.some((id) => cancelledThisDrain.has(id))) {
320
+ ctx.traceInfo(`[drain] dropping stashed prompt cancelled by tombstone (ids=${ids.join(",")})`);
321
+ yield* recordCancelledMessageIds(runtime, ids, "drain-stash");
322
+ stash.splice(s, 1);
323
+ }
324
+ }
325
+ continue;
326
+ }
327
+ if (msg.type === "cmd") {
328
+ if (stash.length > 0) {
329
+ appendPromptStashToFifo(runtime, stash);
330
+ stash.length = 0;
331
+ }
332
+ yield* handleCommand(runtime, msg);
333
+ if (state.orchestrationResult !== null)
334
+ return;
335
+ // Session regeneration: a pending pipeline is advanced by the run
336
+ // loop — return control instead of draining on (a blocking
337
+ // dequeue here would park the session with the regen never
338
+ // starting). Later cmds still pre-empt: the loop re-enters drain
339
+ // between stages and this pass is non-blocking while regen is set.
340
+ if (state.regen)
341
+ return;
342
+ continue;
343
+ }
344
+ const childUpdate = parseChildUpdate(msg.prompt);
345
+ if (childUpdate) {
346
+ const key = `${childUpdate.sessionId}|${childUpdate.updateType}|${childUpdate.content ?? ""}`;
347
+ if (!seenChildUpdates.has(key)) {
348
+ seenChildUpdates.add(key);
349
+ const wasExpectingReport = state.subAgents.find((agent) => agent.sessionId === childUpdate.sessionId)?.expectsReport === true;
350
+ const tracked = yield* applyChildUpdate(runtime, childUpdate);
351
+ if (tracked && !state.pendingShutdown) {
352
+ const childObservedAt = yield ctx.utcNow();
353
+ bufferChildUpdate(runtime, childUpdate, childObservedAt);
354
+ // Fast-path only on a FIRST report from a child spawned with
355
+ // an outstanding expectation — routine later updates keep
356
+ // the normal batch window.
357
+ if (wasExpectingReport)
358
+ markDigestReadyIfAllAgentsSettled(runtime);
359
+ }
360
+ if (tracked && state.waitingForAgentIds) {
361
+ yield* maybeResolveAgentWaitCompletion(runtime);
362
+ }
363
+ }
364
+ continue;
365
+ }
366
+ if (msg.answer !== undefined) {
367
+ const interruptsInputHold = Boolean(state.pendingInputQuestion)
368
+ && (state.activeTimer?.type === "input-grace" || state.activeTimer?.type === "idle");
369
+ if (interruptsInputHold) {
370
+ ctx.traceInfo(`[drain] answer interrupted ${state.activeTimer.type} timer`);
371
+ state.activeTimer = null;
372
+ }
373
+ stash.push({ kind: "answer", expectedQuestion: msg.expectedQuestion !== undefined ? msg.expectedQuestion : state.pendingInputQuestion ? { question: state.pendingInputQuestion.question, iteration: state.pendingInputQuestion.iteration ?? state.iteration } : null, answer: msg.answer, wasFreeform: msg.wasFreeform, ...(msg.sender && typeof msg.sender === "object" ? { sender: msg.sender } : {}) });
374
+ if (interruptsInputHold)
375
+ break;
376
+ continue;
377
+ }
378
+ if (msg.prompt) {
379
+ const incomingClientMessageIds = validClientMessageIds(msg.clientMessageIds);
380
+ if (promptIdsIntersectCancellation(runtime, incomingClientMessageIds)) {
381
+ ctx.traceInfo(`[drain] dropping incoming prompt cancelled by tombstone (ids=${incomingClientMessageIds.join(",")})`);
382
+ yield* recordCancelledMessageIds(runtime, incomingClientMessageIds, "drain-incoming");
383
+ continue;
384
+ }
385
+ const duplicateIds = duplicateClientMessageIds(runtime, incomingClientMessageIds, pendingClientMessageIds);
386
+ if (duplicateIds.length > 0) {
387
+ yield* recordDuplicatePrompt(runtime, incomingClientMessageIds, duplicateIds, "drain");
388
+ continue;
389
+ }
390
+ let userPrompt = msg.prompt;
391
+ state.blockedError = undefined;
392
+ if (state.activeTimer?.type === "wait") {
393
+ const now = yield ctx.utcNow();
394
+ const remainingMs = Math.max(0, state.activeTimer.deadlineMs - now);
395
+ const remainingSec = Math.round(remainingMs / 1000);
396
+ const elapsedMs = state.activeTimer.originalDurationMs - remainingMs;
397
+ const elapsedSec = Math.round(elapsedMs / 1000);
398
+ const totalSec = Math.round(state.activeTimer.originalDurationMs / 1000);
399
+ ctx.traceInfo(`[drain] user prompt interrupted wait timer, ${remainingSec}s remain — orchestration will auto-resume`);
400
+ state.interruptedWaitTimer = {
401
+ remainingSec,
402
+ reason: state.activeTimer.reason,
403
+ shouldRehydrate: state.activeTimer.shouldRehydrate ?? false,
404
+ waitPlan: state.activeTimer.waitPlan,
405
+ interruptKind: "user",
406
+ budget: state.activeTimer.budget === true,
407
+ };
408
+ if (state.activeTimer.shouldRehydrate && userPrompt) {
409
+ userPrompt = wrapWithResumeContext(runtime, userPrompt, `Your ${totalSec}s timer (reason: "${state.activeTimer.reason}") was interrupted by the above message. ` +
410
+ `${elapsedSec}s elapsed, ${remainingSec}s remain. ` +
411
+ `Reply to the message. The timer will be automatically resumed after your reply.`);
412
+ }
413
+ else if (userPrompt) {
414
+ userPrompt = `${userPrompt}\n\n` +
415
+ `[SYSTEM: The above is a message that interrupted your ${totalSec}s timer (reason: "${state.activeTimer.reason}"). ` +
416
+ `${elapsedSec}s elapsed, ${remainingSec}s remain. ` +
417
+ `Reply to the message. The timer will be automatically resumed after your reply.]`;
418
+ }
419
+ state.activeTimer = null;
420
+ }
421
+ else if (state.activeTimer?.type === "cron") {
422
+ const activeCron = state.cronSchedule;
423
+ const now = yield ctx.utcNow();
424
+ const remainingMs = Math.max(0, state.activeTimer.deadlineMs - now);
425
+ state.interruptedCronTimer = {
426
+ remainingMs,
427
+ reason: state.activeTimer.reason,
428
+ originalDurationMs: state.activeTimer.originalDurationMs,
429
+ ...(state.activeTimer.shouldRehydrate ? { shouldRehydrate: true } : {}),
430
+ };
431
+ const cronResumeNote = `This is an internal recurring schedule, not a new user prompt. ` +
432
+ `There is an active recurring schedule every ${activeCron?.intervalSeconds ?? "?"} seconds for "${activeCron?.reason ?? state.activeTimer.reason}". ` +
433
+ `The next cron wake-up will keep the original schedule and resume after the remaining ${Math.round(remainingMs / 1000)} seconds unless you explicitly reset cron. ` +
434
+ `Do NOT call wait() just to keep the recurring loop alive. ` +
435
+ `Call cron(action="cancel") only if you need to stop it.`;
436
+ if (state.activeTimer.shouldRehydrate && userPrompt) {
437
+ userPrompt = wrapWithResumeContext(runtime, userPrompt, cronResumeNote);
438
+ }
439
+ else if (userPrompt) {
440
+ userPrompt = `${userPrompt}\n\n[SYSTEM: ${cronResumeNote}]`;
441
+ }
442
+ ctx.traceInfo(`[drain] user prompt interrupted cron timer`);
443
+ state.activeTimer = null;
444
+ }
445
+ else if (state.activeTimer?.type === "cron_at") {
446
+ const activeCronAt = state.cronAtSchedule;
447
+ const now = yield ctx.utcNow();
448
+ const remainingMs = Math.max(0, state.activeTimer.deadlineMs - now);
449
+ const scheduledAt = activeCronAt?.nextFireAtMs ? new Date(activeCronAt.nextFireAtMs).toISOString() : "unknown";
450
+ const cronAtResumeNote = `This is an internal wall-clock recurring schedule, not a new user prompt. ` +
451
+ `There is an active wall-clock schedule for "${activeCronAt?.reason ?? state.activeTimer.reason}". ` +
452
+ `The pending scheduled fire (${scheduledAt}) is preserved and will run after this turn completes; ` +
453
+ `if the scheduled time passes while you respond, it will fire immediately afterward unless you explicitly cancel or reset the schedule. ` +
454
+ `Do NOT call wait() just to keep the recurring loop alive. ` +
455
+ `Call cron_at(action="cancel") only if you need to stop it.`;
456
+ if (state.activeTimer.shouldRehydrate && userPrompt) {
457
+ userPrompt = wrapWithResumeContext(runtime, userPrompt, cronAtResumeNote);
458
+ }
459
+ else if (userPrompt) {
460
+ userPrompt = `${userPrompt}\n\n[SYSTEM: ${cronAtResumeNote}]`;
461
+ }
462
+ ctx.traceInfo(`[drain] user prompt interrupted cron_at timer (${Math.round(remainingMs / 1000)}s remain)`);
463
+ state.activeTimer = null;
464
+ }
465
+ else if (state.activeTimer?.type === "idle") {
466
+ ctx.traceInfo(`[drain] user prompt within idle window, cancelling idle timer`);
467
+ state.activeTimer = null;
468
+ }
469
+ else if (state.activeTimer?.type === "agent-poll") {
470
+ ctx.traceInfo(`[drain] user prompt interrupted agent wait`);
471
+ state.waitingForAgentIds = null;
472
+ state.activeTimer = null;
473
+ }
474
+ if (state.pendingChildDigest?.updates.length) {
475
+ userPrompt = flushPendingChildDigestIntoPrompt(runtime, userPrompt);
476
+ }
477
+ const incomingAttachments = sanitizePromptAttachmentRefs(msg.attachments);
478
+ stash.push({
479
+ kind: "prompt",
480
+ prompt: userPrompt,
481
+ bootstrap: Boolean(msg.bootstrap),
482
+ ...(msg.requiredTool ? { requiredTool: msg.requiredTool } : {}),
483
+ ...(incomingClientMessageIds.length > 0 ? { clientMessageIds: incomingClientMessageIds } : {}),
484
+ ...(msg.sender && typeof msg.sender === "object" ? { sender: msg.sender } : {}),
485
+ ...(incomingAttachments.length > 0 ? { attachments: incomingAttachments } : {}),
486
+ });
487
+ for (const id of incomingClientMessageIds)
488
+ pendingClientMessageIds.add(id);
489
+ continue;
490
+ }
491
+ ctx.traceInfo(`[drain] skipping unknown: ${JSON.stringify(msg).slice(0, 120)}`);
492
+ }
493
+ if (stash.length > 0)
494
+ appendPromptStashToFifo(runtime, stash);
495
+ }
496
+ // ─── Pre-dispatch sweep: grab any pending cancel tombstone ──
497
+ function* sweepMessagesBeforePromptDispatch(runtime) {
498
+ const { ctx, state } = runtime;
499
+ const stash = [];
500
+ const seenChildUpdates = new Set();
501
+ const pendingClientMessageIds = new Set();
502
+ for (let i = 0; i < MAX_PREDISPATCH_SWEEP; i++) {
503
+ const msgTask = ctx.dequeueEvent("messages");
504
+ const timerTask = ctx.scheduleTimer(PREDISPATCH_CANCEL_SWEEP_MS);
505
+ const race = yield ctx.race(msgTask, timerTask);
506
+ if (race.index === 1)
507
+ break;
508
+ const msg = typeof race.value === "string" ? JSON.parse(race.value) : race.value;
509
+ if (!msg)
510
+ continue;
511
+ if (msg && Array.isArray(msg.cancelPending) && msg.cancelPending.length > 0) {
512
+ const validCancelIds = validClientMessageIds(msg.cancelPending);
513
+ for (const id of validCancelIds)
514
+ state.cancelledMessageIds.add(id);
515
+ if (validCancelIds.length > 0) {
516
+ ctx.traceInfo(`[predispatch] received cancel tombstone (ids=${validCancelIds.join(",")})`);
517
+ for (let s = stash.length - 1; s >= 0; s--) {
518
+ const item = stash[s];
519
+ if (item?.kind !== "prompt")
520
+ continue;
521
+ const ids = Array.isArray(item.clientMessageIds) ? item.clientMessageIds : [];
522
+ if (ids.some((id) => validCancelIds.includes(id))) {
523
+ ctx.traceInfo(`[predispatch] dropping stashed prompt cancelled by tombstone (ids=${ids.join(",")})`);
524
+ yield* recordCancelledMessageIds(runtime, ids, "predispatch-stash");
525
+ stash.splice(s, 1);
526
+ }
527
+ }
528
+ }
529
+ continue;
530
+ }
531
+ if (msg.type === "cmd") {
532
+ if (stash.length > 0) {
533
+ appendPromptStashToFifo(runtime, stash);
534
+ stash.length = 0;
535
+ }
536
+ yield* handleCommand(runtime, msg);
537
+ if (state.orchestrationResult !== null)
538
+ return;
539
+ // Session regeneration: a pending pipeline is advanced by the run
540
+ // loop — return control instead of draining on (a blocking
541
+ // dequeue here would park the session with the regen never
542
+ // starting). Later cmds still pre-empt: the loop re-enters drain
543
+ // between stages and this pass is non-blocking while regen is set.
544
+ if (state.regen)
545
+ return;
546
+ continue;
547
+ }
548
+ const childUpdate = parseChildUpdate(msg.prompt);
549
+ if (childUpdate) {
550
+ const key = `${childUpdate.sessionId}|${childUpdate.updateType}|${childUpdate.content ?? ""}`;
551
+ if (!seenChildUpdates.has(key)) {
552
+ seenChildUpdates.add(key);
553
+ const wasExpectingReport = state.subAgents.find((agent) => agent.sessionId === childUpdate.sessionId)?.expectsReport === true;
554
+ const tracked = yield* applyChildUpdate(runtime, childUpdate);
555
+ if (tracked && !state.pendingShutdown) {
556
+ const childObservedAt = yield ctx.utcNow();
557
+ bufferChildUpdate(runtime, childUpdate, childObservedAt);
558
+ // Fast-path only on a FIRST report from a child spawned with
559
+ // an outstanding expectation — routine later updates keep
560
+ // the normal batch window.
561
+ if (wasExpectingReport)
562
+ markDigestReadyIfAllAgentsSettled(runtime);
563
+ }
564
+ if (tracked && state.waitingForAgentIds) {
565
+ yield* maybeResolveAgentWaitCompletion(runtime);
566
+ }
567
+ }
568
+ continue;
569
+ }
570
+ if (msg.answer !== undefined) {
571
+ stash.push({ kind: "answer", expectedQuestion: msg.expectedQuestion !== undefined ? msg.expectedQuestion : state.pendingInputQuestion ? { question: state.pendingInputQuestion.question, iteration: state.pendingInputQuestion.iteration ?? state.iteration } : null, answer: msg.answer, wasFreeform: msg.wasFreeform, ...(msg.sender && typeof msg.sender === "object" ? { sender: msg.sender } : {}) });
572
+ continue;
573
+ }
574
+ if (msg.prompt) {
575
+ const incomingClientMessageIds = validClientMessageIds(msg.clientMessageIds);
576
+ if (promptIdsIntersectCancellation(runtime, incomingClientMessageIds)) {
577
+ ctx.traceInfo(`[predispatch] dropping incoming prompt cancelled by tombstone (ids=${incomingClientMessageIds.join(",")})`);
578
+ yield* recordCancelledMessageIds(runtime, incomingClientMessageIds, "predispatch-incoming");
579
+ continue;
580
+ }
581
+ const duplicateIds = duplicateClientMessageIds(runtime, incomingClientMessageIds, pendingClientMessageIds);
582
+ if (duplicateIds.length > 0) {
583
+ yield* recordDuplicatePrompt(runtime, incomingClientMessageIds, duplicateIds, "predispatch");
584
+ continue;
585
+ }
586
+ const sweepAttachments = sanitizePromptAttachmentRefs(msg.attachments);
587
+ stash.push({
588
+ kind: "prompt",
589
+ prompt: msg.prompt,
590
+ bootstrap: Boolean(msg.bootstrap),
591
+ ...(msg.requiredTool ? { requiredTool: msg.requiredTool } : {}),
592
+ ...(incomingClientMessageIds.length > 0 ? { clientMessageIds: incomingClientMessageIds } : {}),
593
+ ...(msg.sender && typeof msg.sender === "object" ? { sender: msg.sender } : {}),
594
+ ...(sweepAttachments.length > 0 ? { attachments: sweepAttachments } : {}),
595
+ });
596
+ for (const id of incomingClientMessageIds)
597
+ pendingClientMessageIds.add(id);
598
+ continue;
599
+ }
600
+ ctx.traceInfo(`[predispatch] skipping unknown: ${JSON.stringify(msg).slice(0, 120)}`);
601
+ }
602
+ if (stash.length > 0)
603
+ appendPromptStashToFifo(runtime, stash);
604
+ }
605
+ // ─── decide: pop and process one item from FIFO ─────────────
606
+ function* processAnswer(runtime, answerItem) {
607
+ const pending = runtime.state.pendingInputQuestion?.question;
608
+ // New callers bind to the question they observed at enqueue. Queue-drain
609
+ // snapshots cover older callers; old persisted FIFO entries remain valid.
610
+ const expected = answerItem.expectedQuestion;
611
+ const matchesQuestion = expected === undefined || (expected !== null
612
+ && expected.question === pending
613
+ && (expected.iteration === undefined
614
+ || expected.iteration === (runtime.state.pendingInputQuestion?.iteration ?? runtime.state.iteration)));
615
+ const question = matchesQuestion ? pending : undefined;
616
+ if (matchesQuestion)
617
+ runtime.state.pendingInputQuestion = null;
618
+ // Any writer may answer (security model); attribution shows who did.
619
+ const sender = noteMessageSender(runtime, answerItem.sender);
620
+ const answeredBy = runtime.state.multiWriter && sender?.display ? ` (answered by ${sender.display})` : "";
621
+ // Another writer can already have answered, or a reconnecting client can
622
+ // send against stale question state. Preserve that message as ordinary
623
+ // input instead of inventing a question the agent never asked.
624
+ const answerPrompt = question
625
+ ? `The user was asked: "${question}"\nThe user responded${answeredBy}: "${answerItem.answer}"`
626
+ : String(answerItem.answer);
627
+ maybeQueueSharedPreamble(runtime);
628
+ yield* processPrompt(runtime, answerPrompt, false, undefined, undefined, undefined, sender);
629
+ }
630
+ export function* decide(runtime) {
631
+ const { ctx, state } = runtime;
632
+ // Priority 1: pending tool actions (in-memory, replay carry-forward).
633
+ yield* drainLeadingQueuedScheduleActions(runtime);
634
+ if (state.pendingToolActions.length > 0) {
635
+ const action = state.pendingToolActions.shift();
636
+ ctx.traceInfo(`[orch] replaying queued action: ${action.type} remaining=${state.pendingToolActions.length}`);
637
+ yield* handleTurnResult(runtime, action, "");
638
+ return true;
639
+ }
640
+ // Priority 2: pending prompt (CAN carry-forward or queueFollowup).
641
+ // Hold while waiting for agents — let confirmations accumulate and merge
642
+ // with the agents-done summary for one combined LLM turn.
643
+ if (state.pendingPrompt && !state.waitingForAgentIds) {
644
+ const prompt = state.pendingPrompt;
645
+ const isBootstrap = state.bootstrapPrompt;
646
+ const requiredTool = state.pendingRequiredTool;
647
+ const cycleOrigin = state.pendingCycleOrigin;
648
+ const pendingAttachments = state.pendingAttachments;
649
+ state.pendingPrompt = undefined;
650
+ state.bootstrapPrompt = false;
651
+ state.pendingRequiredTool = undefined;
652
+ state.pendingCycleOrigin = undefined;
653
+ state.pendingAttachments = undefined;
654
+ yield* processPrompt(runtime, prompt, isBootstrap, requiredTool, undefined, cycleOrigin, undefined, pendingAttachments && pendingAttachments.length > 0 ? pendingAttachments : undefined);
655
+ return true;
656
+ }
657
+ // Priority 3: FIFO — next item in arrival order, with prompt batching.
658
+ const item = popNextDispatchFifoItem(runtime);
659
+ if (item) {
660
+ switch (item.kind) {
661
+ case "prompt": {
662
+ const ids = Array.isArray(item.clientMessageIds) ? item.clientMessageIds : [];
663
+ if (ids.length > 0) {
664
+ yield* sweepMessagesBeforePromptDispatch(runtime);
665
+ if (state.orchestrationResult !== null)
666
+ return true;
667
+ }
668
+ if (promptIdsIntersectCancellation(runtime, ids)) {
669
+ ctx.traceInfo(`[decide] dropping FIFO prompt cancelled by tombstone (ids=${ids.join(",")})`);
670
+ yield* recordCancelledMessageIds(runtime, ids, "decide-fifo");
671
+ return true;
672
+ }
673
+ // Batch consecutive prompt FIFO items into a single Copilot turn.
674
+ // Multi-writer attribution: note each item's sender (may flip
675
+ // the session to multi-writer), prefix segments with [FROM:]
676
+ // once flipped, and keep a single turn-level sender only when
677
+ // every merged segment came from the same identity.
678
+ const firstSender = noteMessageSender(runtime, item.sender);
679
+ let mergedPrompt = applySenderAttribution(runtime, firstSender, String(item.prompt || ""));
680
+ let mergedBootstrap = item.bootstrap ?? false;
681
+ let mergedRequiredTool = item.requiredTool;
682
+ const mergedClientMessageIds = [...ids];
683
+ const mergedAttachments = sanitizePromptAttachmentRefs(item.attachments);
684
+ let turnSender = firstSender;
685
+ let mixedSenders = false;
686
+ while (true) {
687
+ const peek = popFifoItem(runtime);
688
+ if (!peek)
689
+ break;
690
+ if (peek.kind !== "prompt") {
691
+ prependToFifo(runtime, peek);
692
+ break;
693
+ }
694
+ const peekIds = Array.isArray(peek.clientMessageIds) ? peek.clientMessageIds : [];
695
+ if (promptIdsIntersectCancellation(runtime, peekIds)) {
696
+ ctx.traceInfo(`[decide] dropping merged FIFO prompt cancelled by tombstone (ids=${peekIds.join(",")})`);
697
+ yield* recordCancelledMessageIds(runtime, peekIds, "decide-merge");
698
+ continue;
699
+ }
700
+ // Never merge an agent's bootstrap kickoff with a person's
701
+ // words. The merged prompt used to inherit `bootstrap` from
702
+ // EITHER side, and bootstrap is what the record keys on: the
703
+ // normal path refuses to record a bootstrap prompt as a
704
+ // user.message at all, and the budget stash stamps one as
705
+ // machine-authored and the portal folds it away. Both
706
+ // outcomes hid the person's message. So a kickoff and a
707
+ // person's prompt run as two turns; the person's is
708
+ // recorded, attributed and visible as its own.
709
+ //
710
+ // Put back at the HEAD. An earlier version appended it,
711
+ // reasoning that a bootstrap is only ever the first item of
712
+ // a fresh session — true, and beside the point: with
713
+ // [kickoff, B, C] queued, appending B left [C, B], and the
714
+ // person's own two messages ran in the wrong order.
715
+ if (Boolean(peek.bootstrap) !== Boolean(mergedBootstrap)) {
716
+ prependToFifo(runtime, peek);
717
+ break;
718
+ }
719
+ const peekSender = noteMessageSender(runtime, peek.sender);
720
+ if (messageSenderKey(peekSender ?? null) !== messageSenderKey(turnSender ?? null)) {
721
+ mixedSenders = true;
722
+ }
723
+ mergedPrompt = `${mergedPrompt}\n\n${applySenderAttribution(runtime, peekSender, String(peek.prompt || ""))}`;
724
+ // Same on both sides by construction now; kept as a plain
725
+ // carry rather than an OR so the intent reads.
726
+ mergedBootstrap = mergedBootstrap && (peek.bootstrap ?? false);
727
+ if (!mergedRequiredTool && peek.requiredTool)
728
+ mergedRequiredTool = peek.requiredTool;
729
+ for (const id of peekIds)
730
+ mergedClientMessageIds.push(id);
731
+ // Merged messages pool their image attachments in arrival
732
+ // order, bounded by the per-turn cap (overflow is dropped
733
+ // here deterministically rather than failing the turn).
734
+ for (const ref of sanitizePromptAttachmentRefs(peek.attachments)) {
735
+ if (mergedAttachments.length >= ATTACHMENTS_MAX_COUNT)
736
+ break;
737
+ mergedAttachments.push(ref);
738
+ }
739
+ }
740
+ maybeQueueSharedPreamble(runtime);
741
+ // Top-level named-agent metadata is translated into the same
742
+ // pending turn field as every other requiredTool. An explicit
743
+ // requirement on the queued prompt still wins.
744
+ mergedRequiredTool ??= state.pendingRequiredTool;
745
+ state.pendingRequiredTool = undefined;
746
+ yield* processPrompt(runtime, mergedPrompt, mergedBootstrap, mergedRequiredTool, mergedClientMessageIds.length > 0 ? mergedClientMessageIds : undefined, undefined, mixedSenders ? undefined : turnSender, mergedAttachments.length > 0 ? mergedAttachments : undefined);
747
+ break;
748
+ }
749
+ case "answer":
750
+ yield* processAnswer(runtime, item);
751
+ break;
752
+ case "timer":
753
+ yield* processTimer(runtime, item);
754
+ break;
755
+ case "agents-done":
756
+ queueFollowup(runtime, item.summary);
757
+ break;
758
+ default:
759
+ ctx.traceInfo(`[decide] unknown FIFO item kind: ${item.kind}`);
760
+ }
761
+ return true;
762
+ }
763
+ // Priority 4: buffered child digest — only after user/FIFO work is drained.
764
+ if (state.pendingChildDigest?.ready && state.pendingChildDigest.updates.length > 0 && !state.waitingForAgentIds) {
765
+ yield* processPendingChildDigest(runtime);
766
+ return true;
767
+ }
768
+ return false;
769
+ }
770
+ // ─── pending child digest dispatch (timer-aware) ────────────
771
+ import { buildPendingChildDigestSystemPrompt, clearPendingChildDigest, } from "./lifecycle.js";
772
+ /**
773
+ * All-quiet fast path: a freshly buffered child update revealed that EVERY
774
+ * tracked sub-agent is settled (terminal, idle, or blocked on input) — nothing
775
+ * will ever act again unprompted. Deliver the digest immediately instead of
776
+ * waiting out the remainder of the batch window so the parent looks at its
777
+ * children NOW rather than after a human pokes it.
778
+ */
779
+ function markDigestReadyIfAllAgentsSettled(runtime) {
780
+ const { state } = runtime;
781
+ if (state.waitingForAgentIds)
782
+ return; // wait resolution handles this case
783
+ if (!state.pendingChildDigest || state.pendingChildDigest.updates.length === 0)
784
+ return;
785
+ if (state.subAgents.length === 0)
786
+ return;
787
+ if (!state.subAgents.every((agent) => isAgentWaitSettledStatus(agent.status)))
788
+ return;
789
+ state.pendingChildDigest.ready = true;
790
+ }
791
+ function* processPendingChildDigest(runtime) {
792
+ const { ctx, state } = runtime;
793
+ const digestPrompt = buildPendingChildDigestSystemPrompt(runtime);
794
+ if (!digestPrompt) {
795
+ clearPendingChildDigest(runtime);
796
+ return;
797
+ }
798
+ const digestDecision = shouldWakeParentForChildDigest(state.pendingChildDigest.updates.map((update) => {
799
+ const agent = state.subAgents.find((entry) => entry.sessionId === update.sessionId);
800
+ return {
801
+ update: {
802
+ kind: update.updateType === "wait"
803
+ ? "wait"
804
+ : update.updateType === "failed"
805
+ ? "error"
806
+ : update.updateType === "cancelled" || update.updateType === "deleted"
807
+ ? "cancelled"
808
+ : update.updateType === "completed"
809
+ ? "completed"
810
+ : "progress",
811
+ summary: update.content,
812
+ ...(update.cycleOrigin ? { cyclic: true } : {}),
813
+ ...(update.cycleStatus === "material" || update.cycleStatus === "blocked"
814
+ ? { material: true }
815
+ : update.cycleStatus === "quiet"
816
+ ? { material: false }
817
+ : {}),
818
+ ...(update.verdict ? { result: { verdict: update.verdict } } : update.cycleStatus === "blocked" ? { result: { verdict: "blocked" } } : {}),
819
+ },
820
+ contract: agent?.contract,
821
+ };
822
+ }));
823
+ if (!digestDecision.wake) {
824
+ yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
825
+ eventType: "session.child_update_suppressed",
826
+ data: {
827
+ reason: digestDecision.reason,
828
+ policy: digestDecision.policy,
829
+ classification: digestDecision.classification,
830
+ updateCount: state.pendingChildDigest.updates.length,
831
+ },
832
+ }]);
833
+ clearPendingChildDigest(runtime);
834
+ return;
835
+ }
836
+ if (state.activeTimer?.type === "wait") {
837
+ const now = yield ctx.utcNow();
838
+ const remainingMs = Math.max(0, state.activeTimer.deadlineMs - now);
839
+ const remainingSec = Math.round(remainingMs / 1000);
840
+ const elapsedMs = state.activeTimer.originalDurationMs - remainingMs;
841
+ const elapsedSec = Math.round(elapsedMs / 1000);
842
+ const totalSec = Math.round(state.activeTimer.originalDurationMs / 1000);
843
+ state.interruptedWaitTimer = {
844
+ remainingSec,
845
+ reason: state.activeTimer.reason,
846
+ shouldRehydrate: state.activeTimer.shouldRehydrate ?? false,
847
+ waitPlan: state.activeTimer.waitPlan,
848
+ interruptKind: "child",
849
+ };
850
+ state.activeTimer = null;
851
+ clearPendingChildDigest(runtime);
852
+ yield* processPrompt(runtime, `[SYSTEM: Buffered child updates interrupted your ${totalSec}s timer (reason: "${state.interruptedWaitTimer.reason}"). ` +
853
+ `${elapsedSec}s elapsed, ${remainingSec}s remain. ` +
854
+ `Review the updates and continue your task now. The remaining wait will be resumed automatically after this turn completes.\n\n${digestPrompt}]`, true);
855
+ return;
856
+ }
857
+ if (state.activeTimer?.type === "cron") {
858
+ const activeCron = state.cronSchedule;
859
+ const now = yield ctx.utcNow();
860
+ const remainingMs = Math.max(0, state.activeTimer.deadlineMs - now);
861
+ state.interruptedCronTimer = {
862
+ remainingMs,
863
+ reason: state.activeTimer.reason,
864
+ originalDurationMs: state.activeTimer.originalDurationMs,
865
+ ...(state.activeTimer.shouldRehydrate ? { shouldRehydrate: true } : {}),
866
+ };
867
+ state.activeTimer = null;
868
+ clearPendingChildDigest(runtime);
869
+ yield* processPrompt(runtime, `[SYSTEM: This is an internal orchestration wake-up caused by child session updates; the user did not send a new message. ` +
870
+ `Buffered child updates arrived while your recurring schedule was waiting for the next wake-up${activeCron ? ` ("${activeCron.reason}")` : ""}. ` +
871
+ `Review the updates and continue your task now. The recurring cron schedule remains active and will be re-armed automatically after this turn completes.\n\n${digestPrompt}]`, true);
872
+ return;
873
+ }
874
+ if (state.activeTimer?.type === "cron_at") {
875
+ const activeCronAt = state.cronAtSchedule;
876
+ const scheduledAt = activeCronAt?.nextFireAtMs ? new Date(activeCronAt.nextFireAtMs).toISOString() : "unknown";
877
+ state.activeTimer = null;
878
+ clearPendingChildDigest(runtime);
879
+ yield* processPrompt(runtime, `[SYSTEM: This is an internal orchestration wake-up caused by child session updates; the user did not send a new message. ` +
880
+ `Buffered child updates arrived while your wall-clock schedule was waiting for its next fire${activeCronAt ? ` ("${activeCronAt.reason}", scheduled ${scheduledAt})` : ""}. ` +
881
+ `Review the updates and continue your task now. The wall-clock cron schedule remains active and will be re-armed automatically after this turn completes.\n\n${digestPrompt}]`, true);
882
+ return;
883
+ }
884
+ if (state.activeTimer?.type === "idle") {
885
+ state.activeTimer = null;
886
+ }
887
+ else if (state.activeTimer?.type === "agent-poll") {
888
+ state.waitingForAgentIds = null;
889
+ state.activeTimer = null;
890
+ }
891
+ clearPendingChildDigest(runtime);
892
+ yield* processPrompt(runtime, `[SYSTEM: ${digestPrompt}]`, true);
893
+ }
894
+ //# sourceMappingURL=queue.js.map