@sema-agent/core 7.10.0 → 7.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/dist/agents/child-model-seat.d.ts +45 -18
  3. package/dist/agents/child-model-seat.js +12 -8
  4. package/dist/agents/subagent.js +6 -5
  5. package/dist/agents/teacher.js +2 -2
  6. package/dist/core/auto-mode-defaults.d.ts +19 -3
  7. package/dist/core/auto-mode-defaults.js +1 -0
  8. package/dist/core/auto-mode.d.ts +24 -20
  9. package/dist/core/auto-mode.js +12 -12
  10. package/dist/core/gate-fold.js +1 -0
  11. package/dist/core/gate-lanes.d.ts +6 -1
  12. package/dist/core/gate-lanes.js +45 -18
  13. package/dist/core/governance-codes.d.ts +1 -0
  14. package/dist/core/governance-codes.js +1 -0
  15. package/dist/core/hooks.d.ts +13 -0
  16. package/dist/core/permission-rule-model.d.ts +5 -3
  17. package/dist/core/permission-rule-model.js +7 -3
  18. package/dist/core/persisted-rule-arms.js +4 -3
  19. package/dist/core/read-only-shell-table.d.ts +87 -0
  20. package/dist/core/read-only-shell-table.js +485 -0
  21. package/dist/core/read-only-shell.d.ts +42 -0
  22. package/dist/core/read-only-shell.js +316 -0
  23. package/dist/core/roles.d.ts +3 -2
  24. package/dist/core/runner/contracts.d.ts +70 -0
  25. package/dist/core/runner/gate-exit.d.ts +5 -0
  26. package/dist/core/runner/prepare-caps-and-workflow.js +38 -12
  27. package/dist/core/runner/prepare-gate-stations.js +9 -0
  28. package/dist/core/runner/prepare-task.js +1 -1
  29. package/dist/core/runner/prepare-turn-wiring.js +1 -1
  30. package/dist/core/runner/runtask.js +34 -510
  31. package/dist/core/runner/stream-halt-verbs.d.ts +38 -0
  32. package/dist/core/runner/stream-halt-verbs.js +82 -0
  33. package/dist/core/runner/stream-lifecycle-verbs.d.ts +34 -0
  34. package/dist/core/runner/stream-lifecycle-verbs.js +126 -0
  35. package/dist/core/runner/stream-reap.d.ts +30 -0
  36. package/dist/core/runner/stream-reap.js +40 -0
  37. package/dist/core/runner/stream-settle-backstop.d.ts +38 -0
  38. package/dist/core/runner/stream-settle-backstop.js +113 -0
  39. package/dist/core/runner/stream-steer-verb.d.ts +30 -0
  40. package/dist/core/runner/stream-steer-verb.js +185 -0
  41. package/dist/core/shell-lexer.d.ts +18 -0
  42. package/dist/core/shell-lexer.js +17 -10
  43. package/dist/core/shell-wrapper-table.js +8 -5
  44. package/dist/core/tool-policy.d.ts +4 -1
  45. package/dist/core/tool-policy.js +1 -1
  46. package/dist/core/tools.d.ts +28 -7
  47. package/dist/core/tools.js +44 -4
  48. package/dist/core/trace.d.ts +15 -0
  49. package/dist/engine/harness/agent-harness.d.ts +3 -1
  50. package/dist/engine/harness/agent-harness.js +1 -1
  51. package/dist/engine/harness/types.d.ts +4 -2
  52. package/dist/index.d.ts +3 -1
  53. package/dist/index.js +2 -0
  54. package/dist/orchestration/run-workflow-tool.d.ts +5 -2
  55. package/dist/orchestration/run-workflow-tool.js +2 -1
  56. package/dist/orchestration/workflow-governance.d.ts +3 -2
  57. package/dist/orchestration/workflow-primitives.d.ts +4 -1
  58. package/dist/orchestration/workflow-primitives.js +1 -6
  59. package/dist/orchestration/workflow.d.ts +12 -4
  60. package/dist/orchestration/workflow.js +24 -7
  61. package/dist/prompt-assembly/turn-snapshot.d.ts +4 -2
  62. package/dist/tools/fs/bash-readonly-classifier.d.ts +5 -0
  63. package/dist/tools/fs/bash-readonly-classifier.js +1 -0
  64. package/dist/tools/fs/fs-bash.js +3 -2
  65. package/package.json +1 -1
  66. package/test/export-surface.snapshot.json +35 -1
