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.
- package/dist/capability-catalog.d.ts +13 -0
- package/dist/capability-catalog.d.ts.map +1 -1
- package/dist/capability-catalog.js +43 -4
- package/dist/capability-catalog.js.map +1 -1
- package/dist/capability-runtime.d.ts +3 -3
- package/dist/capability-runtime.js +3 -3
- package/dist/capability-runtime.js.map +1 -1
- package/dist/managed-session.js +4 -4
- package/dist/managed-session.js.map +1 -1
- package/dist/orchestration/agents.d.ts.map +1 -1
- package/dist/orchestration/agents.js +4 -0
- package/dist/orchestration/agents.js.map +1 -1
- package/dist/orchestration/index.d.ts +1 -1
- package/dist/orchestration/index.js +1 -1
- package/dist/orchestration/runtime.d.ts +1 -1
- package/dist/orchestration-registry.d.ts.map +1 -1
- package/dist/orchestration-registry.js +4 -2
- package/dist/orchestration-registry.js.map +1 -1
- package/dist/orchestration-version.d.ts +1 -1
- package/dist/orchestration-version.js +1 -1
- package/dist/orchestration.d.ts +2 -2
- package/dist/orchestration.js +1 -1
- package/dist/orchestration_1_0_46.d.ts +1 -1
- package/dist/orchestration_1_0_47.d.ts +1 -1
- package/dist/orchestration_1_0_48.d.ts +1 -1
- package/dist/orchestration_1_0_49.d.ts +1 -1
- package/dist/orchestration_1_0_50.d.ts +1 -1
- package/dist/orchestration_1_0_78/agents.d.ts +66 -0
- package/dist/orchestration_1_0_78/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/agents.js +894 -0
- package/dist/orchestration_1_0_78/agents.js.map +1 -0
- package/dist/orchestration_1_0_78/index.d.ts +24 -0
- package/dist/orchestration_1_0_78/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/index.js +13 -0
- package/dist/orchestration_1_0_78/index.js.map +1 -0
- package/dist/orchestration_1_0_78/lifecycle.d.ts +51 -0
- package/dist/orchestration_1_0_78/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/lifecycle.js +981 -0
- package/dist/orchestration_1_0_78/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_78/queue.d.ts +20 -0
- package/dist/orchestration_1_0_78/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/queue.js +894 -0
- package/dist/orchestration_1_0_78/queue.js.map +1 -0
- package/dist/orchestration_1_0_78/runtime.d.ts +31 -0
- package/dist/orchestration_1_0_78/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/runtime.js +244 -0
- package/dist/orchestration_1_0_78/runtime.js.map +1 -0
- package/dist/orchestration_1_0_78/state.d.ts +214 -0
- package/dist/orchestration_1_0_78/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/state.js +178 -0
- package/dist/orchestration_1_0_78/state.js.map +1 -0
- package/dist/orchestration_1_0_78/turn.d.ts +34 -0
- package/dist/orchestration_1_0_78/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/turn.js +1433 -0
- package/dist/orchestration_1_0_78/turn.js.map +1 -0
- package/dist/orchestration_1_0_78/utils.d.ts +39 -0
- package/dist/orchestration_1_0_78/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_78/utils.js +322 -0
- package/dist/orchestration_1_0_78/utils.js.map +1 -0
- package/dist/session-list-timestamps.d.ts +5 -0
- package/dist/session-list-timestamps.d.ts.map +1 -0
- package/dist/session-list-timestamps.js +16 -0
- package/dist/session-list-timestamps.js.map +1 -0
- package/dist/session-manager.d.ts +8 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +79 -7
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +1 -0
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +6 -1
- package/dist/session-proxy.js.map +1 -1
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +21 -2
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- 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
|