pilotswarm-sdk 0.3.1 → 0.3.3
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/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +1090 -1
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +41 -1
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +51 -1
- package/dist/cms.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/inspect-tools.d.ts.map +1 -1
- package/dist/inspect-tools.js +27 -0
- package/dist/inspect-tools.js.map +1 -1
- package/dist/managed-session.d.ts +36 -0
- package/dist/managed-session.d.ts.map +1 -1
- package/dist/managed-session.js +317 -21
- package/dist/managed-session.js.map +1 -1
- package/dist/management-client.d.ts +36 -2
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +114 -2
- package/dist/management-client.js.map +1 -1
- package/dist/orchestration/index.d.ts +2 -2
- package/dist/orchestration/index.js +1 -1
- package/dist/orchestration/lifecycle.d.ts.map +1 -1
- package/dist/orchestration/lifecycle.js +74 -4
- package/dist/orchestration/lifecycle.js.map +1 -1
- package/dist/orchestration/runtime.d.ts +1 -1
- package/dist/orchestration/state.d.ts +1 -0
- package/dist/orchestration/state.d.ts.map +1 -1
- package/dist/orchestration/state.js +1 -0
- package/dist/orchestration/state.js.map +1 -1
- package/dist/orchestration/turn.d.ts +24 -0
- package/dist/orchestration/turn.d.ts.map +1 -1
- package/dist/orchestration/turn.js +378 -167
- package/dist/orchestration/turn.js.map +1 -1
- package/dist/orchestration-registry.d.ts.map +1 -1
- package/dist/orchestration-registry.js +6 -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_54/agents.d.ts +41 -0
- package/dist/orchestration_1_0_54/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/agents.js +758 -0
- package/dist/orchestration_1_0_54/agents.js.map +1 -0
- package/dist/orchestration_1_0_54/index.d.ts +24 -0
- package/dist/orchestration_1_0_54/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/index.js +13 -0
- package/dist/orchestration_1_0_54/index.js.map +1 -0
- package/dist/orchestration_1_0_54/lifecycle.d.ts +41 -0
- package/dist/orchestration_1_0_54/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/lifecycle.js +510 -0
- package/dist/orchestration_1_0_54/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_54/queue.d.ts +7 -0
- package/dist/orchestration_1_0_54/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/queue.js +644 -0
- package/dist/orchestration_1_0_54/queue.js.map +1 -0
- package/dist/orchestration_1_0_54/runtime.d.ts +29 -0
- package/dist/orchestration_1_0_54/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/runtime.js +194 -0
- package/dist/orchestration_1_0_54/runtime.js.map +1 -0
- package/dist/orchestration_1_0_54/state.d.ts +125 -0
- package/dist/orchestration_1_0_54/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/state.js +105 -0
- package/dist/orchestration_1_0_54/state.js.map +1 -0
- package/dist/orchestration_1_0_54/turn.d.ts +6 -0
- package/dist/orchestration_1_0_54/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/turn.js +864 -0
- package/dist/orchestration_1_0_54/turn.js.map +1 -0
- package/dist/orchestration_1_0_54/utils.d.ts +22 -0
- package/dist/orchestration_1_0_54/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_54/utils.js +226 -0
- package/dist/orchestration_1_0_54/utils.js.map +1 -0
- package/dist/orchestration_1_0_55/agents.d.ts +41 -0
- package/dist/orchestration_1_0_55/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/agents.js +758 -0
- package/dist/orchestration_1_0_55/agents.js.map +1 -0
- package/dist/orchestration_1_0_55/index.d.ts +24 -0
- package/dist/orchestration_1_0_55/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/index.js +13 -0
- package/dist/orchestration_1_0_55/index.js.map +1 -0
- package/dist/orchestration_1_0_55/lifecycle.d.ts +41 -0
- package/dist/orchestration_1_0_55/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/lifecycle.js +580 -0
- package/dist/orchestration_1_0_55/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_55/queue.d.ts +7 -0
- package/dist/orchestration_1_0_55/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/queue.js +644 -0
- package/dist/orchestration_1_0_55/queue.js.map +1 -0
- package/dist/orchestration_1_0_55/runtime.d.ts +29 -0
- package/dist/orchestration_1_0_55/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/runtime.js +194 -0
- package/dist/orchestration_1_0_55/runtime.js.map +1 -0
- package/dist/orchestration_1_0_55/state.d.ts +126 -0
- package/dist/orchestration_1_0_55/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/state.js +106 -0
- package/dist/orchestration_1_0_55/state.js.map +1 -0
- package/dist/orchestration_1_0_55/turn.d.ts +17 -0
- package/dist/orchestration_1_0_55/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/turn.js +921 -0
- package/dist/orchestration_1_0_55/turn.js.map +1 -0
- package/dist/orchestration_1_0_55/utils.d.ts +22 -0
- package/dist/orchestration_1_0_55/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_55/utils.js +226 -0
- package/dist/orchestration_1_0_55/utils.js.map +1 -0
- package/dist/session-manager.d.ts +24 -1
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +84 -0
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +19 -0
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +172 -14
- package/dist/session-proxy.js.map +1 -1
- package/dist/sweeper-tools.d.ts +1 -1
- package/dist/sweeper-tools.d.ts.map +1 -1
- package/dist/sweeper-tools.js +20 -75
- package/dist/sweeper-tools.js.map +1 -1
- package/dist/system-agents.d.ts +2 -0
- package/dist/system-agents.d.ts.map +1 -1
- package/dist/system-agents.js +3 -0
- package/dist/system-agents.js.map +1 -1
- package/dist/types.d.ts +46 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +10 -0
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- package/plugins/mgmt/agents/agent-tuner.agent.md +36 -2
- package/plugins/mgmt/agents/sweeper.agent.md +7 -7
- package/plugins/system/agents/default.agent.md +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn.d.ts","sourceRoot":"","sources":["../../src/orchestration/turn.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"turn.d.ts","sourceRoot":"","sources":["../../src/orchestration/turn.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,UAAU,EAAE,MAAM,aAAa,CAAC;AA+BlE,OAAO,EAKH,KAAK,qBAAqB,EAC7B,MAAM,YAAY,CAAC;AAkCpB;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAYpH;AAiJD,wBAAiB,aAAa,CAC1B,OAAO,EAAE,qBAAqB,EAC9B,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,OAAO,EACpB,YAAY,CAAC,EAAE,MAAM,EACrB,gBAAgB,CAAC,EAAE,MAAM,EAAE,EAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,GACjC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CAkM3B;AAID;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,GAAG,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAaxH;AA+TD,wBAAiB,gBAAgB,CAC7B,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,UAAU,EAClB,YAAY,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,GACjC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CA0R3B;AAID,wBAAiB,YAAY,CACzB,OAAO,EAAE,qBAAqB,EAC9B,SAAS,EAAE,GAAG,GACf,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CAgM3B"}
|
|
@@ -1,12 +1,49 @@
|
|
|
1
|
-
import { SESSION_STATE_MISSING_PREFIX } from "../types.js";
|
|
1
|
+
import { SESSION_STATE_MISSING_PREFIX, stopTurnQueueName } from "../types.js";
|
|
2
2
|
import { createSessionProxy } from "../session-proxy.js";
|
|
3
3
|
import { planWaitHandling } from "../wait-affinity.js";
|
|
4
4
|
import { buildShutdownWaitReason, failPendingShutdown, getStillRunningAgentIds, handleSubAgentAction, isSubAgentTerminalStatus, maybeResolveAgentWaitCompletion, refreshTrackedSubAgents, } from "./agents.js";
|
|
5
|
-
import { applyCronAtAction, applyCronAction, continueInput, continueInputWithPrompt, dehydrateForNextTurn, drainLeadingQueuedScheduleActions, ensureTaskContext, maybeCheckpoint, maybeSummarize, publishStatus, versionedContinueAsNew, wrapWithResumeContext, writeLatestResponse, } from "./lifecycle.js";
|
|
5
|
+
import { applyCronAtAction, applyCronAction, continueInput, continueInputWithPrompt, dehydrateForNextTurn, drainLeadingQueuedScheduleActions, ensureTaskContext, maybeCheckpoint, maybeSummarize, publishStatus, versionedContinueAsNew, wrapWithResumeContext, writeCommandResponse, writeLatestResponse, } from "./lifecycle.js";
|
|
6
6
|
import { describeCronAt } from "../cron-at.js";
|
|
7
7
|
import { shouldWakeParentForChildUpdate } from "../child-notifications.js";
|
|
8
8
|
import { INTERNAL_SYSTEM_TURN_PROMPT, MAX_RETRIES, SHUTDOWN_POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, } from "./state.js";
|
|
9
9
|
import { AUTH_FAILURE_USER_HINT, COPILOT_CONNECTION_CLOSED_MAX_RETRIES, COPILOT_CONNECTION_CLOSED_RETRY_DELAY_SECONDS, appendSystemContext, buildConnectionClosedRetryDetail, buildLossyHandoffRehydrationMessage, buildLossyHandoffSummary, extractPromptSystemContext, isAuthFailureError, isCopilotConnectionClosedError, mergePrompt, updateContextUsageFromEvents, } from "./utils.js";
|
|
10
|
+
function currentModelLabel(runtime) {
|
|
11
|
+
const model = runtime.state.config.model || "(default)";
|
|
12
|
+
const effort = runtime.state.config.reasoningEffort;
|
|
13
|
+
return effort ? `${model}:${effort}` : model;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Scan a finished turn's captured events for a failed `set_session_model` tool
|
|
17
|
+
* call. The inline control tool returns its outcome as a plain string (see
|
|
18
|
+
* session-proxy `setSessionModel`), so `tool.execution_complete.data.result`
|
|
19
|
+
* is usually a string; some transports wrap it as `{ content }`. We match the
|
|
20
|
+
* `set_session_model failed` marker across both shapes. Exported for unit tests.
|
|
21
|
+
*/
|
|
22
|
+
export function detectFailedModelSwitch(events) {
|
|
23
|
+
if (!Array.isArray(events))
|
|
24
|
+
return null;
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
if (event?.eventType !== "tool.execution_complete")
|
|
27
|
+
continue;
|
|
28
|
+
const data = event.data || {};
|
|
29
|
+
const result = data.result ?? data.output;
|
|
30
|
+
const content = typeof result === "string"
|
|
31
|
+
? result
|
|
32
|
+
: String(result?.content ?? result?.detailedContent ?? data.content ?? "");
|
|
33
|
+
if (/set_session_model (?:failed|is unavailable|rejected)/i.test(content))
|
|
34
|
+
return content.trim();
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function captureFailedModelSwitchNotice(runtime, result) {
|
|
39
|
+
const failure = detectFailedModelSwitch(result?.events);
|
|
40
|
+
if (!failure)
|
|
41
|
+
return null;
|
|
42
|
+
const modelLabel = currentModelLabel(runtime);
|
|
43
|
+
runtime.state.runtimeModelNotice = `Previous model switch failed; current runtime model is ${modelLabel}. If asked what model you are using, answer this value.`;
|
|
44
|
+
runtime.ctx.traceInfo(`[orch] queued failed model-switch correction: ${failure.slice(0, 160)}`);
|
|
45
|
+
return `Continue on ${modelLabel}; the requested model switch failed.`;
|
|
46
|
+
}
|
|
10
47
|
function* handleConnectionClosedRetry(runtime, errorMessage, rc) {
|
|
11
48
|
const { state } = runtime;
|
|
12
49
|
if (state.retryCount <= COPILOT_CONNECTION_CLOSED_MAX_RETRIES) {
|
|
@@ -140,6 +177,10 @@ export function* processPrompt(runtime, promptText, isBootstrap, requiredTool, c
|
|
|
140
177
|
const extractedPrompt = extractPromptSystemContext(prompt);
|
|
141
178
|
prompt = extractedPrompt.prompt ?? "";
|
|
142
179
|
turnSystemPrompt = mergePrompt(turnSystemPrompt, extractedPrompt.systemPrompt);
|
|
180
|
+
if (prompt && state.runtimeModelNotice) {
|
|
181
|
+
turnSystemPrompt = mergePrompt(turnSystemPrompt, state.runtimeModelNotice);
|
|
182
|
+
state.runtimeModelNotice = undefined;
|
|
183
|
+
}
|
|
143
184
|
const systemOnlyTurn = !prompt && !!turnSystemPrompt;
|
|
144
185
|
if (systemOnlyTurn) {
|
|
145
186
|
prompt = INTERNAL_SYSTEM_TURN_PROMPT;
|
|
@@ -203,7 +244,15 @@ export function* processPrompt(runtime, promptText, isBootstrap, requiredTool, c
|
|
|
203
244
|
let turnResult;
|
|
204
245
|
try {
|
|
205
246
|
state.lastLiveSessionAction = "session-activity";
|
|
206
|
-
|
|
247
|
+
// Stop-turn race: the in-flight runTurn activity vs a dequeue on the
|
|
248
|
+
// TURN-SCOPED stop queue (stopTurn.<iteration>). Scoping the queue to
|
|
249
|
+
// the turn index makes stale stop events structurally unable to kill a
|
|
250
|
+
// later turn — a race loser is dropped and cannot be un-dropped.
|
|
251
|
+
// When the stop wins, duroxide cancel-requests the dropped runTurn
|
|
252
|
+
// work item (lock-steal → isCancelled poll → SDK abort) as the
|
|
253
|
+
// guaranteed backstop; handleTurnStopped layers the fast-path
|
|
254
|
+
// same-affinity abortTurn on top.
|
|
255
|
+
const turnTask = runtime.session.runTurn(prompt, promptIsBootstrap, state.iteration, {
|
|
207
256
|
...(runtime.options.parentSessionId ? { parentSessionId: runtime.options.parentSessionId } : {}),
|
|
208
257
|
nestingLevel: runtime.options.nestingLevel,
|
|
209
258
|
...(requiredTool ? { requiredTool } : {}),
|
|
@@ -211,6 +260,21 @@ export function* processPrompt(runtime, promptText, isBootstrap, requiredTool, c
|
|
|
211
260
|
retryCount: state.retryCount,
|
|
212
261
|
...(clientMessageIds && clientMessageIds.length > 0 ? { clientMessageIds } : {}),
|
|
213
262
|
});
|
|
263
|
+
const stopTask = ctx.dequeueEvent(stopTurnQueueName(state.iteration));
|
|
264
|
+
const race = yield ctx.race(turnTask, stopTask);
|
|
265
|
+
if (race.index === 1) {
|
|
266
|
+
yield* handleTurnStopped(runtime, race.value);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// The select bridge flattens activity failures into their raw error
|
|
270
|
+
// string (duroxide-node make_select_future) instead of throwing, so a
|
|
271
|
+
// failed runTurn must be re-thrown here to reach the existing retry
|
|
272
|
+
// machinery in the catch below.
|
|
273
|
+
const raced = normalizeRacedTurnValue(race.value);
|
|
274
|
+
if (raced.kind === "error") {
|
|
275
|
+
throw new Error(raced.message);
|
|
276
|
+
}
|
|
277
|
+
turnResult = raced.result;
|
|
214
278
|
}
|
|
215
279
|
catch (err) {
|
|
216
280
|
state.config.turnSystemPrompt = undefined;
|
|
@@ -253,9 +317,16 @@ export function* processPrompt(runtime, promptText, isBootstrap, requiredTool, c
|
|
|
253
317
|
}
|
|
254
318
|
state.config.turnSystemPrompt = undefined;
|
|
255
319
|
state.retryCount = 0;
|
|
256
|
-
|
|
320
|
+
let result = typeof turnResult === "string" ? JSON.parse(turnResult) : turnResult;
|
|
257
321
|
const observedAt = yield ctx.utcNow();
|
|
258
322
|
state.contextUsage = updateContextUsageFromEvents(state.contextUsage, result?.events, observedAt);
|
|
323
|
+
const failedModelSwitchContinuePrompt = captureFailedModelSwitchNotice(runtime, result);
|
|
324
|
+
if (failedModelSwitchContinuePrompt && result.type === "completed") {
|
|
325
|
+
result = {
|
|
326
|
+
...result,
|
|
327
|
+
forceContinuePrompt: failedModelSwitchContinuePrompt,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
259
330
|
state.iteration++;
|
|
260
331
|
yield* maybeSummarize(runtime);
|
|
261
332
|
yield* refreshTrackedSubAgents(runtime);
|
|
@@ -266,6 +337,276 @@ export function* processPrompt(runtime, promptText, isBootstrap, requiredTool, c
|
|
|
266
337
|
yield* drainLeadingQueuedScheduleActions(runtime, prompt);
|
|
267
338
|
yield* handleTurnResult(runtime, result, prompt, cycleOrigin);
|
|
268
339
|
}
|
|
340
|
+
// ─── Stop-turn race support ─────────────────────────────────
|
|
341
|
+
/**
|
|
342
|
+
* Normalize a raced runTurn branch value. The duroxide-node select bridge
|
|
343
|
+
* flattens activity failures into their raw error string (make_select_future:
|
|
344
|
+
* `Ok(v) => v, Err(e) => e`) instead of throwing into the generator, so the
|
|
345
|
+
* caller must distinguish a TurnResult payload from an error message.
|
|
346
|
+
*/
|
|
347
|
+
export function normalizeRacedTurnValue(value) {
|
|
348
|
+
let v = value;
|
|
349
|
+
if (typeof v === "string") {
|
|
350
|
+
try {
|
|
351
|
+
v = JSON.parse(v);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return { kind: "error", message: value };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (v && typeof v === "object" && typeof v.type === "string") {
|
|
358
|
+
return { kind: "result", result: v };
|
|
359
|
+
}
|
|
360
|
+
return { kind: "error", message: typeof value === "string" ? value : JSON.stringify(value ?? null) };
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Stop won the race against the in-flight runTurn activity.
|
|
364
|
+
*
|
|
365
|
+
* The dropped runTurn future is already cancel-requested by duroxide (the
|
|
366
|
+
* guaranteed backstop: lock-steal → isCancelled poll → SDK abort, ~2-7s).
|
|
367
|
+
* This path layers the fast-path interrupt on top and owns the authoritative
|
|
368
|
+
* durable bookkeeping — the aborted activity's own writeback is best-effort
|
|
369
|
+
* (it is skipped entirely when the backstop delivered the abort).
|
|
370
|
+
*/
|
|
371
|
+
function* handleTurnStopped(runtime, stopEventRaw) {
|
|
372
|
+
const { ctx, state } = runtime;
|
|
373
|
+
let stopEvent = stopEventRaw;
|
|
374
|
+
if (typeof stopEvent === "string") {
|
|
375
|
+
try {
|
|
376
|
+
stopEvent = JSON.parse(stopEvent);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
stopEvent = {};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (!stopEvent || typeof stopEvent !== "object")
|
|
383
|
+
stopEvent = {};
|
|
384
|
+
const reason = typeof stopEvent.reason === "string" && stopEvent.reason ? stopEvent.reason : "Stopped by user";
|
|
385
|
+
const stoppedIteration = state.iteration;
|
|
386
|
+
ctx.traceInfo(`[orch] stop_turn won the race for turn ${stoppedIteration}; aborting in-flight turn`);
|
|
387
|
+
state.config.turnSystemPrompt = undefined;
|
|
388
|
+
state.retryCount = 0;
|
|
389
|
+
// Fast-path interrupt: same-affinity abortTurn lands on the worker owning
|
|
390
|
+
// the warm session and aborts the SDK request immediately (concurrent
|
|
391
|
+
// dispatch requires stable workerNodeId + a free slot; otherwise the
|
|
392
|
+
// backstop still stops the turn, just slower). Awaiting it also
|
|
393
|
+
// guarantees the per-session run-turn lock is free again before this
|
|
394
|
+
// loop can dispatch the next prompt.
|
|
395
|
+
let abortOutcome = null;
|
|
396
|
+
try {
|
|
397
|
+
const raw = yield runtime.session.abortTurn(reason, stoppedIteration);
|
|
398
|
+
abortOutcome = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
ctx.traceInfo(`[orch] abortTurn activity failed (backstop cancellation still applies): ${err?.message ?? err}`);
|
|
402
|
+
abortOutcome = { outcome: "no_active_turn", detail: `abortTurn failed: ${err?.message ?? err}` };
|
|
403
|
+
}
|
|
404
|
+
// The race already decided the turn's fate: even when abortTurn reports
|
|
405
|
+
// no_active_turn (the backstop got there first, or the turn had just
|
|
406
|
+
// ended), the user's stop is the durable outcome. Record turn_stopped
|
|
407
|
+
// unconditionally and annotate how the interrupt was delivered.
|
|
408
|
+
yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [
|
|
409
|
+
{
|
|
410
|
+
eventType: "session.turn_stopped",
|
|
411
|
+
data: {
|
|
412
|
+
reason,
|
|
413
|
+
turnIndex: stoppedIteration,
|
|
414
|
+
interrupt: abortOutcome?.outcome ?? "unknown",
|
|
415
|
+
...(abortOutcome?.detail ? { detail: abortOutcome.detail } : {}),
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
{ eventType: "system.message", data: { content: "Turn stopped by user." } },
|
|
419
|
+
]);
|
|
420
|
+
// Authoritative CMS transition — also clears active_turn_index (migration
|
|
421
|
+
// 0024 clears it on any state transition away from "running").
|
|
422
|
+
yield runtime.manager.updateCmsState(runtime.input.sessionId, "idle");
|
|
423
|
+
// The turn ran and consumed context even though its result was discarded.
|
|
424
|
+
state.iteration++;
|
|
425
|
+
if (typeof stopEvent.id === "string" && stopEvent.id) {
|
|
426
|
+
yield* writeCommandResponse(runtime, {
|
|
427
|
+
id: stopEvent.id,
|
|
428
|
+
cmd: "stop_turn",
|
|
429
|
+
result: {
|
|
430
|
+
outcome: abortOutcome?.outcome === "stop_forced" ? "stop_forced" : "stopped",
|
|
431
|
+
turnIndex: stoppedIteration,
|
|
432
|
+
...(abortOutcome?.detail ? { detail: abortOutcome.detail } : {}),
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
// Same scheduling semantics as a completed turn: resume interrupted
|
|
437
|
+
// timers, re-arm cron schedules, else idle (skips: writeLatestResponse,
|
|
438
|
+
// parent CHILD_UPDATE notify, forgotten-timer nudge).
|
|
439
|
+
yield* schedulePostTurnContinuation(runtime);
|
|
440
|
+
}
|
|
441
|
+
// ─── Post-turn continuation: resume timers / re-arm schedules / go idle ───
|
|
442
|
+
//
|
|
443
|
+
// Extracted verbatim from the tail of the `completed` turn-result case so the
|
|
444
|
+
// stop-turn path shares identical scheduling semantics: stopping a turn must
|
|
445
|
+
// not silently kill a recurring session's cron loop or a resumable wait
|
|
446
|
+
// (stop-turn plan, edge E9).
|
|
447
|
+
function* schedulePostTurnContinuation(runtime) {
|
|
448
|
+
const { ctx, state, options } = runtime;
|
|
449
|
+
if (state.interruptedWaitTimer && state.interruptedWaitTimer.remainingSec > 0) {
|
|
450
|
+
const saved = state.interruptedWaitTimer;
|
|
451
|
+
state.interruptedWaitTimer = null;
|
|
452
|
+
ctx.traceInfo(`[orch] auto-resuming interrupted wait: ${saved.remainingSec}s (${saved.reason})`);
|
|
453
|
+
if (saved.shouldRehydrate) {
|
|
454
|
+
yield* dehydrateForNextTurn(runtime, "timer", saved.waitPlan?.resetAffinityOnDehydrate ?? true);
|
|
455
|
+
}
|
|
456
|
+
const resumeNow = yield ctx.utcNow();
|
|
457
|
+
publishStatus(runtime, "waiting", {
|
|
458
|
+
waitSeconds: saved.remainingSec,
|
|
459
|
+
waitReason: saved.reason,
|
|
460
|
+
waitStartedAt: resumeNow,
|
|
461
|
+
});
|
|
462
|
+
if (!saved.shouldRehydrate)
|
|
463
|
+
yield* maybeCheckpoint(runtime);
|
|
464
|
+
state.activeTimer = {
|
|
465
|
+
deadlineMs: resumeNow + saved.remainingSec * 1000,
|
|
466
|
+
originalDurationMs: saved.remainingSec * 1000,
|
|
467
|
+
reason: saved.reason,
|
|
468
|
+
type: "wait",
|
|
469
|
+
shouldRehydrate: saved.shouldRehydrate,
|
|
470
|
+
waitPlan: saved.waitPlan,
|
|
471
|
+
};
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (state.interruptedCronTimer && state.interruptedCronTimer.remainingMs > 0) {
|
|
475
|
+
const saved = state.interruptedCronTimer;
|
|
476
|
+
state.interruptedCronTimer = null;
|
|
477
|
+
const remainingMs = Math.max(0, saved.remainingMs);
|
|
478
|
+
const remainingSec = Math.max(1, Math.round(remainingMs / 1000));
|
|
479
|
+
ctx.traceInfo(`[orch] auto-resuming interrupted cron: ${remainingSec}s remain (${saved.reason})`);
|
|
480
|
+
const cronResumePlan = planWaitHandling({
|
|
481
|
+
blobEnabled: state.blobEnabled,
|
|
482
|
+
seconds: remainingSec,
|
|
483
|
+
dehydrateThreshold: options.dehydrateThreshold,
|
|
484
|
+
});
|
|
485
|
+
if (cronResumePlan.shouldDehydrate) {
|
|
486
|
+
yield* dehydrateForNextTurn(runtime, "cron", cronResumePlan.resetAffinityOnDehydrate);
|
|
487
|
+
}
|
|
488
|
+
const resumeNow = yield ctx.utcNow();
|
|
489
|
+
publishStatus(runtime, "waiting", {
|
|
490
|
+
waitSeconds: remainingSec,
|
|
491
|
+
waitReason: saved.reason,
|
|
492
|
+
waitStartedAt: resumeNow,
|
|
493
|
+
});
|
|
494
|
+
if (!cronResumePlan.shouldDehydrate)
|
|
495
|
+
yield* maybeCheckpoint(runtime);
|
|
496
|
+
state.activeTimer = {
|
|
497
|
+
deadlineMs: resumeNow + remainingMs,
|
|
498
|
+
originalDurationMs: remainingMs,
|
|
499
|
+
reason: saved.reason,
|
|
500
|
+
type: "cron",
|
|
501
|
+
shouldRehydrate: cronResumePlan.shouldDehydrate,
|
|
502
|
+
};
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (state.cronSchedule) {
|
|
506
|
+
const activeCron = { ...state.cronSchedule };
|
|
507
|
+
const cronPlan = planWaitHandling({
|
|
508
|
+
blobEnabled: state.blobEnabled,
|
|
509
|
+
seconds: activeCron.intervalSeconds,
|
|
510
|
+
dehydrateThreshold: options.dehydrateThreshold,
|
|
511
|
+
});
|
|
512
|
+
if (cronPlan.shouldDehydrate) {
|
|
513
|
+
yield* dehydrateForNextTurn(runtime, "cron", cronPlan.resetAffinityOnDehydrate);
|
|
514
|
+
}
|
|
515
|
+
yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
|
|
516
|
+
eventType: "session.cron_started",
|
|
517
|
+
data: { intervalSeconds: activeCron.intervalSeconds, reason: activeCron.reason },
|
|
518
|
+
}]);
|
|
519
|
+
const cronStartedAt = yield ctx.utcNow();
|
|
520
|
+
ctx.traceInfo(`[orch] cron timer: ${activeCron.intervalSeconds}s (${activeCron.reason})`);
|
|
521
|
+
publishStatus(runtime, "waiting", {
|
|
522
|
+
waitSeconds: activeCron.intervalSeconds,
|
|
523
|
+
waitReason: activeCron.reason,
|
|
524
|
+
waitStartedAt: cronStartedAt,
|
|
525
|
+
});
|
|
526
|
+
if (!cronPlan.shouldDehydrate)
|
|
527
|
+
yield* maybeCheckpoint(runtime);
|
|
528
|
+
state.activeTimer = {
|
|
529
|
+
deadlineMs: cronStartedAt + activeCron.intervalSeconds * 1000,
|
|
530
|
+
originalDurationMs: activeCron.intervalSeconds * 1000,
|
|
531
|
+
reason: activeCron.reason,
|
|
532
|
+
type: "cron",
|
|
533
|
+
shouldRehydrate: cronPlan.shouldDehydrate,
|
|
534
|
+
};
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (state.cronAtSchedule) {
|
|
538
|
+
const activeCronAt = { ...state.cronAtSchedule };
|
|
539
|
+
if (activeCronAt.maxFires !== undefined && activeCronAt.firesCompleted >= activeCronAt.maxFires) {
|
|
540
|
+
state.cronAtSchedule = undefined;
|
|
541
|
+
yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
|
|
542
|
+
eventType: "session.cron_at_completed",
|
|
543
|
+
data: { reason: activeCronAt.reason, firesCompleted: activeCronAt.firesCompleted, maxFires: activeCronAt.maxFires },
|
|
544
|
+
}]);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const nowMs = yield ctx.utcNow();
|
|
548
|
+
let nextFireAtMs = activeCronAt.nextFireAtMs;
|
|
549
|
+
let nextOccurrenceKey = activeCronAt.nextOccurrenceKey;
|
|
550
|
+
if (!nextFireAtMs || !nextOccurrenceKey) {
|
|
551
|
+
const nextFire = yield runtime.manager.computeCronAtNextFire(activeCronAt, nowMs, activeCronAt.lastOccurrenceKey);
|
|
552
|
+
nextFireAtMs = nextFire.nextFireAtMs;
|
|
553
|
+
nextOccurrenceKey = nextFire.occurrenceKey;
|
|
554
|
+
state.cronAtSchedule = {
|
|
555
|
+
...activeCronAt,
|
|
556
|
+
nextFireAtMs,
|
|
557
|
+
nextOccurrenceKey,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
if (nextFireAtMs === undefined || !nextOccurrenceKey) {
|
|
561
|
+
throw new Error("cron_at next-fire computation did not return a fire time");
|
|
562
|
+
}
|
|
563
|
+
const waitMs = Math.max(0, nextFireAtMs - nowMs);
|
|
564
|
+
const waitSeconds = Math.max(0, Math.ceil(waitMs / 1000));
|
|
565
|
+
const cronAtPlan = planWaitHandling({
|
|
566
|
+
blobEnabled: state.blobEnabled,
|
|
567
|
+
seconds: waitSeconds,
|
|
568
|
+
dehydrateThreshold: options.dehydrateThreshold,
|
|
569
|
+
});
|
|
570
|
+
if (cronAtPlan.shouldDehydrate) {
|
|
571
|
+
yield* dehydrateForNextTurn(runtime, "cron_at", cronAtPlan.resetAffinityOnDehydrate);
|
|
572
|
+
}
|
|
573
|
+
yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
|
|
574
|
+
eventType: "session.cron_at_started",
|
|
575
|
+
data: {
|
|
576
|
+
...state.cronAtSchedule,
|
|
577
|
+
nextFireAt: new Date(nextFireAtMs).toISOString(),
|
|
578
|
+
},
|
|
579
|
+
}]);
|
|
580
|
+
publishStatus(runtime, "waiting", {
|
|
581
|
+
waitSeconds,
|
|
582
|
+
waitReason: activeCronAt.reason,
|
|
583
|
+
waitStartedAt: nowMs,
|
|
584
|
+
});
|
|
585
|
+
if (!cronAtPlan.shouldDehydrate)
|
|
586
|
+
yield* maybeCheckpoint(runtime);
|
|
587
|
+
state.activeTimer = {
|
|
588
|
+
deadlineMs: nowMs + waitMs,
|
|
589
|
+
originalDurationMs: waitMs,
|
|
590
|
+
reason: activeCronAt.reason,
|
|
591
|
+
type: "cron_at",
|
|
592
|
+
shouldRehydrate: cronAtPlan.shouldDehydrate,
|
|
593
|
+
};
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (!state.blobEnabled || options.idleTimeout < 0) {
|
|
597
|
+
yield* maybeCheckpoint(runtime);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
publishStatus(runtime, "idle");
|
|
601
|
+
yield* maybeCheckpoint(runtime);
|
|
602
|
+
const idleNow = yield ctx.utcNow();
|
|
603
|
+
state.activeTimer = {
|
|
604
|
+
deadlineMs: idleNow + options.idleTimeout * 1000,
|
|
605
|
+
originalDurationMs: options.idleTimeout * 1000,
|
|
606
|
+
reason: "idle timeout",
|
|
607
|
+
type: "idle",
|
|
608
|
+
};
|
|
609
|
+
}
|
|
269
610
|
// ─── handleTurnResult: dispatch on TurnResult variant ───────
|
|
270
611
|
function coerceChildQuestionToWait(runtime, result) {
|
|
271
612
|
if (result.type === "completed"
|
|
@@ -315,6 +656,13 @@ export function* handleTurnResult(runtime, result, sourcePrompt, cycleOrigin) {
|
|
|
315
656
|
content: result.content,
|
|
316
657
|
model: result.model,
|
|
317
658
|
});
|
|
659
|
+
if (result.forceContinuePrompt) {
|
|
660
|
+
ctx.traceInfo(`[orch] continuing after terminal model switch failure`);
|
|
661
|
+
yield* versionedContinueAsNew(runtime, continueInputWithPrompt(runtime, result.forceContinuePrompt, {
|
|
662
|
+
bootstrapPrompt: true,
|
|
663
|
+
}));
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
318
666
|
if (options.parentSessionId) {
|
|
319
667
|
const cycleReport = result.cycleReport;
|
|
320
668
|
const cycleMaterial = cycleReport?.status === "material" || cycleReport?.status === "blocked"
|
|
@@ -372,166 +720,7 @@ export function* handleTurnResult(runtime, result, sourcePrompt, cycleOrigin) {
|
|
|
372
720
|
return;
|
|
373
721
|
}
|
|
374
722
|
}
|
|
375
|
-
|
|
376
|
-
const saved = state.interruptedWaitTimer;
|
|
377
|
-
state.interruptedWaitTimer = null;
|
|
378
|
-
ctx.traceInfo(`[orch] auto-resuming interrupted wait: ${saved.remainingSec}s (${saved.reason})`);
|
|
379
|
-
if (saved.shouldRehydrate) {
|
|
380
|
-
yield* dehydrateForNextTurn(runtime, "timer", saved.waitPlan?.resetAffinityOnDehydrate ?? true);
|
|
381
|
-
}
|
|
382
|
-
const resumeNow = yield ctx.utcNow();
|
|
383
|
-
publishStatus(runtime, "waiting", {
|
|
384
|
-
waitSeconds: saved.remainingSec,
|
|
385
|
-
waitReason: saved.reason,
|
|
386
|
-
waitStartedAt: resumeNow,
|
|
387
|
-
});
|
|
388
|
-
if (!saved.shouldRehydrate)
|
|
389
|
-
yield* maybeCheckpoint(runtime);
|
|
390
|
-
state.activeTimer = {
|
|
391
|
-
deadlineMs: resumeNow + saved.remainingSec * 1000,
|
|
392
|
-
originalDurationMs: saved.remainingSec * 1000,
|
|
393
|
-
reason: saved.reason,
|
|
394
|
-
type: "wait",
|
|
395
|
-
shouldRehydrate: saved.shouldRehydrate,
|
|
396
|
-
waitPlan: saved.waitPlan,
|
|
397
|
-
};
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (state.interruptedCronTimer && state.interruptedCronTimer.remainingMs > 0) {
|
|
401
|
-
const saved = state.interruptedCronTimer;
|
|
402
|
-
state.interruptedCronTimer = null;
|
|
403
|
-
const remainingMs = Math.max(0, saved.remainingMs);
|
|
404
|
-
const remainingSec = Math.max(1, Math.round(remainingMs / 1000));
|
|
405
|
-
ctx.traceInfo(`[orch] auto-resuming interrupted cron: ${remainingSec}s remain (${saved.reason})`);
|
|
406
|
-
const cronResumePlan = planWaitHandling({
|
|
407
|
-
blobEnabled: state.blobEnabled,
|
|
408
|
-
seconds: remainingSec,
|
|
409
|
-
dehydrateThreshold: options.dehydrateThreshold,
|
|
410
|
-
});
|
|
411
|
-
if (cronResumePlan.shouldDehydrate) {
|
|
412
|
-
yield* dehydrateForNextTurn(runtime, "cron", cronResumePlan.resetAffinityOnDehydrate);
|
|
413
|
-
}
|
|
414
|
-
const resumeNow = yield ctx.utcNow();
|
|
415
|
-
publishStatus(runtime, "waiting", {
|
|
416
|
-
waitSeconds: remainingSec,
|
|
417
|
-
waitReason: saved.reason,
|
|
418
|
-
waitStartedAt: resumeNow,
|
|
419
|
-
});
|
|
420
|
-
if (!cronResumePlan.shouldDehydrate)
|
|
421
|
-
yield* maybeCheckpoint(runtime);
|
|
422
|
-
state.activeTimer = {
|
|
423
|
-
deadlineMs: resumeNow + remainingMs,
|
|
424
|
-
originalDurationMs: remainingMs,
|
|
425
|
-
reason: saved.reason,
|
|
426
|
-
type: "cron",
|
|
427
|
-
shouldRehydrate: cronResumePlan.shouldDehydrate,
|
|
428
|
-
};
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
if (state.cronSchedule) {
|
|
432
|
-
const activeCron = { ...state.cronSchedule };
|
|
433
|
-
const cronPlan = planWaitHandling({
|
|
434
|
-
blobEnabled: state.blobEnabled,
|
|
435
|
-
seconds: activeCron.intervalSeconds,
|
|
436
|
-
dehydrateThreshold: options.dehydrateThreshold,
|
|
437
|
-
});
|
|
438
|
-
if (cronPlan.shouldDehydrate) {
|
|
439
|
-
yield* dehydrateForNextTurn(runtime, "cron", cronPlan.resetAffinityOnDehydrate);
|
|
440
|
-
}
|
|
441
|
-
yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
|
|
442
|
-
eventType: "session.cron_started",
|
|
443
|
-
data: { intervalSeconds: activeCron.intervalSeconds, reason: activeCron.reason },
|
|
444
|
-
}]);
|
|
445
|
-
const cronStartedAt = yield ctx.utcNow();
|
|
446
|
-
ctx.traceInfo(`[orch] cron timer: ${activeCron.intervalSeconds}s (${activeCron.reason})`);
|
|
447
|
-
publishStatus(runtime, "waiting", {
|
|
448
|
-
waitSeconds: activeCron.intervalSeconds,
|
|
449
|
-
waitReason: activeCron.reason,
|
|
450
|
-
waitStartedAt: cronStartedAt,
|
|
451
|
-
});
|
|
452
|
-
if (!cronPlan.shouldDehydrate)
|
|
453
|
-
yield* maybeCheckpoint(runtime);
|
|
454
|
-
state.activeTimer = {
|
|
455
|
-
deadlineMs: cronStartedAt + activeCron.intervalSeconds * 1000,
|
|
456
|
-
originalDurationMs: activeCron.intervalSeconds * 1000,
|
|
457
|
-
reason: activeCron.reason,
|
|
458
|
-
type: "cron",
|
|
459
|
-
shouldRehydrate: cronPlan.shouldDehydrate,
|
|
460
|
-
};
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
if (state.cronAtSchedule) {
|
|
464
|
-
const activeCronAt = { ...state.cronAtSchedule };
|
|
465
|
-
if (activeCronAt.maxFires !== undefined && activeCronAt.firesCompleted >= activeCronAt.maxFires) {
|
|
466
|
-
state.cronAtSchedule = undefined;
|
|
467
|
-
yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
|
|
468
|
-
eventType: "session.cron_at_completed",
|
|
469
|
-
data: { reason: activeCronAt.reason, firesCompleted: activeCronAt.firesCompleted, maxFires: activeCronAt.maxFires },
|
|
470
|
-
}]);
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
const nowMs = yield ctx.utcNow();
|
|
474
|
-
let nextFireAtMs = activeCronAt.nextFireAtMs;
|
|
475
|
-
let nextOccurrenceKey = activeCronAt.nextOccurrenceKey;
|
|
476
|
-
if (!nextFireAtMs || !nextOccurrenceKey) {
|
|
477
|
-
const nextFire = yield runtime.manager.computeCronAtNextFire(activeCronAt, nowMs, activeCronAt.lastOccurrenceKey);
|
|
478
|
-
nextFireAtMs = nextFire.nextFireAtMs;
|
|
479
|
-
nextOccurrenceKey = nextFire.occurrenceKey;
|
|
480
|
-
state.cronAtSchedule = {
|
|
481
|
-
...activeCronAt,
|
|
482
|
-
nextFireAtMs,
|
|
483
|
-
nextOccurrenceKey,
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
if (nextFireAtMs === undefined || !nextOccurrenceKey) {
|
|
487
|
-
throw new Error("cron_at next-fire computation did not return a fire time");
|
|
488
|
-
}
|
|
489
|
-
const waitMs = Math.max(0, nextFireAtMs - nowMs);
|
|
490
|
-
const waitSeconds = Math.max(0, Math.ceil(waitMs / 1000));
|
|
491
|
-
const cronAtPlan = planWaitHandling({
|
|
492
|
-
blobEnabled: state.blobEnabled,
|
|
493
|
-
seconds: waitSeconds,
|
|
494
|
-
dehydrateThreshold: options.dehydrateThreshold,
|
|
495
|
-
});
|
|
496
|
-
if (cronAtPlan.shouldDehydrate) {
|
|
497
|
-
yield* dehydrateForNextTurn(runtime, "cron_at", cronAtPlan.resetAffinityOnDehydrate);
|
|
498
|
-
}
|
|
499
|
-
yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
|
|
500
|
-
eventType: "session.cron_at_started",
|
|
501
|
-
data: {
|
|
502
|
-
...state.cronAtSchedule,
|
|
503
|
-
nextFireAt: new Date(nextFireAtMs).toISOString(),
|
|
504
|
-
},
|
|
505
|
-
}]);
|
|
506
|
-
publishStatus(runtime, "waiting", {
|
|
507
|
-
waitSeconds,
|
|
508
|
-
waitReason: activeCronAt.reason,
|
|
509
|
-
waitStartedAt: nowMs,
|
|
510
|
-
});
|
|
511
|
-
if (!cronAtPlan.shouldDehydrate)
|
|
512
|
-
yield* maybeCheckpoint(runtime);
|
|
513
|
-
state.activeTimer = {
|
|
514
|
-
deadlineMs: nowMs + waitMs,
|
|
515
|
-
originalDurationMs: waitMs,
|
|
516
|
-
reason: activeCronAt.reason,
|
|
517
|
-
type: "cron_at",
|
|
518
|
-
shouldRehydrate: cronAtPlan.shouldDehydrate,
|
|
519
|
-
};
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
if (!state.blobEnabled || options.idleTimeout < 0) {
|
|
523
|
-
yield* maybeCheckpoint(runtime);
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
publishStatus(runtime, "idle");
|
|
527
|
-
yield* maybeCheckpoint(runtime);
|
|
528
|
-
const idleNow = yield ctx.utcNow();
|
|
529
|
-
state.activeTimer = {
|
|
530
|
-
deadlineMs: idleNow + options.idleTimeout * 1000,
|
|
531
|
-
originalDurationMs: options.idleTimeout * 1000,
|
|
532
|
-
reason: "idle timeout",
|
|
533
|
-
type: "idle",
|
|
534
|
-
};
|
|
723
|
+
yield* schedulePostTurnContinuation(runtime);
|
|
535
724
|
return;
|
|
536
725
|
}
|
|
537
726
|
case "cron":
|
|
@@ -651,6 +840,26 @@ export function* handleTurnResult(runtime, result, sourcePrompt, cycleOrigin) {
|
|
|
651
840
|
case "cancelled":
|
|
652
841
|
ctx.traceInfo("[session] turn cancelled");
|
|
653
842
|
return;
|
|
843
|
+
case "stopped": {
|
|
844
|
+
// Defensive: a turn only classifies "stopped" when the stop marker
|
|
845
|
+
// was set, which normally means handleTurnStopped already ran via
|
|
846
|
+
// the race. Handle it anyway so a marker-set turn that somehow
|
|
847
|
+
// returns through the normal path still lands idle with the event
|
|
848
|
+
// trail (processPrompt already incremented state.iteration).
|
|
849
|
+
ctx.traceInfo("[session] turn reported stopped");
|
|
850
|
+
state.retryCount = 0;
|
|
851
|
+
yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
|
|
852
|
+
eventType: "session.turn_stopped",
|
|
853
|
+
data: {
|
|
854
|
+
reason: result.reason ?? "Stopped by user",
|
|
855
|
+
turnIndex: state.iteration - 1,
|
|
856
|
+
interrupt: "turn-result",
|
|
857
|
+
},
|
|
858
|
+
}]);
|
|
859
|
+
yield runtime.manager.updateCmsState(runtime.input.sessionId, "idle");
|
|
860
|
+
yield* schedulePostTurnContinuation(runtime);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
654
863
|
case "spawn_agent":
|
|
655
864
|
case "message_agent":
|
|
656
865
|
case "check_agents":
|
|
@@ -715,7 +924,7 @@ export function* processTimer(runtime, timerItem) {
|
|
|
715
924
|
data: {},
|
|
716
925
|
}]);
|
|
717
926
|
const activeCron = state.cronSchedule;
|
|
718
|
-
const cycleReportGuidance = "If this cycle finds material changes or blockers that should wake your parent, call report_cycle(status='material' or status='blocked', summary='...') before finishing. If nothing material changed,
|
|
927
|
+
const cycleReportGuidance = "If this cycle finds material changes or blockers that should wake your parent, call report_cycle(status='material' or status='blocked', summary='...') before finishing. If nothing material changed, do NOT call report_cycle at all — just end the turn silently. Do not emit report_cycle(status='quiet') on an uneventful cycle, and never write a tool call as text.";
|
|
719
928
|
const cronPrompt = `[SYSTEM: Scheduled cron wake-up for: "${activeCron.reason}". Resume your recurring task. ${cycleReportGuidance}]`;
|
|
720
929
|
if (timer.shouldRehydrate) {
|
|
721
930
|
yield* processPrompt(runtime, wrapWithResumeContext(runtime, "Resume your recurring task.", `Scheduled cron wake-up for: "${activeCron.reason}". ${cycleReportGuidance}`), true, undefined, undefined, "cron");
|
|
@@ -766,12 +975,14 @@ export function* processTimer(runtime, timerItem) {
|
|
|
766
975
|
`Schedule: ${description}. Scheduled fire: ${new Date(scheduledAtMs).toISOString()}. ` +
|
|
767
976
|
`Resume your recurring task now. ` +
|
|
768
977
|
`If this cycle finds material changes or blockers that should wake your parent, call report_cycle(status='material' or status='blocked', summary='...') before finishing. ` +
|
|
769
|
-
`If nothing material changed,
|
|
978
|
+
`If nothing material changed, do NOT call report_cycle at all — just end the turn silently. ` +
|
|
979
|
+
`Do not emit report_cycle(status='quiet') on an uneventful cycle, and never write a tool call as text.]`;
|
|
770
980
|
if (timer.shouldRehydrate) {
|
|
771
981
|
yield* processPrompt(runtime, wrapWithResumeContext(runtime, "Resume your recurring task.", `Scheduled wall-clock cron wake-up for "${activeCronAt.reason}". ` +
|
|
772
982
|
`Schedule: ${description}. Scheduled fire: ${new Date(scheduledAtMs).toISOString()}. ` +
|
|
773
983
|
`If this cycle finds material changes or blockers that should wake your parent, call report_cycle(status='material' or status='blocked', summary='...') before finishing. ` +
|
|
774
|
-
`If nothing material changed,
|
|
984
|
+
`If nothing material changed, do NOT call report_cycle at all — just end the turn silently. ` +
|
|
985
|
+
`Do not emit report_cycle(status='quiet') on an uneventful cycle, and never write a tool call as text.`), true, undefined, undefined, "cron_at");
|
|
775
986
|
}
|
|
776
987
|
else {
|
|
777
988
|
yield* processPrompt(runtime, cronAtPrompt, true, undefined, undefined, "cron_at");
|