@@ -0,0 +1,82 @@
1
+ import { deliverEngineNotice } from "../types.js";
2
+ export function streamHaltVerbs(input) {
3
+ const { spec, live, ready, orTimeout, readyTimeoutMs: READY_TIMEOUT_MS, run, reapSuspended, runner } = input;
4
+ let destroyOnce;
5
+ const halt = async () => {
6
+ const haltRefused = (msg) => {
7
+ const e = new Error(`cannot halt: ${msg}`);
8
+ e.code = "steering.not_running";
9
+ return e;
10
+ };
11
+ if (live.resultValue)
12
+ throw haltRefused("the task has already finished");
13
+ const h = live.handle ?? (await orTimeout(ready));
14
+ if (!h)
15
+ throw haltRefused("the task is not running");
16
+ const apply = () => {
17
+ const abortOwnedBeforeHalt = h.abortController.signal.aborted;
18
+ const receipt = h.harness.halt();
19
+ if (receipt.accepted && !abortOwnedBeforeHalt) {
20
+ h.loop.userHalted = true;
21
+ }
22
+ else {
23
+ deliverEngineNotice(runner.deps.onNotice, {
24
+ code: "task.halt_unconsumed",
25
+ message: "a user halt arrived while the run was already ending for its own reason: nothing was cut or stopped " +
26
+ "by it — the run's own ending stands, and the result will not carry haltedByUser for this halt.",
27
+ detail: {
28
+ sessionId: h.sessionId,
29
+ runId: h.runId,
30
+ ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
31
+ },
32
+ });
33
+ }
34
+ if (receipt.turnCut) {
35
+ deliverEngineNotice(runner.deps.onNotice, {
36
+ code: "task.turn_interrupted",
37
+ message: "a bare user halt cut the running turn: in-flight work was cut at a manufactured boundary " +
38
+ "(finished tool calls keep their real results; never-started ones settle as interrupted) and " +
39
+ "the run is collecting to a clean, resumable stop — no further model turn will start.",
40
+ detail: {
41
+ cause: "user_halt",
42
+ sessionId: h.sessionId,
43
+ runId: h.runId,
44
+ ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
45
+ },
46
+ });
47
+ }
48
+ return { turnCut: receipt.turnCut };
49
+ };
50
+ try {
51
+ if (live.resultValue !== undefined || h.loop.ended)
52
+ throw haltRefused("the task is no longer running");
53
+ return apply();
54
+ }
55
+ catch (e) {
56
+ if (!(e instanceof Error && e.code === "invalid_state"))
57
+ throw e;
58
+ }
59
+ const birthDeadline = Date.now() + READY_TIMEOUT_MS;
60
+ while (live.resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
61
+ try {
62
+ return apply();
63
+ }
64
+ catch (e2) {
65
+ if (!(e2 instanceof Error && e2.code === "invalid_state"))
66
+ throw e2;
67
+ }
68
+ await new Promise((r) => setTimeout(r, 10));
69
+ }
70
+ throw haltRefused("the task is no longer running");
71
+ };
72
+ const destroy = () => (destroyOnce ??= (async () => {
73
+ const h = live.handle ?? (await orTimeout(ready));
74
+ if (h) {
75
+ h.abortController.abort();
76
+ void h.harness.abort();
77
+ }
78
+ await orTimeout(run.catch(() => { }));
79
+ await reapSuspended();
80
+ })());
81
+ return { halt, destroy };
82
+ }
@@ -0,0 +1,34 @@
1
+ import type { ToolDetachHub } from "../tool-detach.js";
2
+ import type { TaskStream } from "../types.js";
3
+ import type { CaptureOptOutRef, LiveHandle, ManualCompactRef, NotifyRef, TaskStreamLiveSeat } from "./contracts.js";
4
+ export interface StreamLifecycleVerbsInput {
5
+ /** borrowed-readonly — the stream's live state: `resultValue` (the finished-task refusals) and `handle`, read at each call. */
6
+ live: TaskStreamLiveSeat;
7
+ /** borrowed-readonly — the readiness promise the four async verbs await before they touch the handle. */
8
+ ready: Promise<LiveHandle | undefined>;
9
+ /** borrowed-readonly — the driver's timeout race: a wedged prepare cannot hang a verb on `ready`. */
10
+ orTimeout: <T>(p: Promise<T>) => Promise<T | undefined>;
11
+ /** borrowed-readonly — the driver's typed steering refusal (`steering.not_running` by default); the opt-out and compact verbs share it. */
12
+ steeringError: (msg: string, code?: string) => Error;
13
+ /** borrowed-readonly — the notify bridge; `inject` is bound by the run body once the notification lane exists. */
14
+ notifyRef: NotifyRef;
15
+ /** borrowed-readonly — the capture opt-out bridge; `flip` is bound by the run body when a memory session mounted. */
16
+ captureOptOutRef: CaptureOptOutRef;
17
+ /** borrowed-mutable — the manual-compaction request seat: the compact verb arms it and parks its waiter here. */
18
+ manualCompactRef: ManualCompactRef;
19
+ /** borrowed-readonly — the run-local detach hub the detach verb requests through. */
20
+ detachHub: ToolDetachHub;
21
+ }
22
+ export interface StreamLifecycleVerbsResult {
23
+ /** The stream's `notify` verb. */
24
+ notify: TaskStream["notify"];
25
+ /** The stream's `optOutMemoryCapture` verb. */
26
+ optOutMemoryCapture: TaskStream["optOutMemoryCapture"];
27
+ /** The stream's `compact` verb. */
28
+ compact: TaskStream["compact"];
29
+ /** The stream's `detach` verb. */
30
+ detach: TaskStream["detach"];
31
+ /** The stream's `interrupt` verb. */
32
+ interrupt: TaskStream["interrupt"];
33
+ }
34
+ export declare function streamLifecycleVerbs(input: StreamLifecycleVerbsInput): StreamLifecycleVerbsResult;
@@ -0,0 +1,126 @@
1
+ import { isSystemInjectionPriority, SYSTEM_INJECTION_PRIORITIES } from "../task-notification.js";
2
+ export function streamLifecycleVerbs(input) {
3
+ const { live, ready, orTimeout, steeringError, notifyRef, captureOptOutRef, manualCompactRef, detachHub } = input;
4
+ const notify = async (input, opts) => {
5
+ const notifyError = (msg, code) => {
6
+ const e = new Error(`cannot notify: ${msg}`);
7
+ e.code = code;
8
+ return e;
9
+ };
10
+ if (typeof input?.task_id !== "string" || input.task_id.trim() === "") {
11
+ throw notifyError("input.task_id must be a non-empty string", "notify.invalid_payload");
12
+ }
13
+ const VALID_STATUSES = new Set(["completed", "failed", "killed", "cancelled", "event"]);
14
+ if (!VALID_STATUSES.has(input.status)) {
15
+ throw notifyError(`input.status must be one of ${[...VALID_STATUSES].join("/")}`, "notify.invalid_payload");
16
+ }
17
+ if (typeof input.summary !== "string" || input.summary.trim() === "") {
18
+ throw notifyError("input.summary must be a non-empty string", "notify.invalid_payload");
19
+ }
20
+ if (input.result !== undefined && typeof input.result !== "string") {
21
+ throw notifyError("input.result must be a string when present", "notify.invalid_payload");
22
+ }
23
+ if (input.seq !== undefined && (typeof input.seq !== "number" || !Number.isFinite(input.seq))) {
24
+ throw notifyError("input.seq must be a finite number when present", "notify.invalid_payload");
25
+ }
26
+ if (input.source !== undefined && typeof input.source !== "string") {
27
+ throw notifyError("input.source must be a string when present", "notify.invalid_payload");
28
+ }
29
+ const priorityIn = opts?.priority;
30
+ if (priorityIn !== undefined && !isSystemInjectionPriority(priorityIn)) {
31
+ throw notifyError(`opts.priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when present`, "notify.invalid_payload");
32
+ }
33
+ const priority = priorityIn;
34
+ if (priority === "now") {
35
+ throw notifyError('priority "now" (turn interrupt) is not a notification-lane power — it belongs to the steer face; use "next" for earliest-boundary delivery', "notify.invalid_priority");
36
+ }
37
+ const payload = {
38
+ task_id: input.task_id,
39
+ task_type: "external",
40
+ status: input.status,
41
+ summary: input.summary,
42
+ ...(input.result !== undefined ? { result: input.result } : {}),
43
+ ...(input.seq !== undefined ? { seq: input.seq } : {}),
44
+ ...(input.source !== undefined ? { source: input.source } : {}),
45
+ };
46
+ const h = live.handle ?? (await orTimeout(ready));
47
+ if (!h || notifyRef.inject === undefined) {
48
+ throw notifyError("the task is not running", "notify.not_running");
49
+ }
50
+ notifyRef.inject(payload, priority !== undefined ? { priority } : undefined);
51
+ };
52
+ const optOutMemoryCapture = async (options) => {
53
+ const reasonIn = options?.reason;
54
+ if (reasonIn !== undefined && typeof reasonIn !== "string") {
55
+ const e = new Error(`cannot opt out of memory capture: options.reason must be a string when present`);
56
+ e.code = "steering.invalid_content";
57
+ throw e;
58
+ }
59
+ const reason = reasonIn;
60
+ if (live.resultValue)
61
+ throw steeringError("the task has already finished");
62
+ const h = live.handle ?? (await orTimeout(ready));
63
+ if (!h)
64
+ throw steeringError("the task is not running");
65
+ if (captureOptOutRef.flip === undefined) {
66
+ const e = new Error(`cannot opt out of memory capture: this run mounted no memory session (no memory spec / no backend / prepare degraded memory-less) — there is nothing to opt out of`);
67
+ e.code = "memory.capture_optout_unavailable";
68
+ throw e;
69
+ }
70
+ return captureOptOutRef.flip(reason);
71
+ };
72
+ const compact = async (opts) => {
73
+ if (live.resultValue)
74
+ throw steeringError("the task has already finished");
75
+ if (opts?.signal?.aborted)
76
+ return "mooted";
77
+ const h = live.handle ?? (await orTimeout(ready));
78
+ if (!h)
79
+ throw steeringError("the task is not running");
80
+ if (opts?.signal?.aborted)
81
+ return "mooted";
82
+ if (manualCompactRef.closed)
83
+ return "mooted";
84
+ return new Promise((resolve) => {
85
+ const signal = opts?.signal;
86
+ const entry = {
87
+ resolve,
88
+ ...(signal !== undefined ? { signal } : {}),
89
+ ...(opts?.instructions !== undefined ? { instructions: opts.instructions } : {}),
90
+ };
91
+ if (signal !== undefined) {
92
+ const onAbort = () => {
93
+ const i = manualCompactRef.waiters.indexOf(entry);
94
+ if (i >= 0) {
95
+ manualCompactRef.waiters.splice(i, 1);
96
+ if (manualCompactRef.waiters.length === 0) {
97
+ manualCompactRef.requested = false;
98
+ }
99
+ manualCompactRef.emitMooted?.("cancelled");
100
+ entry.resolve("mooted");
101
+ }
102
+ };
103
+ entry.resolve = (outcome) => {
104
+ signal.removeEventListener("abort", onAbort);
105
+ resolve(outcome);
106
+ };
107
+ signal.addEventListener("abort", onAbort, { once: true });
108
+ }
109
+ manualCompactRef.requested = true;
110
+ manualCompactRef.waiters.push(entry);
111
+ });
112
+ };
113
+ const detach = (toolCallId) => {
114
+ detachHub.request(toolCallId);
115
+ };
116
+ const interrupt = async () => {
117
+ const h = live.handle ?? (await orTimeout(ready));
118
+ if (!h)
119
+ return;
120
+ if (!h.abortController.signal.aborted)
121
+ h.loop.userInterrupted = true;
122
+ h.abortController.abort();
123
+ void h.harness.abort();
124
+ };
125
+ return { notify, optOutMemoryCapture, compact, detach, interrupt };
126
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * design/393 S4 — the TaskStream façade's REAP lane (T4), verbatim from `Runner.runTaskStream`: `destroy()`'s
3
+ * once-memoized work for a run that SUSPENDED. Runs AFTER `run` has settled (the destroy verb awaits the run before
4
+ * calling it), fences any concurrent resume first (the single-shot `expire` CAS — only the winner destroys the
5
+ * paused env; a loser retires its reap record only when no same-Runner claimant is in flight), evicts the
6
+ * terminally expired token's parked-constraint entry, and destroys the paused env best-effort behind a
7
+ * swallow-guarded onError sink so the memoized `destroy()` promise keeps its "never throws" contract.
8
+ *
9
+ * The lane returns the same async function the one-file façade declared; the destroy verb still calls it in the
10
+ * same position (after its own `await orTimeout(run.catch(…))`), so every await inside it sits on the tick it did.
11
+ */
12
+ import type { CheckpointToken } from "../checkpoint-store.js";
13
+ import type { InheritedGate, RunnerDepsSeat, SuspendReap, TaskStreamLiveSeat } from "./contracts.js";
14
+ export interface StreamReapInput {
15
+ /** borrowed-readonly — the stream's live state; `reapHandle` is read ONCE at the call (the run has settled, so it is the terminal fact). */
16
+ live: TaskStreamLiveSeat;
17
+ /** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`, after the fence's await too). */
18
+ runner: RunnerDepsSeat;
19
+ /** borrowed-readonly — the Runner's same-Runner claim marks: a lost fence preserves the reap record only while one is in flight. */
20
+ locallyClaimedTokens: Set<CheckpointToken>;
21
+ /** borrowed-mutable — the Runner's parked-constraint registry: the expired token's entry is evicted. */
22
+ parentConstraintRegistry: Map<CheckpointToken, NonNullable<InheritedGate["parentConstraints"]>>;
23
+ /** borrowed-mutable — the Runner's suspended-env reap handles: the winner (and an unclaimed loser) retires this token's record. */
24
+ suspendedEnvReaps: Map<CheckpointToken, SuspendReap>;
25
+ }
26
+ export interface StreamReapResult {
27
+ /** The reap step the destroy verb runs after the run settled: fence, evict, destroy the paused env — never throws. */
28
+ reapSuspended: () => Promise<void>;
29
+ }
30
+ export declare function streamReap(input: StreamReapInput): StreamReapResult;
@@ -0,0 +1,40 @@
1
+ import { hasDestroy } from "../remote-env.js";
2
+ export function streamReap(input) {
3
+ const { live, runner, locallyClaimedTokens, parentConstraintRegistry, suspendedEnvReaps } = input;
4
+ const reapSuspended = async () => {
5
+ const rh = live.reapHandle;
6
+ if (!rh)
7
+ return;
8
+ const reportErr = (err) => {
9
+ try {
10
+ runner.deps.onError?.(err, { phase: "config", sessionId: rh.sessionId });
11
+ }
12
+ catch {
13
+ }
14
+ };
15
+ let won;
16
+ try {
17
+ won = await rh.store.expire(rh.token, rh.scope);
18
+ }
19
+ catch (err) {
20
+ reportErr(err);
21
+ return;
22
+ }
23
+ if (!won) {
24
+ if (!locallyClaimedTokens.has(rh.token))
25
+ suspendedEnvReaps.delete(rh.token);
26
+ return;
27
+ }
28
+ parentConstraintRegistry.delete(rh.token);
29
+ suspendedEnvReaps.delete(rh.token);
30
+ if (rh.env && hasDestroy(rh.env)) {
31
+ try {
32
+ await rh.env.destroy();
33
+ }
34
+ catch (err) {
35
+ reportErr(err);
36
+ }
37
+ }
38
+ };
39
+ return { reapSuspended };
40
+ }
@@ -0,0 +1,38 @@
1
+ import type { CheckpointToken } from "../checkpoint-store.js";
2
+ import type { PushQueue } from "../push-queue.js";
3
+ import type { SessionStore } from "../session.js";
4
+ import { type TaskEvent, type TaskSpec } from "../types.js";
5
+ import type { InheritedGate, ResumeRun, RunnerDepsSeat, SuspendReap, TaskIdRef, TaskStreamLiveSeat } from "./contracts.js";
6
+ export interface StreamSettleBackstopInput {
7
+ /** borrowed-readonly — the task spec (read for the subagent-forwarding predicate the terminal push drains under). */
8
+ spec: TaskSpec;
9
+ /** borrowed-readonly — the resume plan when this leg is a resume: its token, the reopen compensation, the delivered-decision facts. */
10
+ resume: ResumeRun | undefined;
11
+ /** borrowed-mutable — the stream's event queue: the backstop pushes the `done` frame and closes it. */
12
+ queue: PushQueue<TaskEvent>;
13
+ /** borrowed-mutable — the run body's backstop carrier (effective ids + post-prepare observations); the owed delegation terminal is cleared here once delivered. */
14
+ taskIdRef: TaskIdRef;
15
+ /** borrowed-readonly — the invocation's frozen tracer (one sink for the start↔end pair). */
16
+ entryTracer: {
17
+ tracer: TaskSpec["tracer"];
18
+ };
19
+ /** borrowed-readonly — the stream's construction instant, for the backstop `task.end`'s durationMs. */
20
+ runStartedAt: number;
21
+ /** borrowed-mutable — the stream's live state: `resultValue` is read (an already-minted terminal is kept) and written (the failed terminal is minted onto it). */
22
+ live: TaskStreamLiveSeat;
23
+ /** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`, `onDelegationLifecycle`, after awaits too). */
24
+ runner: RunnerDepsSeat;
25
+ /** borrowed-readonly — the Runner's session store (the terminal resume-failure arm's `unpin`). */
26
+ sessions: SessionStore;
27
+ /** borrowed-mutable — the Runner's same-Runner claim marks: every failure leg releases the resume token's claim. */
28
+ locallyClaimedTokens: Set<CheckpointToken>;
29
+ /** borrowed-mutable — the Runner's parked-constraint registry: the reopen-failure and terminal-failure arms evict the consumed resume token's entry. */
30
+ parentConstraintRegistry: Map<CheckpointToken, NonNullable<InheritedGate["parentConstraints"]>>;
31
+ /** borrowed-mutable — the Runner's suspended-env reap handles: the terminal resume-failure arm takes and destroys this token's. */
32
+ suspendedEnvReaps: Map<CheckpointToken, SuspendReap>;
33
+ }
34
+ export interface StreamSettleBackstopResult {
35
+ /** The rejection handler for `run.catch(…)` — the driver installs it; awaiting `run.catch(onRunRejected)` is awaiting the settled result. */
36
+ onRunRejected: (err: unknown) => Promise<void>;
37
+ }
38
+ export declare function streamSettleBackstop(input: StreamSettleBackstopInput): StreamSettleBackstopResult;
@@ -0,0 +1,113 @@
1
+ import { errorCodeOf } from "./assemble-result.js";
2
+ import { resumeDecisionWasNegative } from "./decide-continuation.js";
3
+ import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
4
+ import { hasDestroy } from "../remote-env.js";
5
+ import { createSafeNotifier } from "../safe-notify.js";
6
+ import { settleTeardownLeg } from "./teardown-bounded.js";
7
+ import { emitTrace } from "../trace.js";
8
+ import { deliverDelegationLifecycle } from "../types.js";
9
+ export function streamSettleBackstop(input) {
10
+ const { spec, resume, queue, taskIdRef, entryTracer, runStartedAt, live, runner, sessions, locallyClaimedTokens, parentConstraintRegistry, suspendedEnvReaps } = input;
11
+ const onRunRejected = async (err) => {
12
+ let code = errorCodeOf(err);
13
+ const reopenReason = code === "resume.tool_unavailable" || code === "resume.tool_contract_mismatch" ? "tool_unavailable" : "env_failed";
14
+ const reopenable = resume !== undefined && resume.pendingActionStarted !== true && reopenReason !== undefined && !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true);
15
+ let reopenCommitted = false;
16
+ let reopenStateUnknown = false;
17
+ if (reopenable && resume?.onEnvRestoreFailed) {
18
+ try {
19
+ await resume.onEnvRestoreFailed(reopenReason);
20
+ reopenCommitted = true;
21
+ }
22
+ catch (reopenErr) {
23
+ parentConstraintRegistry.delete(resume.cp.token);
24
+ const original = err instanceof Error ? err : new Error(String(err));
25
+ const reopenMsg = reopenErr instanceof Error ? reopenErr.message : String(reopenErr);
26
+ const definitive = errorCodeOf(reopenErr) === "checkpoint.reopen_failed";
27
+ reopenStateUnknown = !definitive;
28
+ err = Object.assign(new Error(definitive
29
+ ? `${reopenMsg} (original resume failure: ${original.message})`
30
+ : `checkpoint reopen threw mid-flight — state UNKNOWN, confirm via the checkpoint store before retrying: ${reopenMsg} (original resume failure: ${original.message})`, { cause: original }), { code: "checkpoint.reopen_failed" });
31
+ code = "checkpoint.reopen_failed";
32
+ }
33
+ }
34
+ if (resume)
35
+ locallyClaimedTokens.delete(resume.cp.token);
36
+ if (resume && !reopenCommitted) {
37
+ parentConstraintRegistry.delete(resume.cp.token);
38
+ await settleTeardownLeg(() => sessions.unpin?.(resume.cp.sessionId), "sessions.unpin (resume-failure leg)", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: resume.cp.sessionId }));
39
+ if (!reopenStateUnknown) {
40
+ const failedReap = suspendedEnvReaps.get(resume.cp.token);
41
+ suspendedEnvReaps.delete(resume.cp.token);
42
+ if (failedReap?.env !== undefined && hasDestroy(failedReap.env)) {
43
+ const failedEnv = failedReap.env;
44
+ await settleTeardownLeg(() => failedEnv.destroy(), "terminal resume-failure env destroy", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: resume.cp.sessionId }));
45
+ }
46
+ }
47
+ }
48
+ if (live.resultValue !== undefined) {
49
+ try {
50
+ runner.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId: live.resultValue.sessionId });
51
+ }
52
+ catch {
53
+ }
54
+ queue.close();
55
+ return;
56
+ }
57
+ const remoteEnvFailure = (() => {
58
+ let cur = err;
59
+ for (let depth = 0; cur && typeof cur === "object" && depth < 8; depth++) {
60
+ const note = cur.remoteEnvFailure;
61
+ if (note !== undefined)
62
+ return [note];
63
+ cur = cur.cause;
64
+ }
65
+ return undefined;
66
+ })();
67
+ live.resultValue = {
68
+ taskId: taskIdRef.current ?? "unknown",
69
+ sessionId: taskIdRef.sessionId ?? "unknown",
70
+ ...(taskIdRef.runId !== undefined ? { runId: taskIdRef.runId } : {}),
71
+ terminal: { kind: "failed", ...(code !== undefined ? { code } : {}), message: err instanceof Error ? err.message : String(err) },
72
+ result: "",
73
+ ...(remoteEnvFailure !== undefined ? { remoteEnvFailures: remoteEnvFailure } : {}),
74
+ ...(taskIdRef.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: taskIdRef.effectiveMemoryScopes } : {}),
75
+ ...(taskIdRef.effectiveReasoning !== undefined ? { effectiveReasoning: taskIdRef.effectiveReasoning } : {}),
76
+ ...(() => {
77
+ const observed = taskIdRef.editedFiles?.();
78
+ return observed !== undefined && observed.length > 0 ? { editedFiles: observed } : {};
79
+ })(),
80
+ ...(() => {
81
+ const hinted = err.retryAfterMs;
82
+ return code === "memory.admission_required" && typeof hinted === "number" && Number.isFinite(hinted) && hinted > 0
83
+ ? { retryAfterMs: hinted }
84
+ : {};
85
+ })(),
86
+ stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0 },
87
+ };
88
+ emitTrace(entryTracer.tracer, () => ({
89
+ kind: "task.end",
90
+ version: 1,
91
+ taskId: taskIdRef.current ?? "unknown",
92
+ ...(taskIdRef.runId !== undefined ? { runId: taskIdRef.runId } : {}),
93
+ status: "failed",
94
+ errorCode: code,
95
+ turns: 0,
96
+ tokens: 0,
97
+ durationMs: Date.now() - runStartedAt,
98
+ ts: Date.now(),
99
+ }));
100
+ const owedTerminal = taskIdRef.delegationTerminalOwed;
101
+ if (owedTerminal !== undefined) {
102
+ taskIdRef.delegationTerminalOwed = undefined;
103
+ deliverDelegationLifecycle(runner.deps.onDelegationLifecycle, { phase: "terminal", identity: owedTerminal, status: "failed", turns: 0, ...(code !== undefined ? { errorCode: code } : {}) }, createSafeNotifier({
104
+ onError: (f) => console.warn(`[sema-core] ${f.site}: delegation-lifecycle observer threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
105
+ }), "runtask.onDelegationLifecycle");
106
+ }
107
+ if (forwardsSubagentEvents(spec))
108
+ await drainForwardedFramesBeforeDone();
109
+ queue.push({ type: "done", result: live.resultValue });
110
+ queue.close();
111
+ };
112
+ return { onRunRejected };
113
+ }
@@ -0,0 +1,30 @@
1
+ import type { PushQueue } from "../push-queue.js";
2
+ import { type TaskEvent, type TaskSpec, type TaskStream } from "../types.js";
3
+ import type { AcceptedSteerInput, LiveHandle, RunInternals, RunnerDepsSeat, TaskStreamLiveSeat } from "./contracts.js";
4
+ export interface StreamSteerVerbInput {
5
+ /** borrowed-readonly — the task spec (the hooks seat, `principal`, `taskId`). */
6
+ spec: TaskSpec;
7
+ /** borrowed-readonly — the trusted run-internals channel (the spawning tool id the ledger frame carries). */
8
+ internals: RunInternals | undefined;
9
+ /** borrowed-mutable — the stream's event queue: the `human_input` receipt is pushed here on acceptance. */
10
+ queue: PushQueue<TaskEvent>;
11
+ /** borrowed-mutable — this stream's accepted-input dedup domain (the SAME Map the driver clears when the run settles). */
12
+ acceptedSteerInputs: Map<string, AcceptedSteerInput>;
13
+ /** borrowed-readonly — the stream's live state: `resultValue` and `handle`, read at each liveness test. */
14
+ live: TaskStreamLiveSeat;
15
+ /** borrowed-readonly — the readiness promise (settles to the live handle, or `undefined` when the run ended first). */
16
+ ready: Promise<LiveHandle | undefined>;
17
+ /** borrowed-readonly — the driver's bounded race over a promise (the wedged-prepare guard). */
18
+ orTimeout: <T>(p: Promise<T>) => Promise<T | undefined>;
19
+ /** borrowed-readonly — the readiness bound in ms; the birth-window poll's deadline reads it too. */
20
+ readyTimeoutMs: number;
21
+ /** borrowed-readonly — the driver's typed steering refusal (`steering.not_running` by default). */
22
+ steeringError: (msg: string, code?: string) => Error;
23
+ /** borrowed-readonly — the Runner's deployment deps, read LIVE (`hooks`, `onError`, `onNotice`, after awaits too). */
24
+ runner: RunnerDepsSeat;
25
+ }
26
+ export interface StreamSteerVerbResult {
27
+ /** The stream's `steer` verb. */
28
+ steer: TaskStream["steer"];
29
+ }
30
+ export declare function streamSteerVerb(input: StreamSteerVerbInput): StreamSteerVerbResult;