pilotswarm-sdk 0.5.19 → 0.5.21

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