@sema-agent/core 5.33.0 → 5.35.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.
@@ -0,0 +1,156 @@
1
+ /**
2
+ * design/252 G-6 — the LIVENESS half the wiring manifest deliberately does not have.
3
+ *
4
+ * The manifest's own §0 boundary says it: "this is STATIC ASSEMBLY SELF-DISCLOSURE — seam presence and
5
+ * shape, as facts. It does NOT prove liveness (a callback bound to the wrong run, a dead queue, an
6
+ * adapter's internal failure, an answer routed to the wrong instance). Liveness belongs to round-trip
7
+ * probes, which are a separate obligation." This module is that obligation for the PARK lane, and it
8
+ * quotes the boundary rather than dissolving it: a manifest reporting `parkLane.effective: true` over a
9
+ * store declaring `durability: "durable"` says a store is wired and what it CLAIMS. It cannot say the
10
+ * store accepts a row, returns the same row, and lets the fence win — which is the whole park contract,
11
+ * and the part a deployment gets wrong (a read-only mount, a serializer that drops a nested field, a
12
+ * scope column missing from the WHERE).
13
+ *
14
+ * TWO CHECKS, DIFFERENT KINDS, DELIBERATELY IN ONE MODULE:
15
+ * · {@link probeParkRoundTrip} — the live probe: mint a synthetic parked row, read it back, verify the
16
+ * load-bearing fields survived, fence it, confirm the fence landed.
17
+ * · {@link durableParkGapOf} — the STATIC two-halves reading: a durable park is a durable checkpoint
18
+ * store AND a durable session store, and a deployment with one of them is the configuration whose
19
+ * failure mode is a resume that reports a missing session instead of the seat that is missing. One
20
+ * module because they are one question asked at two costs, and a second home for "what does a whole
21
+ * durable park need" is how the two answers drift.
22
+ *
23
+ * WHAT THE PROBE WRITES (read this before wiring it): it PUTs a real checkpoint row into the real
24
+ * store, under a scope of its own (`{@link PARK_SELFCHECK_SCOPE_PREFIX}<random>`) that no task ever
25
+ * uses, and fences it to `expired` on EVERY exit — including the failing ones, since a pending row is,
26
+ * to an approval inbox, an approval waiting for someone.
27
+ *
28
+ * The expired row then REMAINS, and remains permanently as far as this interface is concerned: there is
29
+ * no delete verb on `CheckpointStore`, and `reap` moves PENDING rows to expired rather than removing
30
+ * expired ones. So each probe run costs one small terminal row under one throwaway scope, and reclaiming
31
+ * them is the backend's own retention concern (a table TTL, a sweep keyed on the scope prefix). An inbox
32
+ * does not see them — `listByScope` enumerates pending rows only — but `listScopes` may still report the
33
+ * scope, and during the probe's own window the row is briefly enumerable and, in principle, resolvable
34
+ * by anything watching the whole store. Both are the price of probing the real lane rather than a
35
+ * pretend one; a deployment that cannot pay it should not run the probe, and there is no read-only
36
+ * variant because a read-only probe would prove exactly what the manifest already discloses.
37
+ */
38
+ import type { RunnerDeps, TaskSpec } from "./types.js";
39
+ /** The scope prefix every synthetic probe row is filed under — never a task scope, so a probe row can
40
+ * never be mistaken for (or resolved as) a real pending approval. */
41
+ export declare const PARK_SELFCHECK_SCOPE_PREFIX = "sema:park-selfcheck:";
42
+ /**
43
+ * How long any single store call may take before the probe stops waiting and calls it a failure.
44
+ *
45
+ * A bound is MANDATORY rather than optional, for the reason this module exists at all: the probe is a
46
+ * DIAGNOSTIC for storage that may be sick, and the sickness it is most likely to meet — a connection
47
+ * that neither answers nor errors — is exactly the one an unbounded `await` turns into a wedged
48
+ * startup. A probe that hangs instead of reporting is worse than no probe: it fails in the shape it was
49
+ * written to detect, and silently.
50
+ */
51
+ export declare const PARK_SELFCHECK_STEP_TIMEOUT_MS = 10000;
52
+ /**
53
+ * How far in the FUTURE the synthetic row's abandonment deadline sits.
54
+ *
55
+ * A past deadline would make the row reapable the instant it is filed, and a CORRECT concurrent reaper
56
+ * could then expire it between the probe's own steps — reporting a healthy deployment as a broken one
57
+ * (the read that follows would see an expired row, or the enumeration an empty scope). A short future
58
+ * window keeps the row out of a reaper's reach for the length of the probe while still leaving any
59
+ * residue from a crashed probe reapable shortly after, without an operator doing anything.
60
+ */
61
+ export declare const PARK_SELFCHECK_ROW_TTL_MS = 60000;
62
+ /**
63
+ * The closed set of things a park round-trip can fail at. A machine keys on these; the accompanying
64
+ * `detail` is prose for a person and is never the discriminator.
65
+ *
66
+ * Ordered as the probe walks the contract, which is also the order an operator debugs in: a store that
67
+ * cannot be written is a different day's work from one that writes and then hands back a row with a
68
+ * dropped field.
69
+ */
70
+ export type ParkProbeFindingCode =
71
+ /** `put` threw — the store did not accept a checkpoint at all. */
72
+ "put_failed"
73
+ /** `get` threw, or answered `null` for a token the store had just accepted. */
74
+ | "row_not_readable"
75
+ /** The row came back, but a load-bearing field did not survive the round trip (see `detail` for which). */
76
+ | "row_not_faithful"
77
+ /** The row came back with a status other than `pending` — a park nobody can resolve. */
78
+ | "row_not_pending"
79
+ /** `listByScope` is implemented and did NOT list the pending row: the inbox enumeration is blind to it. */
80
+ | "row_not_enumerable"
81
+ /** `expire` threw, or lost a CAS it was the only contender for — the fence the reaper and
82
+ * `TaskStream.destroy` both depend on does not close. */
83
+ | "fence_failed"
84
+ /** The fence reported a win and the row is still `pending` on a re-read: the CAS did not land. */
85
+ | "fence_not_durable";
86
+ /** One thing that went wrong, with the prose a person needs to act on it. */
87
+ export interface ParkProbeFinding {
88
+ readonly code: ParkProbeFindingCode;
89
+ readonly detail: string;
90
+ }
91
+ /**
92
+ * What one probe run concluded.
93
+ *
94
+ * `"round_trip_ok"` is the ONLY affirmative word, and it is only ever reported with an empty
95
+ * `findings` list — the invariant is asserted rather than assumed, because a probe whose failure can be
96
+ * read as a pass is worse than no probe (it is the manifest's disclosure wearing a verification's
97
+ * clothes, which is precisely what the §0 boundary forbids).
98
+ */
99
+ export interface ParkSelfCheckResult {
100
+ readonly verdict: "round_trip_ok" | "failed" | "not_probed";
101
+ /** NON-EMPTY ⟺ `verdict === "failed"`. The two non-failing verdicts carry none: `"round_trip_ok"`
102
+ * because nothing went wrong, `"not_probed"` because nothing was attempted (its reason is in
103
+ * {@link summary} — a finding there would name a defect in a store that was never touched). */
104
+ readonly findings: readonly ParkProbeFinding[];
105
+ /** The scope the synthetic row was filed under, so an operator can find (and reap) the residue.
106
+ * Present from the moment a write was ATTEMPTED — including a `put` that threw, because a backend can
107
+ * commit and then surface a transport error, so a scope on a failed attempt is exactly where an
108
+ * operator has to look. Absent only on `"not_probed"`, where nothing was attempted. */
109
+ readonly scope?: string;
110
+ /** A single sentence naming the outcome — for a startup log line. Never the machine discriminator. */
111
+ readonly summary: string;
112
+ /** The static two-halves reading taken alongside the probe (see {@link durableParkGapOf}); absent when
113
+ * the durable topology is whole or does not apply. A probe can pass every round-trip check on a
114
+ * deployment whose parks still cannot be resumed after a restart, and this is that fact. */
115
+ readonly durableTopologyGap?: string;
116
+ /** Additive, on `"round_trip_ok"` only: the expire CAS reported a LOSS while the row nonetheless
117
+ * closed. From outside this is indistinguishable from the store's own housekeeping getting there
118
+ * first — so the verdict stays green — but a backend that transitions the row and then mis-reports
119
+ * the CAS win would pass on exactly this arm, and `TaskStream.destroy` trusts that boolean. A
120
+ * machine-readable member rather than a summary sentence, so a harness that wants to alert on it
121
+ * can (the summary is never the discriminator). */
122
+ readonly casLossObserved?: true;
123
+ }
124
+ /**
125
+ * design/252 G-6 (sibling finding, from the G-5 demo) — the DURABLE PARK's two halves, and the sentence
126
+ * to say when only one of them is wired.
127
+ *
128
+ * A park that survives a restart needs BOTH: the checkpoint row (the question) and the session (the
129
+ * conversation the answer resumes into). A deployment that wires a durable checkpoint store over the
130
+ * default in-memory session store parks perfectly and then, after a restart, fails the resume with
131
+ * "session … does not exist" — a SYMPTOM, reported at the seat that is present, naming nothing about
132
+ * the seat that is missing. Whoever reads that message goes looking for a lost session id.
133
+ *
134
+ * Returns the naming sentence, or `undefined` when there is nothing to say: both halves durable (whole),
135
+ * or no checkpoint store at all (the deployment is not doing durable parks, so there is no gap — the
136
+ * manifest's `parkLane.capable: false` is the fact there).
137
+ */
138
+ export declare function durableParkGapOf(halves: {
139
+ checkpointDurable: boolean;
140
+ checkpointWired: boolean;
141
+ sessionDurable: boolean;
142
+ }): string | undefined;
143
+ /** The two-halves reading taken off a deployment's own seats. Split from {@link durableParkGapOf} so the
144
+ * sentence can be unit-tested without a Runner and the seat reading has one home. */
145
+ export declare function durableParkGapFor(deps: Pick<RunnerDeps, "checkpointStore" | "sessionStore">, spec?: Pick<TaskSpec, "checkpointStore">): string | undefined;
146
+ /**
147
+ * design/252 G-6 — drive one synthetic park through the wired store and report what the assembly can
148
+ * actually DO, as opposed to what it declares.
149
+ *
150
+ * Never throws for a store's failure: every step's exception is captured into a {@link ParkProbeFinding}
151
+ * so a startup self-check can decide for itself whether a dead park lane is fatal. That decision is the
152
+ * deployment's, deliberately — this module reports, loudly and in a closed vocabulary, and does not
153
+ * legislate. What it will NOT do is report a pass it did not observe: `"round_trip_ok"` is returned only
154
+ * with an empty findings list, and the invariant is asserted below rather than left to reading.
155
+ */
156
+ export declare function probeParkRoundTrip(deps: Pick<RunnerDeps, "checkpointStore" | "sessionStore">, spec?: Pick<TaskSpec, "checkpointStore">): Promise<ParkSelfCheckResult>;
@@ -0,0 +1,251 @@
1
+ import { boundInputHashOf, canonicalize } from "./canonical-json.js";
2
+ import { MAX_SUPPORTED_CHECKPOINT_VERSION, mintCheckpointToken, resolveCheckpointStore } from "./checkpoint-store.js";
3
+ import { resolveDeclaredDurability } from "./wiring-manifest.js";
4
+ export const PARK_SELFCHECK_SCOPE_PREFIX = "sema:park-selfcheck:";
5
+ export const PARK_SELFCHECK_STEP_TIMEOUT_MS = 10_000;
6
+ export const PARK_SELFCHECK_ROW_TTL_MS = 60_000;
7
+ async function within(op, step) {
8
+ let timer;
9
+ try {
10
+ return await Promise.race([
11
+ Promise.resolve()
12
+ .then(op)
13
+ .then((ok) => ({ ok }))
14
+ .catch((threw) => ({ threw })),
15
+ new Promise((resolve) => {
16
+ timer = setTimeout(() => resolve({ timedOut: `${step} did not answer within ${PARK_SELFCHECK_STEP_TIMEOUT_MS}ms — a store that neither answers nor errors is the failure this probe exists to surface, not one it may wait on` }), PARK_SELFCHECK_STEP_TIMEOUT_MS);
17
+ }),
18
+ ]);
19
+ }
20
+ finally {
21
+ if (timer !== undefined)
22
+ clearTimeout(timer);
23
+ }
24
+ }
25
+ function declaresDurable(store, name) {
26
+ try {
27
+ return resolveDeclaredDurability(store, name) === "durable";
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ export function durableParkGapOf(halves) {
34
+ if (!halves.checkpointWired)
35
+ return undefined;
36
+ if (halves.checkpointDurable && halves.sessionDurable)
37
+ return undefined;
38
+ if (halves.checkpointDurable && !halves.sessionDurable) {
39
+ return ("this deployment wires a checkpoint store that declares durability but a session store that does not " +
40
+ "(RunnerDeps.sessionStore — absent means the built-in in-memory store): a durable park needs BOTH halves, " +
41
+ "so a parked approval survives the restart while the conversation it resumes into does not, and a resume " +
42
+ "arriving afterwards finds the checkpoint and not the session");
43
+ }
44
+ if (!halves.checkpointDurable && halves.sessionDurable) {
45
+ return ("this deployment wires a session store that declares durability but a checkpoint store that does not " +
46
+ "(RunnerDeps.checkpointStore.durability): a durable park needs BOTH halves, so the conversation survives " +
47
+ "the restart while the parked approval that was to resume it does not");
48
+ }
49
+ return undefined;
50
+ }
51
+ export function durableParkGapFor(deps, spec = {}) {
52
+ const checkpointStore = resolveCheckpointStore(spec, deps);
53
+ return durableParkGapOf({
54
+ checkpointWired: checkpointStore !== undefined,
55
+ checkpointDurable: declaresDurable(checkpointStore, "checkpointStore"),
56
+ sessionDurable: declaresDurable(deps.sessionStore, "sessionStore"),
57
+ });
58
+ }
59
+ function syntheticCheckpoint(scope) {
60
+ return {
61
+ token: mintCheckpointToken(),
62
+ scope,
63
+ sessionId: `${scope}:session`,
64
+ leafId: `${scope}:leaf`,
65
+ gate: { kind: "human", reason: "park wiring self-check (design/252 G-6) — no person is being asked", toolName: "SelfCheck" },
66
+ pendingAction: {
67
+ kind: "tool_approval",
68
+ toolCallId: `${scope}:call`,
69
+ toolName: "SelfCheck",
70
+ args: { probe: true, nested: { depth: 2, list: [1, 2, 3] } },
71
+ boundInputHash: boundInputHashOf({ probe: true, nested: { depth: 2, list: [1, 2, 3] } }),
72
+ batchToolCallIds: [`${scope}:call`],
73
+ completedCallIds: [],
74
+ },
75
+ state: { activeTools: ["SelfCheck"], nestedStats: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0, anyUnpriced: false } },
76
+ status: "pending",
77
+ createdAt: Date.now(),
78
+ version: MAX_SUPPORTED_CHECKPOINT_VERSION,
79
+ suspendCount: 1,
80
+ suspendedAt: Date.now(),
81
+ resourceLedger: { spentMicroUsd: 0, spentTokens: 0, spentTurns: 0, sliceCount: 1 },
82
+ humanReview: { count: 1, totalWaitMs: 0, gates: [{ kind: "human", waitMs: 0, decision: "approve" }] },
83
+ sourceTaskId: `${scope}:task`,
84
+ principal: `${scope}:principal`,
85
+ durableApproval: { scope, ttlMs: PARK_SELFCHECK_ROW_TTL_MS },
86
+ deadline: Date.now() + PARK_SELFCHECK_ROW_TTL_MS,
87
+ };
88
+ }
89
+ function faithfulnessDelta(sent, got) {
90
+ const project = (cp) => ({
91
+ token: cp.token,
92
+ scope: cp.scope,
93
+ sessionId: cp.sessionId,
94
+ leafId: cp.leafId,
95
+ gate: cp.gate,
96
+ pendingAction: cp.pendingAction,
97
+ state: cp.state,
98
+ deadline: cp.deadline,
99
+ createdAt: cp.createdAt,
100
+ version: cp.version,
101
+ suspendCount: cp.suspendCount,
102
+ suspendedAt: cp.suspendedAt,
103
+ resourceLedger: cp.resourceLedger,
104
+ humanReview: cp.humanReview,
105
+ sourceTaskId: cp.sourceTaskId,
106
+ principal: cp.principal,
107
+ durableApproval: cp.durableApproval,
108
+ });
109
+ const a = canonicalize(project(sent));
110
+ const b = canonicalize(project(got));
111
+ return a === b ? undefined : `the row came back different from the row that was filed (sent ${a}; read ${b})`;
112
+ }
113
+ export async function probeParkRoundTrip(deps, spec = {}) {
114
+ const gap = durableParkGapFor(deps, spec);
115
+ const gapCell = gap !== undefined ? { durableTopologyGap: gap } : {};
116
+ const store = resolveCheckpointStore(spec, deps);
117
+ if (store === undefined) {
118
+ return {
119
+ verdict: "not_probed",
120
+ findings: [],
121
+ summary: "no checkpoint store is wired on this assembly — there is no park lane to probe (the manifest reports the same fact as parkLane.capable: false)",
122
+ ...gapCell,
123
+ };
124
+ }
125
+ const scope = `${PARK_SELFCHECK_SCOPE_PREFIX}${mintCheckpointToken()}`;
126
+ const cp = syntheticCheckpoint(scope);
127
+ const said = (err) => {
128
+ try {
129
+ const m = err instanceof Error ? err.message : undefined;
130
+ return typeof m === "string" ? m : `a ${typeof err} the store threw`;
131
+ }
132
+ catch {
133
+ return "a value the store threw that could not be described";
134
+ }
135
+ };
136
+ const shown = (v) => {
137
+ try {
138
+ return typeof v === "string" ? JSON.stringify(v) : `a ${typeof v}`;
139
+ }
140
+ catch {
141
+ return "an undescribable value";
142
+ }
143
+ };
144
+ const fenceQuietly = async () => {
145
+ await within(async () => {
146
+ try {
147
+ await store.expire(cp.token, scope);
148
+ }
149
+ catch {
150
+ }
151
+ }, "the cleanup fence");
152
+ };
153
+ const failed = async (code, detail) => {
154
+ await fenceQuietly();
155
+ return { verdict: "failed", findings: [{ code, detail }], scope, summary: `the park lane failed its round-trip self-check at "${code}": ${detail}`, ...gapCell };
156
+ };
157
+ let putStarted;
158
+ const put = await within(() => {
159
+ const p = Promise.resolve().then(() => store.put(cp.token, cp));
160
+ putStarted = p;
161
+ return p;
162
+ }, "put");
163
+ if ("timedOut" in put) {
164
+ void putStarted?.catch(() => { }).then(() => store.expire(cp.token, scope)).catch(() => { });
165
+ return await failed("put_failed", put.timedOut);
166
+ }
167
+ if ("threw" in put)
168
+ return await failed("put_failed", `the checkpoint store refused to file a synthetic park: ${said(put.threw)}`);
169
+ const first = await within(() => store.get(cp.token), "get");
170
+ if ("timedOut" in first)
171
+ return await failed("row_not_readable", first.timedOut);
172
+ if ("threw" in first)
173
+ return await failed("row_not_readable", `the checkpoint store threw reading back the row it had just accepted: ${said(first.threw)}`);
174
+ const read = first.ok;
175
+ if (read === null)
176
+ return await failed("row_not_readable", "the checkpoint store accepted the row and then answered null for its own token — a park filed here would be unresumable");
177
+ if (typeof read !== "object")
178
+ return await failed("row_not_readable", `the checkpoint store answered ${read === undefined ? "undefined" : typeof read} for its own token — neither a checkpoint nor the absence of one`);
179
+ let delta;
180
+ let status;
181
+ try {
182
+ delta = faithfulnessDelta(cp, read);
183
+ status = read.status;
184
+ }
185
+ catch (err) {
186
+ return await failed("row_not_faithful", `the row the store returned could not be read for comparison: ${said(err)}`);
187
+ }
188
+ if (delta !== undefined)
189
+ return await failed("row_not_faithful", delta);
190
+ if (status !== "pending")
191
+ return await failed("row_not_pending", `the filed row came back with status ${shown(status)} — a park in any other status is one nobody can resolve`);
192
+ const enumerable = store.listByScope !== undefined;
193
+ if (store.listByScope !== undefined) {
194
+ const listByScope = store.listByScope.bind(store);
195
+ const listed = await within(() => listByScope(scope), "listByScope");
196
+ if ("timedOut" in listed)
197
+ return await failed("row_not_enumerable", listed.timedOut);
198
+ if ("threw" in listed)
199
+ return await failed("row_not_enumerable", `listByScope threw for the probe's own scope: ${said(listed.threw)}`);
200
+ let present;
201
+ try {
202
+ present = Array.isArray(listed.ok) && listed.ok.some((row) => row?.token === cp.token);
203
+ }
204
+ catch (err) {
205
+ return await failed("row_not_enumerable", `the listByScope result could not be read: ${said(err)}`);
206
+ }
207
+ if (!present)
208
+ return await failed("row_not_enumerable", `listByScope("${scope}") did not include the pending row it holds — an inbox enumerating this scope would show no approvals`);
209
+ }
210
+ const fence = await within(() => store.expire(cp.token, scope), "expire");
211
+ if ("timedOut" in fence)
212
+ return await failed("fence_failed", fence.timedOut);
213
+ if ("threw" in fence)
214
+ return await failed("fence_failed", `expire threw on the probe's own pending row: ${said(fence.threw)}`);
215
+ const fenced = fence.ok === true;
216
+ const second = await within(() => store.get(cp.token), "get (after the fence)");
217
+ if ("timedOut" in second)
218
+ return await failed("fence_not_durable", second.timedOut);
219
+ if ("threw" in second)
220
+ return await failed("fence_not_durable", `the store threw re-reading the fenced row: ${said(second.threw)}`);
221
+ const after = second.ok;
222
+ let afterStatus;
223
+ if (after !== null) {
224
+ if (typeof after !== "object")
225
+ return await failed("fence_not_durable", `the store answered ${after === undefined ? "undefined" : typeof after} re-reading the fenced row — neither a checkpoint nor the absence of one`);
226
+ try {
227
+ afterStatus = after.status;
228
+ }
229
+ catch (err) {
230
+ return await failed("fence_not_durable", `the fenced row's status could not be read: ${said(err)}`);
231
+ }
232
+ if (afterStatus !== "expired") {
233
+ return await failed(fenced ? "fence_not_durable" : "fence_failed", fenced
234
+ ? `expire reported a win and the row reads ${shown(afterStatus)} — the contract's transition is pending → expired, so the CAS did not land`
235
+ : `expire lost an UNCONTENDED CAS and the row reads ${shown(afterStatus)} — the fence that keeps a checkpoint from being both reaped and resumed does not close here`);
236
+ }
237
+ }
238
+ const steps = `filed, read back${enumerable ? ", enumerated" : ""} and fenced`;
239
+ const notExercised = enumerable ? "" : " (this store implements no listByScope, so the inbox enumeration was NOT exercised)";
240
+ const casNote = fenced ? "" : " (the expire CAS reported a loss while the row closed — consistent with store housekeeping, but a backend that mis-reports CAS wins is not distinguished here)";
241
+ return {
242
+ verdict: "round_trip_ok",
243
+ findings: [],
244
+ ...(fenced ? {} : { casLossObserved: true }),
245
+ scope,
246
+ summary: after === null
247
+ ? `the park lane ${steps} a synthetic checkpoint${notExercised} — the row is no longer present (a deleting backend or its own housekeeping removed it), so nothing resumable remains${casNote}`
248
+ : `the park lane ${steps} a synthetic checkpoint${notExercised} — residual expired row in scope "${scope}"${casNote}`,
249
+ ...gapCell,
250
+ };
251
+ }
@@ -3,7 +3,10 @@ export declare class PushQueue<T> implements AsyncIterable<T> {
3
3
  private buffer;
4
4
  private waiters;
5
5
  private closed;
6
- push(value: T): void;
6
+ /** Returns whether the value was ACCEPTED (`false` after close — the push is a silent no-op then).
7
+ * Callers that mirror a pushed value to a second reader gate on this so both sides stay in parity
8
+ * (#253: a status frame must not reach a side sink the stream itself already refused). */
9
+ push(value: T): boolean;
7
10
  close(): void;
8
11
  [Symbol.asyncIterator](): AsyncIterator<T>;
9
12
  }
@@ -4,7 +4,7 @@ export class PushQueue {
4
4
  closed = false;
5
5
  push(value) {
6
6
  if (this.closed) {
7
- return;
7
+ return false;
8
8
  }
9
9
  const waiter = this.waiters.shift();
10
10
  if (waiter) {
@@ -13,6 +13,7 @@ export class PushQueue {
13
13
  else {
14
14
  this.buffer.push(value);
15
15
  }
16
+ return true;
16
17
  }
17
18
  close() {
18
19
  this.closed = true;
@@ -40,6 +40,12 @@ export interface PrepareAcquireReconcileInput {
40
40
  * {@link import("./prepare-safety-scan.js").PrepareSafetyScanResult}); reconcile classifies an
41
41
  * orphaned call's retry-safety by it. */
42
42
  toolEffects: Map<string, ToolEffect>;
43
+ /** borrowed-readonly — design/252 G-6 sibling: the durable-park TOPOLOGY gap sentence for this
44
+ * deployment (`durableParkGapFor`), or `undefined` when the topology is whole / does not apply.
45
+ * Read ONLY on the fail-loud missing-session path below, where it turns a symptom into a named
46
+ * absent seat; nothing in the phase's control flow depends on it. Computed by the driver (which
47
+ * holds `deps`) rather than here, keeping this slice's input the narrow Pick its contract says. */
48
+ durableParkGap?: string;
43
49
  }
44
50
  /** The phase's outputs (design/238 相 API 规则件 four-class form). All five are fresh bindings —
45
51
  * the driver destructures them into consts, so a consumer moved ahead of this call is a lexical
@@ -39,7 +39,8 @@ export async function prepareAcquireReconcile(input) {
39
39
  }
40
40
  catch (err) {
41
41
  if (spec.requireExistingSession && err?.code === "not_found") {
42
- const e = new Error(`requireExistingSession: session "${spec.sessionId}" does not exist — refusing a silent fresh run (design/114 Phase3)`);
42
+ const e = new Error(`requireExistingSession: session "${spec.sessionId}" does not exist — refusing a silent fresh run (design/114 Phase3)` +
43
+ (input.durableParkGap !== undefined ? `. Note the assembly: ${input.durableParkGap}` : ""));
43
44
  e.code = "resume.session_not_found";
44
45
  throw e;
45
46
  }
@@ -169,9 +169,14 @@ export interface Prepared {
169
169
  * emits; the tool_result-side delete in prepare-task never fires for immediate results). */
170
170
  blockedToolCalls: Set<string>;
171
171
  /**
172
- * What ended the approval a gated call was waiting on, keyed by tool-call id written ONLY by the
173
- * tool gate, at the one exit where an ask resolved, and read once when that call's `tool_end` frame
174
- * is minted (the reader deletes on read; a call the gate never settled has no entry).
172
+ * WHAT ended the approval a gated call was waiting on and design/252 G-7WHOSE settlement it
173
+ * was, keyed by tool-call id: written ONLY by the tool gate, at the one exit where an ask resolved,
174
+ * and read once when that call's `tool_end` frame is minted (the reader deletes on read; a call the
175
+ * gate never settled has no entry, and an entry never names neither fact).
176
+ *
177
+ * ONE record rather than two parallel maps because they are one observation: an attribution without
178
+ * the settlement kind beside it is unreadable ("alice" — approved? her window elapsed?), and two maps
179
+ * keyed alike are two chances to drain one and leak the other.
175
180
  *
176
181
  * It is a sideband and not a field on the tool RESULT because a result is not a trustworthy carrier
177
182
  * for this: `details` is arbitrary tool-authored data that post-tool hooks may also replace, so a
@@ -180,7 +185,10 @@ export interface Prepared {
180
185
  * adjudicating layer can write. Same reason the entries are keyed by CALL id: the gate adjudicated
181
186
  * that exact call, and the frame that reads it is that call's own.
182
187
  */
183
- approvalSettledBy: Map<string, import("../tool-policy.js").ApprovalSettledBy>;
188
+ approvalSettlement: Map<string, {
189
+ settledBy?: import("../tool-policy.js").ApprovalSettledBy;
190
+ approver?: string;
191
+ }>;
184
192
  /** Summed usage of nested sub-runs (sub-agents) spawned by this task's tools. */
185
193
  nestedStats: NestedUsageAccum;
186
194
  /** RB-430-a: prepare-time rewind disclosures (conversation-only branch / no snapshot backend / no file
@@ -1285,6 +1293,19 @@ export interface RunInternals {
1285
1293
  * MODEL context (this is purely a render channel). Absent unless the deployment opted in.
1286
1294
  */
1287
1295
  onForwardEvent?: (event: TaskEvent) => void;
1296
+ /**
1297
+ * #253 — the run's OWN top-level `status` TaskEvent stream (brain liveness: rate-limit/retry/
1298
+ * reconnect/circuit-open), offered to the internals holder beside the queue. The queue alone was
1299
+ * enough for a direct `runTask` caller (the TaskStream carries these frames), but a COMPOSITION
1300
+ * entry (verify/cascade) drains its inner legs' queues itself — without this seat, an inner leg's
1301
+ * retry disclosure died inside the gate and the wire showed a silent stall. Fed the SAME frame
1302
+ * object the queue receives, at the same moment; contained by the run's safe notifier (#248 form:
1303
+ * a throwing sink is swallowed, first failure per site disclosed, never faults the leg). Subagent
1304
+ * frames still ride {@link onForwardEvent} — this seat is ONLY the run's own status type.
1305
+ */
1306
+ onStatusEvent?: (event: Extract<TaskEvent, {
1307
+ type: "status";
1308
+ }>) => void;
1288
1309
  /**
1289
1310
  * design/115 P2 core slice — trusted run-local system-injection sink. `Runner.runLocked` wires this to the
1290
1311
  * live TaskStream queue plus the current harness follow-up lane; it is not a public TaskSpec field.