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,981 @@
1
+ import { describeCronAt } from "../cron-at.js";
2
+ import { normalizeMessageSender } from "../message-sender.js";
3
+ import { COMMAND_VERSION_KEY, RESPONSE_LATEST_KEY, RESPONSE_VERSION_KEY, commandResponseKey, } from "../types.js";
4
+ import { createSessionProxy } from "../session-proxy.js";
5
+ import { beginGracefulShutdown, cancelInFlightDistiller, } from "./agents.js";
6
+ import { FIRST_SUMMARIZE_DELAY, INTERNAL_SYSTEM_TURN_PROMPT, REPEAT_SUMMARIZE_DELAY, } from "./state.js";
7
+ import { appendSystemContext, extractPromptSystemContext, mergePrompt, } from "./utils.js";
8
+ // ─── Custom status / KV helpers ─────────────────────────────
9
+ export function publishStatus(runtime, status, extra = {}) {
10
+ const { state } = runtime;
11
+ const signal = {
12
+ status,
13
+ iteration: state.iteration,
14
+ ...(state.lastResponseVersion > 0 ? { responseVersion: state.lastResponseVersion } : {}),
15
+ ...(state.lastCommandVersion > 0 ? { commandVersion: state.lastCommandVersion } : {}),
16
+ ...(state.lastCommandId ? { commandId: state.lastCommandId } : {}),
17
+ ...(state.cronAtSchedule
18
+ ? {
19
+ cronActive: true,
20
+ cronKind: "wall-clock",
21
+ cronReason: state.cronAtSchedule.reason,
22
+ cronNextFireAt: state.cronAtSchedule.nextFireAtMs,
23
+ cronTimezone: state.cronAtSchedule.tz,
24
+ cronMaxFires: state.cronAtSchedule.maxFires,
25
+ cronFiresCompleted: state.cronAtSchedule.firesCompleted,
26
+ }
27
+ : state.cronSchedule
28
+ ? {
29
+ cronActive: true,
30
+ cronKind: "interval",
31
+ cronInterval: state.cronSchedule.intervalSeconds,
32
+ cronReason: state.cronSchedule.reason,
33
+ }
34
+ : { cronActive: false }),
35
+ ...(state.contextUsage ? { contextUsage: state.contextUsage } : {}),
36
+ ...extra,
37
+ // A late answer may finish another turn while this question remains open.
38
+ // Keep the question independently of latestResponse and its newer iteration.
39
+ ...(status === "input_required" && state.pendingInputQuestion ? {
40
+ pendingQuestion: state.pendingInputQuestion.question,
41
+ questionIteration: state.pendingInputQuestion.iteration ?? state.iteration,
42
+ choices: state.pendingInputQuestion.choices,
43
+ allowFreeform: state.pendingInputQuestion.allowFreeform,
44
+ } : {}),
45
+ };
46
+ runtime.ctx.setCustomStatus(JSON.stringify(signal));
47
+ }
48
+ function writeJsonValue(ctx, key, value) {
49
+ ctx.setValue(key, JSON.stringify(value));
50
+ }
51
+ export function readCounter(ctx, key) {
52
+ const raw = ctx.getValue(key);
53
+ if (raw == null)
54
+ return 0;
55
+ const parsed = Number(raw);
56
+ return Number.isFinite(parsed) ? parsed : 0;
57
+ }
58
+ function bumpCounter(ctx, key) {
59
+ const next = readCounter(ctx, key) + 1;
60
+ ctx.setValue(key, String(next));
61
+ return next;
62
+ }
63
+ export function* writeLatestResponse(runtime, payload) {
64
+ const version = bumpCounter(runtime.ctx, RESPONSE_VERSION_KEY);
65
+ const emittedAt = yield runtime.ctx.utcNow();
66
+ const responsePayload = {
67
+ schemaVersion: 1,
68
+ version,
69
+ emittedAt,
70
+ ...payload,
71
+ };
72
+ writeJsonValue(runtime.ctx, RESPONSE_LATEST_KEY, responsePayload);
73
+ runtime.state.lastResponseVersion = version;
74
+ return responsePayload;
75
+ }
76
+ export function* writeCommandResponse(runtime, response) {
77
+ const version = bumpCounter(runtime.ctx, COMMAND_VERSION_KEY);
78
+ const emittedAt = yield runtime.ctx.utcNow();
79
+ const payload = {
80
+ ...response,
81
+ schemaVersion: 1,
82
+ version,
83
+ emittedAt,
84
+ };
85
+ writeJsonValue(runtime.ctx, commandResponseKey(response.id), payload);
86
+ runtime.state.lastCommandVersion = version;
87
+ runtime.state.lastCommandId = response.id;
88
+ yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
89
+ eventType: "session.command_completed",
90
+ data: { cmd: response.cmd, id: response.id },
91
+ }]);
92
+ return payload;
93
+ }
94
+ // ─── Hydration / dehydration / checkpointing ────────────────
95
+ export function wrapWithResumeContext(runtime, userPrompt, extra) {
96
+ const base = runtime.state.pendingRehydrationMessage ??
97
+ `The session was dehydrated and has been rehydrated on a new worker. ` +
98
+ `The LLM conversation history is preserved.`;
99
+ runtime.state.pendingRehydrationMessage = undefined;
100
+ const parts = [userPrompt, ``, `[SYSTEM: ${base}`];
101
+ if (extra)
102
+ parts.push(extra);
103
+ parts.push(`]`);
104
+ return parts.join("\n");
105
+ }
106
+ /**
107
+ * Session lifecycle protocol (§3.4 tier 3): release the worker affinity by
108
+ * rotating the GUID — a pure orchestration-state change, no activity at all.
109
+ * Nothing needs uploading (every completed turn committed its snapshot
110
+ * inside the runTurn activity) and nothing needs telling the old worker:
111
+ * its local copy is a cache reclaimed by its own eviction clock. The next
112
+ * event hydrates wherever duroxide places the new key.
113
+ */
114
+ export function* releaseAffinity(runtime, reason, eventData) {
115
+ const { ctx, state } = runtime;
116
+ ctx.traceInfo(`[orch] releasing worker affinity (reason=${reason})`);
117
+ state.activeTimer = null;
118
+ state.affinityKey = yield ctx.newGuid();
119
+ runtime.session = createSessionProxy(ctx, runtime.input.sessionId, state.affinityKey, state.config, "agent-handoff-v2");
120
+ try {
121
+ yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
122
+ eventType: "session.affinity_released",
123
+ data: { reason, snapshotVersion: state.snapshotVersion, ...(eventData ?? {}) },
124
+ }]);
125
+ }
126
+ catch (err) {
127
+ ctx.traceInfo(`[orch] affinity_released event failed (non-fatal): ${err.message ?? err}`);
128
+ }
129
+ }
130
+ export function* maybeSummarize(runtime) {
131
+ if (runtime.options.isSystem)
132
+ return;
133
+ const now = yield runtime.ctx.utcNow();
134
+ if (runtime.state.nextSummarizeAt === 0) {
135
+ runtime.state.nextSummarizeAt = now + FIRST_SUMMARIZE_DELAY;
136
+ return;
137
+ }
138
+ if (now < runtime.state.nextSummarizeAt)
139
+ return;
140
+ try {
141
+ runtime.ctx.traceInfo(`[orch] summarizing session title`);
142
+ yield runtime.manager.summarizeSession(runtime.input.sessionId);
143
+ }
144
+ catch (err) {
145
+ runtime.ctx.traceInfo(`[orch] summarize failed: ${err.message}`);
146
+ }
147
+ runtime.state.nextSummarizeAt = now + REPEAT_SUMMARIZE_DELAY;
148
+ }
149
+ // ─── Task context / cron action helpers ─────────────────────
150
+ export function ensureTaskContext(runtime, sourcePrompt) {
151
+ if (runtime.state.taskContext || !sourcePrompt)
152
+ return;
153
+ runtime.state.taskContext = sourcePrompt.slice(0, 2000);
154
+ const base = typeof runtime.options.baseSystemMessage === "string"
155
+ ? runtime.options.baseSystemMessage ?? ""
156
+ : runtime.options.baseSystemMessage?.content ?? "";
157
+ runtime.state.config.systemMessage = base + (base ? "\n\n" : "") +
158
+ "[RECURRING TASK]\n" +
159
+ "Original user request (always remember, even if conversation history is truncated):\n\"" +
160
+ runtime.state.taskContext + "\"";
161
+ }
162
+ export function applyCronAction(runtime, action, sourcePrompt) {
163
+ runtime.state.interruptedCronTimer = null;
164
+ if (action.action === "cancel") {
165
+ runtime.ctx.traceInfo("[orch] cron cancelled");
166
+ runtime.state.cronSchedule = undefined;
167
+ runtime.state.cronAtSchedule = undefined;
168
+ return;
169
+ }
170
+ ensureTaskContext(runtime, sourcePrompt);
171
+ runtime.state.cronAtSchedule = undefined;
172
+ runtime.state.cronSchedule = {
173
+ intervalSeconds: action.intervalSeconds,
174
+ reason: action.reason,
175
+ };
176
+ runtime.ctx.traceInfo(`[orch] cron scheduled: every ${action.intervalSeconds}s (${action.reason})`);
177
+ }
178
+ export function* applyCronAtAction(runtime, action, sourcePrompt) {
179
+ runtime.state.interruptedCronTimer = null;
180
+ if (action.action === "cancel") {
181
+ runtime.ctx.traceInfo("[orch] cron_at cancelled");
182
+ runtime.state.cronAtSchedule = undefined;
183
+ runtime.state.cronSchedule = undefined;
184
+ yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
185
+ eventType: "session.cron_at_cancelled",
186
+ data: {},
187
+ }]);
188
+ return;
189
+ }
190
+ ensureTaskContext(runtime, sourcePrompt);
191
+ const afterUtcMs = yield runtime.ctx.utcNow();
192
+ const nextFire = yield runtime.manager.computeCronAtNextFire(action.schedule, afterUtcMs, action.schedule.lastOccurrenceKey);
193
+ runtime.state.cronSchedule = undefined;
194
+ runtime.state.cronAtSchedule = {
195
+ ...action.schedule,
196
+ firesCompleted: action.schedule.firesCompleted ?? 0,
197
+ nextFireAtMs: nextFire.nextFireAtMs,
198
+ nextOccurrenceKey: nextFire.occurrenceKey,
199
+ };
200
+ runtime.ctx.traceInfo(`[orch] cron_at scheduled: ${describeCronAt(runtime.state.cronAtSchedule)} (${runtime.state.cronAtSchedule.reason})`);
201
+ yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
202
+ eventType: "session.cron_at_scheduled",
203
+ data: {
204
+ ...runtime.state.cronAtSchedule,
205
+ nextFireAt: new Date(nextFire.nextFireAtMs).toISOString(),
206
+ localTime: nextFire.localTime,
207
+ skippedOccurrences: nextFire.skippedOccurrences,
208
+ },
209
+ }]);
210
+ }
211
+ export function* drainLeadingQueuedScheduleActions(runtime, sourcePrompt) {
212
+ while (runtime.state.pendingToolActions[0]?.type === "cron" || runtime.state.pendingToolActions[0]?.type === "cron_at") {
213
+ const action = runtime.state.pendingToolActions.shift();
214
+ if (action.type === "cron") {
215
+ applyCronAction(runtime, action, sourcePrompt);
216
+ }
217
+ else {
218
+ yield* applyCronAtAction(runtime, action, sourcePrompt);
219
+ }
220
+ }
221
+ }
222
+ export const drainLeadingQueuedCronActions = drainLeadingQueuedScheduleActions;
223
+ // ─── Cancellation tombstone helpers ─────────────────────────
224
+ export function promptIdsIntersectCancellation(runtime, ids) {
225
+ return ids.length > 0 && ids.some((id) => runtime.state.cancelledMessageIds.has(id));
226
+ }
227
+ export function* recordCancelledMessageIds(runtime, ids, reason) {
228
+ const nextIds = ids.filter((id) => id && !runtime.state.emittedCancelledMessageIds.has(id));
229
+ if (nextIds.length === 0)
230
+ return;
231
+ for (const id of nextIds)
232
+ runtime.state.emittedCancelledMessageIds.add(id);
233
+ yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
234
+ eventType: "pending_messages.cancelled",
235
+ data: {
236
+ clientMessageIds: nextIds,
237
+ reason,
238
+ },
239
+ }]);
240
+ }
241
+ // ─── Pending child-digest accumulation ──────────────────────
242
+ export function bufferChildUpdate(runtime, update, observedAtMs) {
243
+ if (!runtime.state.pendingChildDigest) {
244
+ runtime.state.pendingChildDigest = {
245
+ startedAtMs: observedAtMs,
246
+ updates: [],
247
+ };
248
+ }
249
+ const nextEntry = {
250
+ sessionId: update.sessionId,
251
+ updateType: update.updateType,
252
+ ...(update.content ? { content: update.content.slice(0, 2000) } : {}),
253
+ ...(update.cycleOrigin ? { cycleOrigin: update.cycleOrigin } : {}),
254
+ ...(update.cycleStatus ? { cycleStatus: update.cycleStatus } : {}),
255
+ ...(update.verdict ? { verdict: update.verdict } : {}),
256
+ observedAtMs,
257
+ };
258
+ const existingIndex = runtime.state.pendingChildDigest.updates.findIndex((entry) => entry.sessionId === update.sessionId);
259
+ if (existingIndex >= 0) {
260
+ // One entry per child, latest wins — EXCEPT that a lifecycle update
261
+ // (completed / failed / cancelled) is never overwritten by a later
262
+ // wait or progress note. With the batch window scaled up to five
263
+ // minutes (childUpdateBatchMs) a child can finish and then start
264
+ // waiting inside one window; the parent must still see the finish.
265
+ const existing = runtime.state.pendingChildDigest.updates[existingIndex];
266
+ if (childUpdateRank(nextEntry.updateType) >= childUpdateRank(existing.updateType)) {
267
+ runtime.state.pendingChildDigest.updates[existingIndex] = nextEntry;
268
+ }
269
+ }
270
+ else {
271
+ runtime.state.pendingChildDigest.updates.push(nextEntry);
272
+ }
273
+ }
274
+ function childUpdateRank(updateType) {
275
+ switch (updateType) {
276
+ case "failed":
277
+ case "cancelled":
278
+ case "deleted":
279
+ case "completed":
280
+ return 2;
281
+ default:
282
+ return 1;
283
+ }
284
+ }
285
+ /** True when the buffered digest carries a child failure or cancellation — the parent should hear those at once. */
286
+ export function pendingChildDigestHasError(runtime) {
287
+ const digest = runtime.state.pendingChildDigest;
288
+ if (!digest)
289
+ return false;
290
+ return digest.updates.some((entry) => entry.updateType === "failed" || entry.updateType === "cancelled" || entry.updateType === "deleted");
291
+ }
292
+ export function clearPendingChildDigest(runtime) {
293
+ runtime.state.pendingChildDigest = null;
294
+ }
295
+ export function buildPendingChildDigestSystemPrompt(runtime) {
296
+ const digest = runtime.state.pendingChildDigest;
297
+ if (!digest || digest.updates.length === 0)
298
+ return undefined;
299
+ const lines = digest.updates.map((update) => {
300
+ const agent = runtime.state.subAgents.find((entry) => entry.sessionId === update.sessionId);
301
+ const label = agent?.orchId ?? update.sessionId;
302
+ const task = agent?.task ? `Task: "${agent.task.slice(0, 120)}"\n` : "";
303
+ const status = agent?.status ?? update.updateType;
304
+ const resultText = String(update.content || agent?.result || "").trim();
305
+ const result = resultText ? resultText.slice(0, 240) : "(no summary)";
306
+ return ` - Agent ${label}\n` +
307
+ ` ${task}` +
308
+ ` Update: ${update.updateType}\n` +
309
+ ` Status: ${status}\n` +
310
+ ` Result: ${result}`;
311
+ });
312
+ return `Buffered child updates arrived during the last 30 seconds:\n${lines.join("\n")}\nReview the updates and continue your task.`;
313
+ }
314
+ export function flushPendingChildDigestIntoPrompt(runtime, rawPrompt) {
315
+ const childDigestPrompt = buildPendingChildDigestSystemPrompt(runtime);
316
+ if (!childDigestPrompt)
317
+ return rawPrompt;
318
+ clearPendingChildDigest(runtime);
319
+ return appendSystemContext(rawPrompt, childDigestPrompt);
320
+ }
321
+ // ─── Followup queueing ──────────────────────────────────────
322
+ export function queueFollowup(runtime, nextPrompt) {
323
+ let text = nextPrompt;
324
+ const trimmed = text.trim();
325
+ if (trimmed.startsWith("[SYSTEM:") && trimmed.endsWith("]")) {
326
+ text = trimmed.slice("[SYSTEM:".length, -1).trim();
327
+ }
328
+ runtime.state.pendingPrompt = mergePrompt(runtime.state.pendingPrompt, text);
329
+ }
330
+ // ─── Continue-as-new construction ───────────────────────────
331
+ export function buildContinueInput(runtime, overrides = {}) {
332
+ const { state, options, input } = runtime;
333
+ const { prompt: overridePrompt, requiredTool: overrideRequiredTool, systemPrompt: overrideSystemPrompt, cycleOrigin: overrideCycleOrigin, bootstrapPrompt: overrideBootstrapPrompt, rehydrationMessage: overrideRehydrationMessage, ...restOverrides } = overrides;
334
+ const carriedPrompt = overridePrompt ?? state.pendingPrompt;
335
+ const carriedRequiredTool = overrideRequiredTool ?? state.pendingRequiredTool;
336
+ const carriedSystemPrompt = overrideSystemPrompt ?? state.pendingSystemPrompt;
337
+ const carriedCycleOrigin = overrideCycleOrigin ?? state.pendingCycleOrigin;
338
+ const carriedRehydrationMessage = overrideRehydrationMessage ?? state.pendingRehydrationMessage;
339
+ const promptForInput = carriedPrompt
340
+ ?? (carriedSystemPrompt ? INTERNAL_SYSTEM_TURN_PROMPT : undefined);
341
+ const bootstrapForInput = overrideBootstrapPrompt
342
+ ?? (carriedPrompt ? state.bootstrapPrompt : carriedSystemPrompt ? true : undefined);
343
+ return {
344
+ sessionId: input.sessionId,
345
+ config: state.config,
346
+ iteration: state.iteration,
347
+ affinityKey: state.affinityKey,
348
+ preserveAffinityOnHydrate: state.preserveAffinityOnHydrate,
349
+ needsHydration: state.needsHydration,
350
+ snapshotVersion: state.snapshotVersion,
351
+ blobEnabled: state.blobEnabled,
352
+ idleTimeout: options.idleTimeout,
353
+ inputGracePeriod: options.inputGracePeriod,
354
+ ...(carriedRehydrationMessage ? { rehydrationMessage: carriedRehydrationMessage } : {}),
355
+ nextSummarizeAt: state.nextSummarizeAt,
356
+ taskContext: state.taskContext,
357
+ baseSystemMessage: options.baseSystemMessage,
358
+ ...(state.blockedError ? { blockedError: { ...state.blockedError } } : {}),
359
+ ...(state.cronSchedule ? { cronSchedule: state.cronSchedule } : {}),
360
+ ...(state.cronAtSchedule ? { cronAtSchedule: state.cronAtSchedule } : {}),
361
+ ...(state.contextUsage ? { contextUsage: state.contextUsage } : {}),
362
+ ...(state.recentClientMessageIds?.length ? { recentClientMessageIds: [...state.recentClientMessageIds] } : {}),
363
+ ...(carriedSystemPrompt ? { systemPrompt: carriedSystemPrompt } : {}),
364
+ ...(state.runtimeModelNotice ? { runtimeModelNotice: state.runtimeModelNotice } : {}),
365
+ ...(promptForInput ? { prompt: promptForInput } : {}),
366
+ // Attachment refs ride the carried prompt — without this, any prompt
367
+ // consumed into pendingPrompt across continue-as-new silently became
368
+ // text-only (the string-only carry predates image attachments).
369
+ ...(promptForInput && state.pendingAttachments?.length
370
+ ? { attachments: [...state.pendingAttachments] }
371
+ : {}),
372
+ ...(carriedRequiredTool ? { requiredTool: carriedRequiredTool } : {}),
373
+ ...(carriedCycleOrigin ? { cycleOrigin: carriedCycleOrigin } : {}),
374
+ ...(promptForInput && bootstrapForInput !== undefined ? { bootstrapPrompt: bootstrapForInput } : {}),
375
+ subAgents: state.subAgents,
376
+ ...(state.reportedFirstCompletionToParent ? { reportedFirstCompletionToParent: true } : {}),
377
+ ...(state.pendingToolActions.length > 0 ? { pendingToolActions: state.pendingToolActions } : {}),
378
+ // Session regeneration (1.0.67): epoch + in-flight pipeline state ride
379
+ // EVERY continue-as-new (forced CANs included) or they silently drop.
380
+ transcriptEpoch: state.transcriptEpoch,
381
+ ...(state.epochStartPending ? { epochStartPending: true } : {}),
382
+ ...(state.regen ? { regen: state.regen } : {}),
383
+ ...(state.pendingEpochCommit ? { pendingEpochCommit: state.pendingEpochCommit } : {}),
384
+ ...(state.epochStartIteration ? { epochStartIteration: state.epochStartIteration } : {}),
385
+ ...(state.lastRegenAtMs ? { lastRegenAtMs: state.lastRegenAtMs } : {}),
386
+ parentSessionId: options.parentSessionId,
387
+ nestingLevel: options.nestingLevel,
388
+ ...(options.isSystem ? { isSystem: true } : {}),
389
+ ...(input.agentId ? { agentId: input.agentId } : {}),
390
+ retryCount: 0,
391
+ ...(state.pendingInputQuestion ? { pendingInputQuestion: state.pendingInputQuestion } : {}),
392
+ ...(state.waitingForAgentIds ? { waitingForAgentIds: state.waitingForAgentIds } : {}),
393
+ ...(state.interruptedWaitTimer ? { interruptedWaitTimer: state.interruptedWaitTimer } : {}),
394
+ // A queued-while-blocked prompt must survive the epoch boundary too,
395
+ // or continue-as-new becomes one more way to destroy it.
396
+ ...(state.budgetStash && state.budgetStash.length > 0 ? { budgetStash: state.budgetStash } : {}),
397
+ ...(state.interruptedCronTimer ? { interruptedCronTimer: state.interruptedCronTimer } : {}),
398
+ ...(state.pendingChildDigest ? { pendingChildDigest: state.pendingChildDigest } : {}),
399
+ ...(state.pendingShutdown ? { pendingShutdown: state.pendingShutdown } : {}),
400
+ // Multi-writer attribution posture (security model): carried so a
401
+ // shared session stays attributed across continue-as-new. Omitted
402
+ // entirely for single-writer sessions, keeping the CAN input
403
+ // byte-identical to pre-sender builds.
404
+ ...(state.observedSenderKeys?.length ? { observedSenderKeys: [...state.observedSenderKeys] } : {}),
405
+ ...(state.multiWriter ? { multiWriter: true } : {}),
406
+ ...(state.sharedPreambleSent ? { sharedPreambleSent: true } : {}),
407
+ ...(state.ownerDisplay ? { ownerDisplay: state.ownerDisplay } : {}),
408
+ ...restOverrides,
409
+ };
410
+ }
411
+ export function buildContinueInputWithPrompt(runtime, nextPrompt, overrides = {}) {
412
+ const extracted = extractPromptSystemContext(nextPrompt);
413
+ const mergedPrompt = mergePrompt(runtime.state.pendingPrompt, extracted.prompt);
414
+ const mergedSystemPrompt = mergePrompt(runtime.state.pendingSystemPrompt, extracted.systemPrompt);
415
+ return buildContinueInput(runtime, {
416
+ ...(mergedPrompt ? { prompt: mergedPrompt } : {}),
417
+ ...(mergedSystemPrompt ? { systemPrompt: mergedSystemPrompt } : {}),
418
+ ...overrides,
419
+ });
420
+ }
421
+ /** Capture the active timer state into a continueAsNew input and yield the version-bumped CAN. */
422
+ export function* versionedContinueAsNew(runtime, canInput) {
423
+ const { state } = runtime;
424
+ if (state.activeTimer) {
425
+ const now = yield runtime.ctx.utcNow();
426
+ const remainingMs = Math.max(0, state.activeTimer.deadlineMs - now);
427
+ canInput.activeTimerState = {
428
+ remainingMs,
429
+ reason: state.activeTimer.reason,
430
+ type: state.activeTimer.type,
431
+ originalDurationMs: state.activeTimer.originalDurationMs,
432
+ ...(state.activeTimer.shouldRehydrate ? { shouldRehydrate: true } : {}),
433
+ ...(state.activeTimer.waitPlan ? { waitPlan: state.activeTimer.waitPlan } : {}),
434
+ ...(state.activeTimer.content ? { content: state.activeTimer.content } : {}),
435
+ ...(state.activeTimer.question ? { question: state.activeTimer.question } : {}),
436
+ ...(state.activeTimer.choices ? { choices: state.activeTimer.choices } : {}),
437
+ ...(state.activeTimer.allowFreeform !== undefined ? { allowFreeform: state.activeTimer.allowFreeform } : {}),
438
+ ...(state.activeTimer.agentIds ? { agentIds: state.activeTimer.agentIds } : {}),
439
+ };
440
+ }
441
+ // Lifecycle protocol: no checkpoint before a warm CAN — every turn
442
+ // already committed its snapshot inside the runTurn activity, so a CAN
443
+ // carries no undurable state.
444
+ canInput.sourceOrchestrationVersion = runtime.versions.currentVersion;
445
+ yield runtime.ctx.continueAsNewVersioned(canInput, runtime.versions.latestVersion);
446
+ }
447
+ export function continueInput(runtime, overrides = {}) {
448
+ return buildContinueInput(runtime, overrides);
449
+ }
450
+ export function continueInputWithPrompt(runtime, nextPrompt, overrides = {}) {
451
+ return buildContinueInputWithPrompt(runtime, nextPrompt, overrides);
452
+ }
453
+ // ─── Command handling ───────────────────────────────────────
454
+ export function* handleCommand(runtime, cmdMsg) {
455
+ runtime.ctx.traceInfo(`[orch-cmd] ${cmdMsg.cmd} id=${cmdMsg.id}`);
456
+ yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
457
+ eventType: "session.command_received",
458
+ data: { cmd: cmdMsg.cmd, id: cmdMsg.id },
459
+ }]);
460
+ switch (cmdMsg.cmd) {
461
+ case "set_model": {
462
+ const newModel = String(cmdMsg.args?.model || "").trim();
463
+ if (!newModel) {
464
+ const resp = {
465
+ id: cmdMsg.id,
466
+ cmd: cmdMsg.cmd,
467
+ error: "set_model requires a non-empty model",
468
+ };
469
+ yield* writeCommandResponse(runtime, resp);
470
+ publishStatus(runtime, "idle");
471
+ return;
472
+ }
473
+ const oldModel = runtime.state.config.model || "(default)";
474
+ const hasEffort = cmdMsg.args?.reasoningEffort !== undefined;
475
+ const oldEffort = runtime.state.config.reasoningEffort ?? null;
476
+ const newEffort = hasEffort
477
+ ? (cmdMsg.args?.reasoningEffort ? String(cmdMsg.args.reasoningEffort) : null)
478
+ : oldEffort;
479
+ // Context-window tier is applied like reasoning effort: omitted from
480
+ // the args → preserve the session's current tier; present → apply it
481
+ // (the next turn rebinds the model, requiresModelRebind() sees the
482
+ // change). Durable via the checkpointed orchestration config.
483
+ const hasContextTier = cmdMsg.args?.contextTier !== undefined;
484
+ const oldContextTier = runtime.state.config.contextTier ?? null;
485
+ const newContextTier = hasContextTier
486
+ ? (cmdMsg.args?.contextTier ? String(cmdMsg.args.contextTier) : null)
487
+ : oldContextTier;
488
+ runtime.state.config = {
489
+ ...runtime.state.config,
490
+ model: newModel,
491
+ ...(hasEffort ? { reasoningEffort: newEffort } : {}),
492
+ ...(hasContextTier ? { contextTier: newContextTier } : {}),
493
+ };
494
+ const newModelLabel = newEffort ? `${newModel}:${newEffort}` : newModel;
495
+ runtime.state.runtimeModelNotice = `Runtime model for this turn is ${newModelLabel}. If asked what model you are using, answer this value.`;
496
+ yield* captureModelSwitchInterruptedTimer(runtime, newModelLabel);
497
+ yield runtime.manager.updateSessionModel(runtime.input.sessionId, newModel, newEffort, newContextTier, String(cmdMsg.args?.source ?? "user"));
498
+ yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
499
+ eventType: "session.model_changed",
500
+ data: { oldModel, newModel, oldReasoningEffort: oldEffort, newReasoningEffort: newEffort, oldContextTier, newContextTier, source: cmdMsg.args?.source ?? "user" },
501
+ }]);
502
+ const resp = {
503
+ id: cmdMsg.id,
504
+ cmd: cmdMsg.cmd,
505
+ result: { ok: true, oldModel, newModel, oldReasoningEffort: oldEffort, newReasoningEffort: newEffort, oldContextTier, newContextTier, appliesOn: "next_turn" },
506
+ };
507
+ yield* writeCommandResponse(runtime, resp);
508
+ publishStatus(runtime, "idle");
509
+ yield* versionedContinueAsNew(runtime, continueInputWithPrompt(runtime, `Continue on ${newModelLabel}.`, {
510
+ bootstrapPrompt: true,
511
+ }));
512
+ return;
513
+ }
514
+ case "regenerate": {
515
+ const state = runtime.state;
516
+ const nowMs = yield runtime.ctx.utcNow();
517
+ const source = String(cmdMsg.args?.source ?? "operator");
518
+ // Operator force: an explicit operator action (button/API/MCP, gated
519
+ // session:manage) may bypass the SOFT rate limits (min-age; cooldown
520
+ // is already operator-exempt below). It never bypasses the hard
521
+ // gates: a regen already in flight, or a system session.
522
+ const force = cmdMsg.args?.force === true && source === "operator";
523
+ const refuse = function* (reason) {
524
+ yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
525
+ eventType: "session.regenerate_refused",
526
+ data: { attemptId: cmdMsg.id, reason, source, requestedBy: cmdMsg.requestedBy ?? null },
527
+ }]);
528
+ yield* writeCommandResponse(runtime, { id: cmdMsg.id, cmd: cmdMsg.cmd, error: reason });
529
+ publishStatus(runtime, "idle");
530
+ };
531
+ if (state.regen) {
532
+ yield* refuse("already_pending");
533
+ return;
534
+ }
535
+ if (runtime.options.isSystem) {
536
+ yield* refuse("is_system");
537
+ return;
538
+ }
539
+ // A session tearing down must not start a 5-minute distill pipeline
540
+ // the shutdown would then abandon (leaking the distiller and, on a
541
+ // held grounding prompt, spinning continue-as-news). Force cannot
542
+ // bypass this — it is a hard gate, not a rate limit.
543
+ if (state.pendingShutdown) {
544
+ yield* refuse("shutting_down");
545
+ return;
546
+ }
547
+ // The previous flip's rebirth is not yet proven (grounding turn
548
+ // pending). Starting another regen now would replace the unconsumed
549
+ // pendingEpochCommit and merge the two grounding bootstraps
550
+ // (adversarial-review finding). Also a hard gate — force cannot skip it.
551
+ if (state.epochStartPending) {
552
+ yield* refuse("epoch_unsettled");
553
+ return;
554
+ }
555
+ if (!force && state.iteration - state.epochStartIteration < 5) {
556
+ yield* refuse("too_young");
557
+ return;
558
+ }
559
+ // Cooldown applies to every agent-driven trigger — self (tool) AND
560
+ // parent — so a supervising parent (which a child can prompt-inject)
561
+ // cannot destroy a child's transcript every few turns. Operator and
562
+ // policy triggers bypass the cooldown but still respect min-age.
563
+ const REGEN_COOLDOWN_MS = 6 * 60 * 60 * 1000;
564
+ if ((source === "tool" || source === "parent")
565
+ && state.lastRegenAtMs > 0 && nowMs - state.lastRegenAtMs < REGEN_COOLDOWN_MS) {
566
+ yield* refuse("cooldown");
567
+ return;
568
+ }
569
+ if (source === "tool") {
570
+ // Owner-sender gate: a non-owner-attributed turn must not
571
+ // trigger a self-regen. Positive assertion — refuse whenever an
572
+ // explicitly non-owner sender is present (a collaborator human,
573
+ // OR a child/system-instigated turn whose injection tried to
574
+ // make the agent self-regen). A senderless self-driven turn
575
+ // (the common single-writer case) has no non-owner principal
576
+ // and is allowed. Not gated on the derived multiWriter flag,
577
+ // which never flips for agent/system senders.
578
+ const sender = normalizeMessageSender(cmdMsg.sender);
579
+ if (sender && sender.relation !== "owner") {
580
+ yield* refuse("not_owner");
581
+ return;
582
+ }
583
+ }
584
+ else if (source === "parent") {
585
+ if (!cmdMsg.requestedBy || cmdMsg.requestedBy !== runtime.options.parentSessionId) {
586
+ yield* refuse("not_parent");
587
+ return;
588
+ }
589
+ }
590
+ const handoffRaw = typeof cmdMsg.args?.handoff === "string" ? cmdMsg.args.handoff : undefined;
591
+ const instructionsRaw = typeof cmdMsg.args?.instructions === "string" ? cmdMsg.args.instructions : undefined;
592
+ state.regen = {
593
+ attemptId: cmdMsg.id,
594
+ stage: "requested",
595
+ requestedAtMs: nowMs,
596
+ trigger: source === "tool" ? "tool" : source === "parent" ? "parent" : source === "policy" ? "policy" : "operator",
597
+ ...(cmdMsg.requestedBy ? { requestedBy: cmdMsg.requestedBy } : {}),
598
+ ...(handoffRaw ? { handoff: handoffRaw.slice(0, 4_000) } : {}),
599
+ ...(instructionsRaw ? { instructions: instructionsRaw.slice(0, 4_000) } : {}),
600
+ // LLM (service-session) distillation is the default; callers may
601
+ // pick the fast deterministic package per regen.
602
+ distillMode: cmdMsg.args?.distill_mode === "deterministic" ? "deterministic" : "llm",
603
+ ...(typeof cmdMsg.args?.distillerReasoningEffort === "string" && cmdMsg.args.distillerReasoningEffort
604
+ ? { distillerReasoningEffort: String(cmdMsg.args.distillerReasoningEffort) }
605
+ : {}),
606
+ ...(typeof cmdMsg.args?.distillerContextTier === "string" && cmdMsg.args.distillerContextTier
607
+ ? { distillerContextTier: String(cmdMsg.args.distillerContextTier) }
608
+ : {}),
609
+ ...(typeof cmdMsg.args?.distillerModel === "string" && cmdMsg.args.distillerModel
610
+ ? { distillerModel: String(cmdMsg.args.distillerModel) }
611
+ : {}),
612
+ ...(typeof cmdMsg.args?.model === "string" && cmdMsg.args.model
613
+ ? { model: String(cmdMsg.args.model) }
614
+ : {}),
615
+ };
616
+ yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
617
+ eventType: "session.regenerate_requested",
618
+ data: { attemptId: cmdMsg.id, trigger: state.regen.trigger, requestedBy: cmdMsg.requestedBy ?? null, hasHandoff: Boolean(handoffRaw) },
619
+ }]);
620
+ yield* writeCommandResponse(runtime, {
621
+ id: cmdMsg.id,
622
+ cmd: cmdMsg.cmd,
623
+ result: { ok: true, accepted: true, attemptId: cmdMsg.id, fromEpoch: state.transcriptEpoch, appliesOn: "pipeline" },
624
+ });
625
+ publishStatus(runtime, "running", { regenStage: "requested" });
626
+ return;
627
+ }
628
+ case "cancel_regen": {
629
+ const state = runtime.state;
630
+ if (!state.regen) {
631
+ yield* writeCommandResponse(runtime, { id: cmdMsg.id, cmd: cmdMsg.cmd, error: "no_regen_pending" });
632
+ publishStatus(runtime, "idle");
633
+ return;
634
+ }
635
+ const cancelled = state.regen.attemptId;
636
+ yield* cancelInFlightDistiller(runtime);
637
+ state.regen = null;
638
+ yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
639
+ eventType: "session.regenerate_failed",
640
+ data: { attemptId: cancelled, stage: "cancelled", error: "cancelled by request" },
641
+ }]);
642
+ yield* writeCommandResponse(runtime, { id: cmdMsg.id, cmd: cmdMsg.cmd, result: { ok: true, cancelledAttemptId: cancelled } });
643
+ publishStatus(runtime, "idle");
644
+ return;
645
+ }
646
+ case "list_models": {
647
+ publishStatus(runtime, "idle", { cmdProcessing: cmdMsg.id });
648
+ let models;
649
+ try {
650
+ const raw = yield runtime.manager.listModels();
651
+ models = typeof raw === "string" ? JSON.parse(raw) : raw;
652
+ }
653
+ catch (err) {
654
+ const resp = {
655
+ id: cmdMsg.id,
656
+ cmd: cmdMsg.cmd,
657
+ error: err.message || String(err),
658
+ };
659
+ yield* writeCommandResponse(runtime, resp);
660
+ publishStatus(runtime, "idle");
661
+ return;
662
+ }
663
+ const resp = {
664
+ id: cmdMsg.id,
665
+ cmd: cmdMsg.cmd,
666
+ result: { models, currentModel: runtime.state.config.model },
667
+ };
668
+ yield* writeCommandResponse(runtime, resp);
669
+ publishStatus(runtime, "idle");
670
+ return;
671
+ }
672
+ case "get_info": {
673
+ const resp = {
674
+ id: cmdMsg.id,
675
+ cmd: cmdMsg.cmd,
676
+ result: {
677
+ model: runtime.state.config.model || "(default)",
678
+ iteration: runtime.state.iteration,
679
+ sessionId: runtime.input.sessionId,
680
+ affinityKey: runtime.state.affinityKey,
681
+ affinityKeyShort: runtime.state.affinityKey?.slice(0, 8),
682
+ preserveAffinityOnHydrate: runtime.state.preserveAffinityOnHydrate,
683
+ needsHydration: runtime.state.needsHydration,
684
+ blobEnabled: runtime.state.blobEnabled,
685
+ contextUsage: runtime.state.contextUsage,
686
+ },
687
+ };
688
+ yield* writeCommandResponse(runtime, resp);
689
+ publishStatus(runtime, "idle");
690
+ return;
691
+ }
692
+ case "done":
693
+ case "cancel":
694
+ case "delete": {
695
+ runtime.ctx.traceInfo(`[orch] ${cmdMsg.cmd} command received — beginning graceful ${cmdMsg.cmd}`);
696
+ yield* beginGracefulShutdown(runtime, cmdMsg.cmd, cmdMsg);
697
+ return;
698
+ }
699
+ default: {
700
+ const resp = {
701
+ id: cmdMsg.id,
702
+ cmd: cmdMsg.cmd,
703
+ error: `Unknown command: ${cmdMsg.cmd}`,
704
+ };
705
+ yield* writeCommandResponse(runtime, resp);
706
+ publishStatus(runtime, "idle");
707
+ return;
708
+ }
709
+ }
710
+ }
711
+ function* captureModelSwitchInterruptedTimer(runtime, newModelLabel) {
712
+ const timer = runtime.state.activeTimer;
713
+ if (!timer)
714
+ return;
715
+ const now = yield runtime.ctx.utcNow();
716
+ const notePrefix = `Model switch accepted; continuing immediately on ${newModelLabel}`;
717
+ switch (timer.type) {
718
+ case "wait": {
719
+ const remainingMs = Math.max(0, timer.deadlineMs - now);
720
+ runtime.state.interruptedWaitTimer = {
721
+ remainingSec: Math.max(1, Math.round(remainingMs / 1000)),
722
+ reason: timer.reason,
723
+ shouldRehydrate: timer.shouldRehydrate ?? false,
724
+ ...(timer.waitPlan ? { waitPlan: timer.waitPlan } : {}),
725
+ };
726
+ runtime.ctx.traceInfo(`[orch-cmd] ${notePrefix}; will auto-resume interrupted wait (${runtime.state.interruptedWaitTimer.remainingSec}s remain)`);
727
+ runtime.state.activeTimer = null;
728
+ return;
729
+ }
730
+ case "cron": {
731
+ const remainingMs = Math.max(0, timer.deadlineMs - now);
732
+ runtime.state.interruptedCronTimer = {
733
+ remainingMs,
734
+ reason: timer.reason,
735
+ originalDurationMs: timer.originalDurationMs,
736
+ ...(timer.shouldRehydrate ? { shouldRehydrate: true } : {}),
737
+ };
738
+ runtime.ctx.traceInfo(`[orch-cmd] ${notePrefix}; will auto-resume interrupted cron (${Math.round(remainingMs / 1000)}s remain)`);
739
+ runtime.state.activeTimer = null;
740
+ return;
741
+ }
742
+ case "cron_at":
743
+ case "idle":
744
+ case "agent-poll":
745
+ case "input-grace":
746
+ runtime.ctx.traceInfo(`[orch-cmd] ${notePrefix}; clearing active ${timer.type} timer`);
747
+ runtime.state.activeTimer = null;
748
+ return;
749
+ }
750
+ }
751
+ // ─── Session regeneration pipeline (1.0.67, proposal §4) ────────
752
+ //
753
+ // Called from the run loop AFTER drain whenever state.regen is set: one
754
+ // stage per invocation, returning to the loop between stages so a queued
755
+ // cancel_regen / cancel / delete pre-empts a pending regen. Archive and
756
+ // distill are attempt-idempotent activities; any pre-flip failure aborts
757
+ // into the fail-safe (regen cleared, session continues in the old epoch —
758
+ // nothing has changed). The flip is the point of no return: a continue-as-
759
+ // new carrying the fresh epoch, the boundary record, and the flip-mutation
760
+ // table's dispositions (contextUsage zeroed, shared preamble re-armed,
761
+ // pending question and roster carried).
762
+ /** Overall budget for a service-session distillation before the deterministic fallback. */
763
+ const DISTILLER_SESSION_DEADLINE_MS = 5 * 60 * 1000;
764
+ /** Durable pause between distiller polls (drain sweeps for pre-empting cmds in between). */
765
+ const DISTILLER_POLL_MS = 10_000;
766
+ /**
767
+ * Deterministic distill: the in-activity closure package (M1 path). Shared by
768
+ * the per-regen deterministic mode and every LLM-path fallback (no model,
769
+ * distiller failed, deadline exceeded) — the regen never blocks on
770
+ * distillation quality.
771
+ */
772
+ function* runDeterministicDistill(runtime, regen) {
773
+ const state = runtime.state;
774
+ const distill = yield runtime.manager.runRegenDistill(runtime.input.sessionId, state.transcriptEpoch, regen.attemptId, {
775
+ ...(regen.handoff ? { handoff: regen.handoff } : {}),
776
+ ...(regen.instructions ? { instructions: regen.instructions } : {}),
777
+ ...(state.config.model ? { sessionModel: state.config.model } : {}),
778
+ ...(regen.distillerModel ? { distillerModel: regen.distillerModel } : {}),
779
+ ...(regen.distillerReasoningEffort ? { distillerReasoningEffort: regen.distillerReasoningEffort } : {}),
780
+ ...(regen.distillerContextTier ? { distillerContextTier: regen.distillerContextTier } : {}),
781
+ ...(regen.archiveArtifactId ? { archiveArtifactId: regen.archiveArtifactId } : {}),
782
+ ...(regen.archiveChunkIds?.length ? { archiveChunkIds: regen.archiveChunkIds } : {}),
783
+ });
784
+ const bootstrap = String(distill?.bootstrap ?? "");
785
+ if (!bootstrap)
786
+ throw new Error("distill produced no bootstrap");
787
+ state.regen = {
788
+ ...regen,
789
+ stage: "distilled",
790
+ packageArtifactId: String(distill?.packageArtifactId ?? ""),
791
+ bootstrap,
792
+ };
793
+ state.regen.distillMs = Number(distill?.distillMs) || 0;
794
+ state.regen.distillerModel = String(distill?.distillerModel ?? "");
795
+ state.regen.distillModeFinal = "deterministic";
796
+ }
797
+ export function* advanceRegenPipeline(runtime) {
798
+ const { state } = runtime;
799
+ const regen = state.regen;
800
+ if (!regen)
801
+ return false;
802
+ const sessionId = runtime.input.sessionId;
803
+ try {
804
+ if (regen.stage === "requested") {
805
+ publishStatus(runtime, "running", { regenStage: "archiving" });
806
+ const archive = yield runtime.manager.runRegenArchive(sessionId, state.transcriptEpoch, regen.attemptId);
807
+ state.regen = {
808
+ ...regen,
809
+ stage: "archived",
810
+ archiveArtifactId: String(archive?.archiveArtifactId ?? ""),
811
+ };
812
+ // Chunk list rides alongside the id so the distiller can page the
813
+ // WHOLE archive; archiveArtifactId stays the first chunk for
814
+ // consumers (and already-archived epochs) that predate chunking.
815
+ state.regen.selectionStrategy = String(archive?.selectionStrategy ?? "");
816
+ state.regen.elidedCount = Number(archive?.elidedCount) || 0;
817
+ state.regen.archiveChunkIds = Array.isArray(archive?.archiveChunkIds)
818
+ ? archive.archiveChunkIds.map((id) => String(id))
819
+ : [];
820
+ state.regen.archiveMs = Number(archive?.archiveMs) || 0;
821
+ state.regen.turnsArchived = Number(archive?.turnsArchived) || 0;
822
+ state.regen.compactionsArchived = Number(archive?.compactionsArchived) || 0;
823
+ return false;
824
+ }
825
+ if (regen.stage === "archived") {
826
+ publishStatus(runtime, "running", { regenStage: "distilling" });
827
+ // Deterministic mode (per-regen choice) → the in-activity closure
828
+ // package, exactly the M1 path. LLM mode (default) → spawn the
829
+ // regen-distiller SERVICE SESSION under the tree root and poll it.
830
+ if (regen.distillMode === "deterministic") {
831
+ yield* runDeterministicDistill(runtime, regen);
832
+ return false;
833
+ }
834
+ const spawn = yield runtime.manager.runRegenSpawnDistiller(sessionId, state.transcriptEpoch, regen.attemptId, {
835
+ ...(regen.archiveArtifactId ? { archiveArtifactId: regen.archiveArtifactId } : {}),
836
+ ...(regen.archiveChunkIds?.length ? { archiveChunkIds: regen.archiveChunkIds } : {}),
837
+ ...(regen.handoff ? { handoff: regen.handoff } : {}),
838
+ ...(regen.instructions ? { instructions: regen.instructions } : {}),
839
+ ...(regen.distillerModel ? { distillerModel: regen.distillerModel } : {}),
840
+ ...(regen.distillerReasoningEffort ? { distillerReasoningEffort: regen.distillerReasoningEffort } : {}),
841
+ ...(regen.distillerContextTier ? { distillerContextTier: regen.distillerContextTier } : {}),
842
+ });
843
+ if (!spawn?.distillerSessionId) {
844
+ // No resolvable distiller model (or deterministic-only deployment
845
+ // kill switch inside the activity) — the deterministic package is
846
+ // the floor, never a blocked regen.
847
+ runtime.ctx.traceInfo(`[orch] distiller spawn fell back (${String(spawn?.fallback ?? "unknown")}) — deterministic package`);
848
+ yield* runDeterministicDistill(runtime, regen);
849
+ return false;
850
+ }
851
+ const spawnedAt = yield runtime.ctx.utcNow();
852
+ state.regen = {
853
+ ...regen,
854
+ stage: "distilling",
855
+ distillerSessionId: String(spawn.distillerSessionId),
856
+ // Record the RESOLVED model separately — leave distillerModel
857
+ // (the requester's override) intact so a later deterministic
858
+ // fallback still resolves the operator's choice, not "(default)".
859
+ distillerModelResolved: String(spawn.distillerModel ?? "(default)"),
860
+ distillStartedAtMs: spawnedAt,
861
+ };
862
+ return false;
863
+ }
864
+ if (regen.stage === "distilling") {
865
+ publishStatus(runtime, "running", { regenStage: "distilling", distillerSessionId: regen.distillerSessionId });
866
+ const nowMs = yield runtime.ctx.utcNow();
867
+ const startedAt = regen.distillStartedAtMs ?? regen.requestedAtMs;
868
+ const check = yield runtime.manager.runRegenCheckDistiller(regen.distillerSessionId);
869
+ if (check?.status === "completed") {
870
+ const collect = yield runtime.manager.runRegenCollectDistiller(sessionId, state.transcriptEpoch, regen.attemptId, regen.distillerSessionId, {
871
+ ...(regen.archiveArtifactId ? { archiveArtifactId: regen.archiveArtifactId } : {}),
872
+ ...(regen.archiveChunkIds?.length ? { archiveChunkIds: regen.archiveChunkIds } : {}),
873
+ ...(regen.handoff ? { handoff: regen.handoff } : {}),
874
+ ...(regen.instructions ? { instructions: regen.instructions } : {}),
875
+ ...(regen.distillerModel ? { distillerModel: regen.distillerModel } : {}),
876
+ ...(regen.distillerReasoningEffort ? { distillerReasoningEffort: regen.distillerReasoningEffort } : {}),
877
+ ...(regen.distillerContextTier ? { distillerContextTier: regen.distillerContextTier } : {}),
878
+ });
879
+ const bootstrap = String(collect?.bootstrap ?? "");
880
+ if (!bootstrap)
881
+ throw new Error("distiller collect produced no bootstrap");
882
+ const doneAt = yield runtime.ctx.utcNow();
883
+ state.regen = {
884
+ ...regen,
885
+ stage: "distilled",
886
+ packageArtifactId: String(collect?.packageArtifactId ?? ""),
887
+ bootstrap,
888
+ distillerModelResolved: String(collect?.distillerModel ?? regen.distillerModelResolved ?? "(default)"),
889
+ };
890
+ state.regen.distillMs = Math.max(0, doneAt - startedAt);
891
+ state.regen.distillModeFinal = String(collect?.distillMode ?? "llm");
892
+ return false;
893
+ }
894
+ if (check?.status === "failed" || nowMs - startedAt > DISTILLER_SESSION_DEADLINE_MS) {
895
+ runtime.ctx.traceInfo(`[orch] distiller ${regen.distillerSessionId} ${check?.status === "failed" ? `failed (${check?.reason})` : "deadline exceeded"} — cancelling + deterministic package`);
896
+ yield runtime.manager.runRegenCancelDistiller(regen.distillerSessionId);
897
+ yield* runDeterministicDistill(runtime, regen);
898
+ return false;
899
+ }
900
+ // Still running: pace the poll with a short durable timer, then
901
+ // yield back to the drain (which sweeps for pre-empting cmds).
902
+ yield runtime.ctx.scheduleTimer(DISTILLER_POLL_MS);
903
+ return false;
904
+ }
905
+ // stage "distilled" → FLIP. COMMIT POINT: the distill result is in
906
+ // history; from here replay roll-forwards through the CAN.
907
+ const nowMs = yield runtime.ctx.utcNow();
908
+ publishStatus(runtime, "running", { regenStage: "flipping" });
909
+ const fromEpoch = state.transcriptEpoch;
910
+ const toEpoch = fromEpoch + 1;
911
+ const r = regen;
912
+ // Clear the armed idle/affinity/cron timer (stashing wait/cron as
913
+ // interrupted so they re-arm after the grounding turn) — exactly as
914
+ // set_model does before its bootstrap CAN. Without this the reborn
915
+ // execution restores the timer and drain parks on it BEFORE decide
916
+ // dispatches the grounding bootstrap, delaying the rebirth for up to
917
+ // the hold window.
918
+ yield* captureModelSwitchInterruptedTimer(runtime, "regenerated context");
919
+ // Optional model rebind: the reborn session's grounding turn runs on
920
+ // config.model, so an operator-supplied replacement (e.g. escaping a
921
+ // removed model) must be applied to the carried config before the CAN.
922
+ if (r.model && typeof r.model === "string") {
923
+ state.config = { ...state.config, model: r.model };
924
+ yield runtime.manager.updateSessionModel(sessionId, r.model);
925
+ }
926
+ // Drop runtime notices that describe the dead transcript before the
927
+ // carry-list is built.
928
+ state.runtimeModelNotice = undefined;
929
+ const canInput = {
930
+ ...continueInputWithPrompt(runtime, regen.bootstrap ?? "", { bootstrapPrompt: true }),
931
+ // Grounding is prompt-level in M1 (FIRST ACTIONS in the rendered
932
+ // bootstrap): a hard requiredTool would fail the grounding turn on
933
+ // deployments without fact tools. Enforce once fact tools are
934
+ // guaranteed present.
935
+ transcriptEpoch: toEpoch,
936
+ epochStartPending: true,
937
+ epochStartIteration: state.iteration,
938
+ lastRegenAtMs: nowMs,
939
+ regen: undefined,
940
+ contextUsage: undefined,
941
+ sharedPreambleSent: false,
942
+ pendingEpochCommit: {
943
+ fromEpoch,
944
+ toEpoch,
945
+ attemptId: regen.attemptId,
946
+ trigger: regen.trigger,
947
+ requestedAtMs: regen.requestedAtMs,
948
+ ...(regen.archiveArtifactId ? { archiveArtifactId: regen.archiveArtifactId } : {}),
949
+ ...(regen.archiveChunkIds?.length ? { archiveChunkIds: regen.archiveChunkIds } : {}),
950
+ ...(regen.packageArtifactId ? { packageArtifactId: regen.packageArtifactId } : {}),
951
+ ...(r.turnsArchived ? { turnsArchived: r.turnsArchived } : {}),
952
+ ...(r.compactionsArchived ? { compactionsArchived: r.compactionsArchived } : {}),
953
+ ...(r.archiveMs ? { archiveMs: r.archiveMs } : {}),
954
+ ...(r.distillMs ? { distillMs: r.distillMs } : {}),
955
+ ...(r.distillModeFinal ? { distillMode: r.distillModeFinal } : {}),
956
+ ...((r.distillerModelResolved || r.distillerModel) ? { distillerModel: r.distillerModelResolved || r.distillerModel } : {}),
957
+ ...(r.distillerSessionId && r.distillModeFinal === "llm" ? { distillerSessionId: r.distillerSessionId } : {}),
958
+ },
959
+ };
960
+ yield* versionedContinueAsNew(runtime, canInput);
961
+ return true;
962
+ }
963
+ catch (err) {
964
+ // FAIL-SAFE (pre-flip only): clear the pipeline so a later attempt is
965
+ // not refused as pending; the session continues in the old epoch. If a
966
+ // distiller service session was in flight, cancel it first so it does
967
+ // not park idle forever (the sweeper never reclaims a live session).
968
+ const failedStage = state.regen?.stage ?? "requested";
969
+ const attemptId = state.regen?.attemptId ?? "unknown";
970
+ yield* cancelInFlightDistiller(runtime);
971
+ state.regen = null;
972
+ runtime.ctx.traceInfo(`[orch] regen attempt ${attemptId} failed at ${failedStage}: ${err?.message ?? err}`);
973
+ yield runtime.manager.recordSessionEvent(sessionId, [{
974
+ eventType: "session.regenerate_failed",
975
+ data: { attemptId, stage: failedStage, error: String(err?.message ?? err).slice(0, 500) },
976
+ }]);
977
+ publishStatus(runtime, "idle");
978
+ return false;
979
+ }
980
+ }
981
+ //# sourceMappingURL=lifecycle.js.map