pilotswarm-sdk 0.3.2 → 0.4.0
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/README.md +21 -4
- package/api/README.md +44 -0
- package/api/index.d.ts +86 -0
- package/api/index.js +16 -0
- package/api/src/api-client.js +363 -0
- package/api/src/http-api-transport.js +342 -0
- package/api/src/protocol.js +230 -0
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +9 -0
- package/dist/client.js.map +1 -1
- package/dist/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +250 -0
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +10 -0
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +5 -0
- package/dist/cms.js.map +1 -1
- package/dist/index.d.ts +25 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -7
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +35 -0
- package/dist/managed-session.d.ts.map +1 -1
- package/dist/managed-session.js +74 -0
- package/dist/managed-session.js.map +1 -1
- package/dist/management-client.d.ts +88 -3
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +259 -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/runtime.d.ts +1 -1
- package/dist/orchestration/turn.d.ts +13 -0
- package/dist/orchestration/turn.d.ts.map +1 -1
- package/dist/orchestration/turn.js +317 -163
- package/dist/orchestration/turn.js.map +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_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 +20 -1
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +62 -0
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +7 -0
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +35 -1
- package/dist/session-proxy.js.map +1 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +13 -3
- package/dist/session-store.js.map +1 -1
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/dist/web/api-connection.d.ts +20 -0
- package/dist/web/api-connection.d.ts.map +1 -0
- package/dist/web/api-connection.js +23 -0
- package/dist/web/api-connection.js.map +1 -0
- package/dist/web/web-client.d.ts +91 -0
- package/dist/web/web-client.d.ts.map +1 -0
- package/dist/web/web-client.js +378 -0
- package/dist/web/web-client.js.map +1 -0
- package/dist/web/web-fact-store.d.ts +78 -0
- package/dist/web/web-fact-store.d.ts.map +1 -0
- package/dist/web/web-fact-store.js +112 -0
- package/dist/web/web-fact-store.js.map +1 -0
- package/dist/web/web-graph-store.d.ts +38 -0
- package/dist/web/web-graph-store.d.ts.map +1 -0
- package/dist/web/web-graph-store.js +68 -0
- package/dist/web/web-graph-store.js.map +1 -0
- package/dist/web/web-management-client.d.ts +125 -0
- package/dist/web/web-management-client.d.ts.map +1 -0
- package/dist/web/web-management-client.js +256 -0
- package/dist/web/web-management-client.js.map +1 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +10 -0
- package/dist/worker.js.map +1 -1
- package/package.json +16 -6
|
@@ -1,8 +1,8 @@
|
|
|
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";
|
|
@@ -244,7 +244,15 @@ export function* processPrompt(runtime, promptText, isBootstrap, requiredTool, c
|
|
|
244
244
|
let turnResult;
|
|
245
245
|
try {
|
|
246
246
|
state.lastLiveSessionAction = "session-activity";
|
|
247
|
-
|
|
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, {
|
|
248
256
|
...(runtime.options.parentSessionId ? { parentSessionId: runtime.options.parentSessionId } : {}),
|
|
249
257
|
nestingLevel: runtime.options.nestingLevel,
|
|
250
258
|
...(requiredTool ? { requiredTool } : {}),
|
|
@@ -252,6 +260,21 @@ export function* processPrompt(runtime, promptText, isBootstrap, requiredTool, c
|
|
|
252
260
|
retryCount: state.retryCount,
|
|
253
261
|
...(clientMessageIds && clientMessageIds.length > 0 ? { clientMessageIds } : {}),
|
|
254
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;
|
|
255
278
|
}
|
|
256
279
|
catch (err) {
|
|
257
280
|
state.config.turnSystemPrompt = undefined;
|
|
@@ -314,6 +337,276 @@ export function* processPrompt(runtime, promptText, isBootstrap, requiredTool, c
|
|
|
314
337
|
yield* drainLeadingQueuedScheduleActions(runtime, prompt);
|
|
315
338
|
yield* handleTurnResult(runtime, result, prompt, cycleOrigin);
|
|
316
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
|
+
}
|
|
317
610
|
// ─── handleTurnResult: dispatch on TurnResult variant ───────
|
|
318
611
|
function coerceChildQuestionToWait(runtime, result) {
|
|
319
612
|
if (result.type === "completed"
|
|
@@ -427,166 +720,7 @@ export function* handleTurnResult(runtime, result, sourcePrompt, cycleOrigin) {
|
|
|
427
720
|
return;
|
|
428
721
|
}
|
|
429
722
|
}
|
|
430
|
-
|
|
431
|
-
const saved = state.interruptedWaitTimer;
|
|
432
|
-
state.interruptedWaitTimer = null;
|
|
433
|
-
ctx.traceInfo(`[orch] auto-resuming interrupted wait: ${saved.remainingSec}s (${saved.reason})`);
|
|
434
|
-
if (saved.shouldRehydrate) {
|
|
435
|
-
yield* dehydrateForNextTurn(runtime, "timer", saved.waitPlan?.resetAffinityOnDehydrate ?? true);
|
|
436
|
-
}
|
|
437
|
-
const resumeNow = yield ctx.utcNow();
|
|
438
|
-
publishStatus(runtime, "waiting", {
|
|
439
|
-
waitSeconds: saved.remainingSec,
|
|
440
|
-
waitReason: saved.reason,
|
|
441
|
-
waitStartedAt: resumeNow,
|
|
442
|
-
});
|
|
443
|
-
if (!saved.shouldRehydrate)
|
|
444
|
-
yield* maybeCheckpoint(runtime);
|
|
445
|
-
state.activeTimer = {
|
|
446
|
-
deadlineMs: resumeNow + saved.remainingSec * 1000,
|
|
447
|
-
originalDurationMs: saved.remainingSec * 1000,
|
|
448
|
-
reason: saved.reason,
|
|
449
|
-
type: "wait",
|
|
450
|
-
shouldRehydrate: saved.shouldRehydrate,
|
|
451
|
-
waitPlan: saved.waitPlan,
|
|
452
|
-
};
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
if (state.interruptedCronTimer && state.interruptedCronTimer.remainingMs > 0) {
|
|
456
|
-
const saved = state.interruptedCronTimer;
|
|
457
|
-
state.interruptedCronTimer = null;
|
|
458
|
-
const remainingMs = Math.max(0, saved.remainingMs);
|
|
459
|
-
const remainingSec = Math.max(1, Math.round(remainingMs / 1000));
|
|
460
|
-
ctx.traceInfo(`[orch] auto-resuming interrupted cron: ${remainingSec}s remain (${saved.reason})`);
|
|
461
|
-
const cronResumePlan = planWaitHandling({
|
|
462
|
-
blobEnabled: state.blobEnabled,
|
|
463
|
-
seconds: remainingSec,
|
|
464
|
-
dehydrateThreshold: options.dehydrateThreshold,
|
|
465
|
-
});
|
|
466
|
-
if (cronResumePlan.shouldDehydrate) {
|
|
467
|
-
yield* dehydrateForNextTurn(runtime, "cron", cronResumePlan.resetAffinityOnDehydrate);
|
|
468
|
-
}
|
|
469
|
-
const resumeNow = yield ctx.utcNow();
|
|
470
|
-
publishStatus(runtime, "waiting", {
|
|
471
|
-
waitSeconds: remainingSec,
|
|
472
|
-
waitReason: saved.reason,
|
|
473
|
-
waitStartedAt: resumeNow,
|
|
474
|
-
});
|
|
475
|
-
if (!cronResumePlan.shouldDehydrate)
|
|
476
|
-
yield* maybeCheckpoint(runtime);
|
|
477
|
-
state.activeTimer = {
|
|
478
|
-
deadlineMs: resumeNow + remainingMs,
|
|
479
|
-
originalDurationMs: remainingMs,
|
|
480
|
-
reason: saved.reason,
|
|
481
|
-
type: "cron",
|
|
482
|
-
shouldRehydrate: cronResumePlan.shouldDehydrate,
|
|
483
|
-
};
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
if (state.cronSchedule) {
|
|
487
|
-
const activeCron = { ...state.cronSchedule };
|
|
488
|
-
const cronPlan = planWaitHandling({
|
|
489
|
-
blobEnabled: state.blobEnabled,
|
|
490
|
-
seconds: activeCron.intervalSeconds,
|
|
491
|
-
dehydrateThreshold: options.dehydrateThreshold,
|
|
492
|
-
});
|
|
493
|
-
if (cronPlan.shouldDehydrate) {
|
|
494
|
-
yield* dehydrateForNextTurn(runtime, "cron", cronPlan.resetAffinityOnDehydrate);
|
|
495
|
-
}
|
|
496
|
-
yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
|
|
497
|
-
eventType: "session.cron_started",
|
|
498
|
-
data: { intervalSeconds: activeCron.intervalSeconds, reason: activeCron.reason },
|
|
499
|
-
}]);
|
|
500
|
-
const cronStartedAt = yield ctx.utcNow();
|
|
501
|
-
ctx.traceInfo(`[orch] cron timer: ${activeCron.intervalSeconds}s (${activeCron.reason})`);
|
|
502
|
-
publishStatus(runtime, "waiting", {
|
|
503
|
-
waitSeconds: activeCron.intervalSeconds,
|
|
504
|
-
waitReason: activeCron.reason,
|
|
505
|
-
waitStartedAt: cronStartedAt,
|
|
506
|
-
});
|
|
507
|
-
if (!cronPlan.shouldDehydrate)
|
|
508
|
-
yield* maybeCheckpoint(runtime);
|
|
509
|
-
state.activeTimer = {
|
|
510
|
-
deadlineMs: cronStartedAt + activeCron.intervalSeconds * 1000,
|
|
511
|
-
originalDurationMs: activeCron.intervalSeconds * 1000,
|
|
512
|
-
reason: activeCron.reason,
|
|
513
|
-
type: "cron",
|
|
514
|
-
shouldRehydrate: cronPlan.shouldDehydrate,
|
|
515
|
-
};
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
if (state.cronAtSchedule) {
|
|
519
|
-
const activeCronAt = { ...state.cronAtSchedule };
|
|
520
|
-
if (activeCronAt.maxFires !== undefined && activeCronAt.firesCompleted >= activeCronAt.maxFires) {
|
|
521
|
-
state.cronAtSchedule = undefined;
|
|
522
|
-
yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
|
|
523
|
-
eventType: "session.cron_at_completed",
|
|
524
|
-
data: { reason: activeCronAt.reason, firesCompleted: activeCronAt.firesCompleted, maxFires: activeCronAt.maxFires },
|
|
525
|
-
}]);
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
const nowMs = yield ctx.utcNow();
|
|
529
|
-
let nextFireAtMs = activeCronAt.nextFireAtMs;
|
|
530
|
-
let nextOccurrenceKey = activeCronAt.nextOccurrenceKey;
|
|
531
|
-
if (!nextFireAtMs || !nextOccurrenceKey) {
|
|
532
|
-
const nextFire = yield runtime.manager.computeCronAtNextFire(activeCronAt, nowMs, activeCronAt.lastOccurrenceKey);
|
|
533
|
-
nextFireAtMs = nextFire.nextFireAtMs;
|
|
534
|
-
nextOccurrenceKey = nextFire.occurrenceKey;
|
|
535
|
-
state.cronAtSchedule = {
|
|
536
|
-
...activeCronAt,
|
|
537
|
-
nextFireAtMs,
|
|
538
|
-
nextOccurrenceKey,
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
if (nextFireAtMs === undefined || !nextOccurrenceKey) {
|
|
542
|
-
throw new Error("cron_at next-fire computation did not return a fire time");
|
|
543
|
-
}
|
|
544
|
-
const waitMs = Math.max(0, nextFireAtMs - nowMs);
|
|
545
|
-
const waitSeconds = Math.max(0, Math.ceil(waitMs / 1000));
|
|
546
|
-
const cronAtPlan = planWaitHandling({
|
|
547
|
-
blobEnabled: state.blobEnabled,
|
|
548
|
-
seconds: waitSeconds,
|
|
549
|
-
dehydrateThreshold: options.dehydrateThreshold,
|
|
550
|
-
});
|
|
551
|
-
if (cronAtPlan.shouldDehydrate) {
|
|
552
|
-
yield* dehydrateForNextTurn(runtime, "cron_at", cronAtPlan.resetAffinityOnDehydrate);
|
|
553
|
-
}
|
|
554
|
-
yield runtime.manager.recordSessionEvent(runtime.input.sessionId, [{
|
|
555
|
-
eventType: "session.cron_at_started",
|
|
556
|
-
data: {
|
|
557
|
-
...state.cronAtSchedule,
|
|
558
|
-
nextFireAt: new Date(nextFireAtMs).toISOString(),
|
|
559
|
-
},
|
|
560
|
-
}]);
|
|
561
|
-
publishStatus(runtime, "waiting", {
|
|
562
|
-
waitSeconds,
|
|
563
|
-
waitReason: activeCronAt.reason,
|
|
564
|
-
waitStartedAt: nowMs,
|
|
565
|
-
});
|
|
566
|
-
if (!cronAtPlan.shouldDehydrate)
|
|
567
|
-
yield* maybeCheckpoint(runtime);
|
|
568
|
-
state.activeTimer = {
|
|
569
|
-
deadlineMs: nowMs + waitMs,
|
|
570
|
-
originalDurationMs: waitMs,
|
|
571
|
-
reason: activeCronAt.reason,
|
|
572
|
-
type: "cron_at",
|
|
573
|
-
shouldRehydrate: cronAtPlan.shouldDehydrate,
|
|
574
|
-
};
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
if (!state.blobEnabled || options.idleTimeout < 0) {
|
|
578
|
-
yield* maybeCheckpoint(runtime);
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
publishStatus(runtime, "idle");
|
|
582
|
-
yield* maybeCheckpoint(runtime);
|
|
583
|
-
const idleNow = yield ctx.utcNow();
|
|
584
|
-
state.activeTimer = {
|
|
585
|
-
deadlineMs: idleNow + options.idleTimeout * 1000,
|
|
586
|
-
originalDurationMs: options.idleTimeout * 1000,
|
|
587
|
-
reason: "idle timeout",
|
|
588
|
-
type: "idle",
|
|
589
|
-
};
|
|
723
|
+
yield* schedulePostTurnContinuation(runtime);
|
|
590
724
|
return;
|
|
591
725
|
}
|
|
592
726
|
case "cron":
|
|
@@ -706,6 +840,26 @@ export function* handleTurnResult(runtime, result, sourcePrompt, cycleOrigin) {
|
|
|
706
840
|
case "cancelled":
|
|
707
841
|
ctx.traceInfo("[session] turn cancelled");
|
|
708
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
|
+
}
|
|
709
863
|
case "spawn_agent":
|
|
710
864
|
case "message_agent":
|
|
711
865
|
case "check_agents":
|