@sema-agent/core 7.9.1 → 7.10.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/agents/child-model-seat.d.ts +81 -0
  3. package/dist/agents/child-model-seat.js +46 -0
  4. package/dist/agents/subagent.d.ts +5 -3
  5. package/dist/agents/subagent.js +20 -14
  6. package/dist/core/ask-unresolvable-notice.d.ts +52 -0
  7. package/dist/core/ask-unresolvable-notice.js +25 -0
  8. package/dist/core/auto-mode.d.ts +62 -3
  9. package/dist/core/auto-mode.js +31 -0
  10. package/dist/core/checkpoint-store.d.ts +14 -0
  11. package/dist/core/checkpoint-store.js +2 -1
  12. package/dist/core/engine-notice.d.ts +28 -7
  13. package/dist/core/gate-lanes.js +15 -0
  14. package/dist/core/governance-codes.d.ts +1 -1
  15. package/dist/core/governance-codes.js +4 -0
  16. package/dist/core/hooks.d.ts +24 -1
  17. package/dist/core/hooks.js +2 -0
  18. package/dist/core/permission-rule-model.d.ts +51 -16
  19. package/dist/core/permission-rule-model.js +55 -21
  20. package/dist/core/permission-rules.d.ts +6 -4
  21. package/dist/core/permission-rules.js +14 -14
  22. package/dist/core/roles.d.ts +8 -0
  23. package/dist/core/runner/contracts.d.ts +29 -4
  24. package/dist/core/runner/denial-limit-arms.d.ts +14 -3
  25. package/dist/core/runner/denial-limit-arms.js +15 -5
  26. package/dist/core/runner/permission-rule-lanes.d.ts +7 -1
  27. package/dist/core/runner/permission-rule-lanes.js +9 -3
  28. package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
  29. package/dist/core/runner/prepare-caps-and-workflow.js +14 -4
  30. package/dist/core/runner/prepare-gate-stations.d.ts +3 -2
  31. package/dist/core/runner/prepare-gate-stations.js +3 -0
  32. package/dist/core/runner/prepare-policy-chain.js +7 -6
  33. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
  34. package/dist/core/runner/prepare-wiring-manifest.js +8 -1
  35. package/dist/core/runner/runtask.d.ts +34 -32
  36. package/dist/core/runner/runtask.js +64 -34
  37. package/dist/core/runner-deps.d.ts +9 -2
  38. package/dist/core/swappable-deps.d.ts +90 -0
  39. package/dist/core/swappable-deps.js +55 -0
  40. package/dist/core/tool-policy.d.ts +26 -0
  41. package/dist/core/tool-policy.js +5 -1
  42. package/dist/core/wiring-manifest.d.ts +15 -1
  43. package/dist/core/wiring-manifest.js +10 -2
  44. package/dist/core/workflow-journal-store.d.ts +21 -2
  45. package/dist/core/workflow-journal-store.js +1 -1
  46. package/dist/engine/execution-env/node-execution-env.d.ts +2 -0
  47. package/dist/engine/execution-env/node-execution-env.js +2 -1
  48. package/dist/engine/harness/types.d.ts +11 -0
  49. package/dist/index.d.ts +5 -3
  50. package/dist/index.js +5 -3
  51. package/dist/orchestration/run-workflow-tool.d.ts +17 -0
  52. package/dist/orchestration/run-workflow-tool.js +12 -0
  53. package/dist/orchestration/workflow-observe.d.ts +1 -1
  54. package/dist/orchestration/workflow-observe.js +2 -0
  55. package/dist/orchestration/workflow-types.d.ts +37 -2
  56. package/dist/orchestration/workflow-types.js +16 -0
  57. package/dist/orchestration/workflow.d.ts +41 -2
  58. package/dist/orchestration/workflow.js +316 -48
  59. package/dist/stores/file/workflow-journal-store.js +10 -3
  60. package/package.json +1 -1
  61. package/test/export-surface.snapshot.json +51 -5
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The deployment seats a Runner may HOT-SWAP after construction — the vocabulary behind the ONE door,
3
+ * `Runner.swapDeps` (runtask.ts).
4
+ *
5
+ * WHY ONE DOOR. Each hot-swappable seat used to be a verb of its own (`swapModels` was the first), and a
6
+ * verb per seat restates the same three laws every time — validate the candidate BEFORE anything is
7
+ * written, write the deps object ONCE (a swap is atomic: a refused candidate leaves the current deps
8
+ * byte-identical), announce the change under a notice code — while the question "which seats may be
9
+ * swapped at all" was answered by the list of verbs, i.e. by nothing a compiler could see. With one door
10
+ * the laws are stated once, the swappable set is THIS closed list, and every seat carries its rows in
11
+ * the tables beside it: the compiler refuses a member without its rows ({@link AssertAllKeysHandled}),
12
+ * the door refuses a key outside the set at run time (never a silent ignore — a hot-config driver
13
+ * handing `{ brain }` must learn that the brain is not a swappable seat, not watch nothing happen).
14
+ *
15
+ * WHAT A SWAP MEANS — the natural snapshot, one sentence for every seat: a running task reads the deps
16
+ * object it was handed at its prepare and holds that reference; a swap replaces the Runner's object, so
17
+ * every LATER prepare reads the new seat and every in-flight leg keeps the value it prepared under.
18
+ * Nothing is re-routed, re-priced or re-fenced mid-run; the divergence window is exactly the in-flight
19
+ * legs' lifetime, by design (the `swapModels` precedent, now the rule for the whole set).
20
+ *
21
+ * KEY PRESENCE IS THE INSTRUCTION: a key present on the candidate is REPLACED (an explicit `undefined`
22
+ * CLEARS an optional seat back to its engine default — `readFace: undefined` ⇒ the deployment declares
23
+ * no read face, `tiers: undefined` ⇒ no tier bindings); a key absent is KEPT. `Object.hasOwn`, never
24
+ * truthiness, so the two spellings cannot be confused. An EMPTY candidate is refused: a swap that names
25
+ * no seat is a caller defect, not a no-op.
26
+ */
27
+ import type { AssertAllKeysHandled } from "./ask-origin.js";
28
+ import type { RunnerDeps } from "./runner-deps.js";
29
+ /**
30
+ * The closed set of hot-swappable deployment seats.
31
+ * - `models` — the model catalog generation (the same-name redirect, re-pricing and retirement rules
32
+ * of the former `swapModels`: a string ref resolves against the new catalog at the next
33
+ * prepare; a `Model` object a leg already holds is untouched).
34
+ * - `tiers` — the tier bindings, a BINDING OVER a catalog: it may only be swapped together with
35
+ * `models` (the door refuses `tiers` alone — re-expanding bindings over an already
36
+ * expanded catalog would keep the retired tier words as ordinary keys, which is exactly
37
+ * the stale-generation shape the swap exists to end). Omitted beside `models` ⇒ the
38
+ * current bindings are re-applied over the new catalog; present (including `undefined`)
39
+ * ⇒ replaced.
40
+ * - `readFace` — the deployment's read-face declaration ({@link RunnerDeps.readFace}); screened by the
41
+ * same value gate every prepare applies (`assertReadFaceValue`: `"open"` | `"roots"`
42
+ * | absent, anything else refused loudly). The RESOLVED face of a leg is still the
43
+ * resolver's business (task seat, governance, the read-only mount); the swap changes the
44
+ * deployment seat that resolution reads.
45
+ */
46
+ export declare const SWAPPABLE_DEP_SEATS: readonly ["models", "tiers", "readFace"];
47
+ export type SwappableDepSeat = (typeof SWAPPABLE_DEP_SEATS)[number];
48
+ /** The candidate a swap takes: the swappable seats of {@link RunnerDeps}, each optional (presence = instruction). */
49
+ export type SwappableDeps = Pick<RunnerDeps, SwappableDepSeat>;
50
+ /** Whether a key names a swappable seat. */
51
+ export declare function isSwappableDepSeat(k: unknown): k is SwappableDepSeat;
52
+ /**
53
+ * Which notice a seat's successful swap is announced under — the disposition table over the set (a seat
54
+ * with no row does not compile: {@link SwapNoticeTableCoversEverySeat}). `models` and `tiers` share the
55
+ * catalog-generation line (one swap, one line, both counts); the read face has its own.
56
+ */
57
+ export declare const SWAP_SEAT_NOTICE: {
58
+ readonly models: "config.models_swapped";
59
+ readonly tiers: "config.models_swapped";
60
+ readonly readFace: "config.read_face_swapped";
61
+ };
62
+ /** The fence over the notice table: `never` while every seat has a row. */
63
+ export type SwapNoticeTableCoversEverySeat = AssertAllKeysHandled<Exclude<SwappableDepSeat, keyof typeof SWAP_SEAT_NOTICE>>;
64
+ /** What the screen hands back: the seats present (set order) and the ONE read of each present seat's value,
65
+ * copied at screen time — the door writes from THIS object and never re-reads the caller's. */
66
+ export interface ScreenedSwappableDeps {
67
+ readonly seats: readonly SwappableDepSeat[];
68
+ /** A fresh plain object: the screened value of every present seat (`models`/`tiers` shallow-copied,
69
+ * `readFace` the screened word; an explicit `undefined` is kept as a PRESENT own key so the door can
70
+ * tell "clear this seat" from "not named"). */
71
+ readonly candidate: SwappableDeps;
72
+ }
73
+ /**
74
+ * Screen a swap candidate BEFORE anything is written: the object shape, the key set (every own key must
75
+ * be a member of {@link SWAPPABLE_DEP_SEATS}; at least one), the `tiers`-rides-with-`models` rule, and
76
+ * each present seat's own value gate. Returns the seats present (set order) and a SNAPSHOT of their values
77
+ * — the one read. Throws on the first defect; the caller has written nothing yet, so the current deps stay
78
+ * in force byte-identical.
79
+ *
80
+ * ONE READ, BY CONSTRUCTION: every seat value is read off the caller's object exactly once, here, and the
81
+ * screened copy is what the door writes and announces. A getter-backed or concurrently-mutated candidate
82
+ * therefore cannot present a legal value to the screen and a different one to the write (or to the
83
+ * announcement, which runs AFTER the write — a throw there would have left a half-announced swap in
84
+ * force). The prepare doors read their deps seats the same way.
85
+ *
86
+ * The per-seat gate is the SAME one the seat's prepare-time reader applies (`readFace` ⇒
87
+ * `assertReadFaceValue` with the deployment-seat name a prepare door uses — one value, one loudness, one
88
+ * seat name on every leg), so a value the door admits is a value every later prepare admits.
89
+ */
90
+ export declare function screenSwappableDeps(next: unknown): ScreenedSwappableDeps;
@@ -0,0 +1,55 @@
1
+ import { assertReadFaceValue } from "../tools/fs/read-face.js";
2
+ export const SWAPPABLE_DEP_SEATS = ["models", "tiers", "readFace"];
3
+ const SWAPPABLE_DEP_SEAT_SET = new Set(SWAPPABLE_DEP_SEATS);
4
+ export function isSwappableDepSeat(k) {
5
+ return SWAPPABLE_DEP_SEAT_SET.has(k);
6
+ }
7
+ export const SWAP_SEAT_NOTICE = {
8
+ models: "config.models_swapped",
9
+ tiers: "config.models_swapped",
10
+ readFace: "config.read_face_swapped",
11
+ };
12
+ export function screenSwappableDeps(next) {
13
+ if (next === null || typeof next !== "object" || Array.isArray(next)) {
14
+ throw new Error(`swapDeps: the candidate must be a plain object naming the seats to swap (got ${next === null ? "null" : Array.isArray(next) ? "array" : typeof next}) — the current deps stay in force`);
15
+ }
16
+ const keys = Object.keys(next);
17
+ const unknown = keys.filter((k) => !isSwappableDepSeat(k));
18
+ if (unknown.length > 0) {
19
+ throw new Error(`swapDeps: ${unknown.map((k) => JSON.stringify(k)).join(", ")} is not a hot-swappable seat (the closed set is ${SWAPPABLE_DEP_SEATS.join(" | ")}) — the current deps stay in force`);
20
+ }
21
+ const present = SWAPPABLE_DEP_SEATS.filter((seat) => Object.hasOwn(next, seat));
22
+ if (present.length === 0) {
23
+ throw new Error(`swapDeps: the candidate names no seat (an empty swap is a caller defect, not a no-op) — the current deps stay in force`);
24
+ }
25
+ const raw = next;
26
+ const candidate = {};
27
+ for (const seat of present) {
28
+ const v = raw[seat];
29
+ switch (seat) {
30
+ case "models":
31
+ if (v === null || typeof v !== "object" || Array.isArray(v)) {
32
+ throw new Error(`swapDeps: models must be a plain Record<string, Model> (got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}) — the current generation stays in force`);
33
+ }
34
+ candidate.models = { ...v };
35
+ break;
36
+ case "tiers":
37
+ if (!present.includes("models")) {
38
+ throw new Error(`swapDeps: tiers is a binding over models — pass the catalog it binds in the same swap (\`{ models, tiers }\`); the current generation stays in force`);
39
+ }
40
+ if (v !== undefined && (v === null || typeof v !== "object" || Array.isArray(v))) {
41
+ throw new Error(`swapDeps: tiers must be a plain Record<string, ModelRef> or undefined (got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}) — the current generation stays in force`);
42
+ }
43
+ candidate.tiers = v === undefined ? undefined : { ...v };
44
+ break;
45
+ case "readFace":
46
+ candidate.readFace = assertReadFaceValue(v, "readFace (deployment seat)");
47
+ break;
48
+ default: {
49
+ const _exhaustive = seat;
50
+ throw new Error(`swapDeps: unhandled seat ${String(_exhaustive)}`);
51
+ }
52
+ }
53
+ }
54
+ return { seats: present, candidate: candidate };
55
+ }
@@ -204,6 +204,23 @@ export type PermissionResult = {
204
204
  * same safe direction as `matchedAskRule`); it cannot state a window of its own, because the
205
205
  * route it would be a window for has not been chosen yet. */
206
206
  denialLimitFallback?: import("./auto-mode.js").UnarmedDenialLimitFallback;
207
+ /** #616 (additive): the auto-mode classifier was CONSULTED on this ask and could not run — the ask
208
+ * flows the original chain exactly as it would have (routing, origin, bit and members unchanged),
209
+ * carrying the station FACT beside them so a card can say "asked because the classifier was
210
+ * unavailable (timeout)" instead of reading as ordinary hesitation. `cause` is the verdict's own
211
+ * word ({@link import("./auto-mode.js").AutoModeUnavailableCause}: `error` / `timeout` /
212
+ * `breaker_open`). A FACT, not an origin: CC 2.1.250 marks the same condition as a denial KIND on
213
+ * the outcome (`automode-unavailable`) beside the decision's provenance, never in place of it, and
214
+ * this engine's divergence (the ask reaches a person instead of being denied) does not move the
215
+ * fact onto the origin axis either. ENGINE-STAMPED at the classifier stations (the gate's own and
216
+ * the inherited-lane arms) only when the verdict was `unavailable`; `parse_error` stamps nothing
217
+ * (the classifier ran and answered outside its contract — a different sentence). Display metadata:
218
+ * nothing reads it to decide anything, so a policy that self-declares it can only put its own
219
+ * sentence on its own card. Carried onto the approval request and the durable row by the carry
220
+ * stations. */
221
+ classifierUnavailable?: {
222
+ readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
223
+ };
207
224
  /** #144 disclosure (additive): a persisted allow rule MATCHED this call but could not clear the
208
225
  * ask, because the ask is MANDATED (operator shellGate:"always", or the tool's own
209
226
  * egress/irreversibility marks) rather than a classifier's hesitation — "allow rules silence
@@ -1191,6 +1208,15 @@ export interface AskRequest {
1191
1208
  * that forwards a decision's member here does not compile, which is the point: the window is the
1192
1209
  * route's fact, and forwarding it silently is how a configured window becomes a wait with none. */
1193
1210
  readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1211
+ /** #616 (additive) — present ⇔ the auto-mode classifier was consulted on this ask and could not run (see
1212
+ * the {@link PermissionResult} ask-arm member of the same name): the card's "asked because the
1213
+ * classifier was unavailable" fact, with the verdict's own cause word. Absent on every ask the classifier
1214
+ * answered, was not eligible for, or was not wired for — read presence, never absence. Filled by the
1215
+ * carry stations from the decision, never a caller/worker-settable field; the durable park row carries
1216
+ * the same member (`PendingAction.tool_approval.classifierUnavailable`). */
1217
+ readonly classifierUnavailable?: {
1218
+ readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
1219
+ };
1194
1220
  /** (additive) WHICH AUTHORITY raised this ask — the wire twin of the {@link PermissionResult} ask-arm
1195
1221
  * member of the same name, one word from the closed {@link import("./ask-origin.js").AskOrigin} set.
1196
1222
  * The gate's own mint station copies the engine-stamped word; the three inherited-lane stations
@@ -410,6 +410,7 @@ export function combinePolicies(...policies) {
410
410
  let ruleAskText;
411
411
  let probeMandateSeen = false;
412
412
  let fallbackSeen;
413
+ let classifierUnavailableSeen;
413
414
  for (const p of policies) {
414
415
  const d = refuseOutOfContractDecision(await p.check(current, signal));
415
416
  if (d.action === "deny") {
@@ -440,13 +441,16 @@ export function combinePolicies(...policies) {
440
441
  probeMandateSeen = true;
441
442
  if (d.action === "ask" && d.denialLimitFallback !== undefined && fallbackSeen === undefined)
442
443
  fallbackSeen = d.denialLimitFallback;
444
+ if (d.action === "ask" && d.classifierUnavailable !== undefined && classifierUnavailableSeen === undefined)
445
+ classifierUnavailableSeen = d.classifierUnavailable;
443
446
  }
444
447
  if (asked) {
445
448
  const merged = rewrite?.updatedInput;
446
449
  const withRuleAsk = ruleAskText !== undefined && asked.matchedAskRule === undefined ? { ...asked, matchedAskRule: ruleAskText } : asked;
447
450
  const withMark = probeMandateSeen && withRuleAsk.probeMandated !== true ? { ...withRuleAsk, probeMandated: true } : withRuleAsk;
448
451
  const withFallback = fallbackSeen !== undefined && withMark.denialLimitFallback === undefined ? { ...withMark, denialLimitFallback: fallbackSeen, requiresRealApproval: true } : withMark;
449
- return merged !== undefined ? { ...withFallback, updatedInput: merged } : withFallback;
452
+ const withFact = classifierUnavailableSeen !== undefined && withFallback.classifierUnavailable === undefined ? { ...withFallback, classifierUnavailable: { cause: classifierUnavailableSeen.cause } } : withFallback;
453
+ return merged !== undefined ? { ...withFact, updatedInput: merged } : withFact;
450
454
  }
451
455
  const allowed = rewrite ?? ALLOW;
452
456
  if (settledAllow === undefined)
@@ -244,10 +244,21 @@ export interface WiringManifest {
244
244
  * classifier decider was minted for this leg), and `reason` names the first arm that failed
245
245
  * otherwise — see {@link AUTO_MODE_ARM_REASONS}. A serving layer that used to infer the mode from
246
246
  * spec shape reads it here instead.
247
+ *
248
+ * `breaker` (#616) — the SESSION-level breaker read face: the most recent one-way breaker trip recorded
249
+ * for this session on this Runner (an EARLIER leg's decider — the latch is a per-run fact and a leg-start
250
+ * manifest's own decider is closed by construction, which is why there is no `open` boolean here: it
251
+ * would read `false` on every mint; the reason vocabulary reserves `latch_open` for a mid-leg re-read
252
+ * face nothing mints today). Present ⇔ a trip was recorded (never tripped, evicted or a standalone
253
+ * prepare ⇒ absent). `{ openedAtMs, lastCause, failures, runId }` — see
254
+ * {@link import("./auto-mode.js").AutoModeBreakerTrip}. A per-leg observation like `mcp`, so it is NOT
255
+ * part of {@link configFingerprint}: legs of one assembly must fingerprint alike whether or not a
256
+ * classifier was down.
247
257
  */
248
258
  autoMode?: {
249
259
  armed: boolean;
250
260
  reason: AutoModeArmReason;
261
+ breaker?: import("./auto-mode.js").AutoModeBreakerTrip;
251
262
  };
252
263
  /**
253
264
  * EFFECTIVE half only, and ALWAYS present on an engine-minted effective manifest (the `autoMode`
@@ -336,10 +347,13 @@ export interface WiringFacts {
336
347
  restore: string;
337
348
  };
338
349
  /** Effective half only — see {@link WiringManifest.autoMode}; the static half has no leg to arm.
339
- * `armed` must agree with `reason` (`armed ⇔ reason === "armed"`); a contradicting pair is refused. */
350
+ * `armed` must agree with `reason` (`armed ⇔ reason === "armed"`); a contradicting pair is refused.
351
+ * `breaker` (#616) is the session's most recent recorded trip, or absent; a trip whose cause is outside
352
+ * the closed set or whose numbers are not finite is refused. */
340
353
  autoMode?: {
341
354
  armed: boolean;
342
355
  reason: AutoModeArmReason;
356
+ breaker?: import("./auto-mode.js").AutoModeBreakerTrip;
343
357
  };
344
358
  /** Effective half only — see {@link WiringManifest.mcp}; the static half materializes nothing.
345
359
  * Copied entry-wise onto the manifest (the caller's array is never aliased). */
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { AUTO_MODE_BREAKER_CAUSES, isAutoModeBreakerCause } from "./auto-mode.js";
2
3
  import { canonicalize } from "./canonical-json.js";
3
4
  import { resolveCheckpointStore } from "./checkpoint-store.js";
4
5
  import { isLiveQuestionFace } from "./ask-question.js";
@@ -71,7 +72,13 @@ function readAutoModeFact(fact) {
71
72
  if (!AUTO_MODE_ARM_REASONS.includes(fact.reason) || fact.armed !== (fact.reason === "armed")) {
72
73
  throw new Error(`the auto-mode wiring fact is inconsistent (armed=${String(fact.armed)}, reason=${JSON.stringify(fact.reason)}) — armed must hold exactly when reason is "armed", and reason must be one of ${AUTO_MODE_ARM_REASONS.join("|")}`);
73
74
  }
74
- return { armed: fact.armed, reason: fact.reason };
75
+ const b = fact.breaker;
76
+ if (b === undefined)
77
+ return { armed: fact.armed, reason: fact.reason };
78
+ if (!isAutoModeBreakerCause(b.lastCause) || !Number.isFinite(b.openedAtMs) || !Number.isInteger(b.failures) || b.failures < 1 || typeof b.runId !== "string" || b.runId === "") {
79
+ throw new Error(`the auto-mode breaker fact is malformed (${JSON.stringify(b)}) — lastCause must be one of ${AUTO_MODE_BREAKER_CAUSES.join("|")}, openedAtMs finite, failures a positive integer, runId a non-empty string`);
80
+ }
81
+ return { armed: fact.armed, reason: fact.reason, breaker: { openedAtMs: b.openedAtMs, lastCause: b.lastCause, failures: b.failures, runId: b.runId } };
75
82
  }
76
83
  export function deriveWiringManifest(facts) {
77
84
  if (facts.half === "static" && facts.leg !== undefined) {
@@ -133,8 +140,9 @@ export function deriveWiringManifest(facts) {
133
140
  const { leg: _leg, mcp: _mcp, tools: _tools, ...assembly } = manifest;
134
141
  const { provenance: _askSeat, ...askForHash } = assembly.ask;
135
142
  const { provenance: _questionSeat, ...questionForHash } = assembly.question;
143
+ const { breaker: _breaker, ...autoModeForHash } = assembly.autoMode ?? {};
136
144
  manifest.configFingerprint = createHash("sha256")
137
- .update(canonicalize({ ...assembly, ask: askForHash, question: questionForHash }))
145
+ .update(canonicalize({ ...assembly, ask: askForHash, question: questionForHash, ...(assembly.autoMode !== undefined ? { autoMode: autoModeForHash } : {}) }))
138
146
  .digest("hex")
139
147
  .slice(0, 16);
140
148
  }
@@ -5,12 +5,31 @@ import type { TaskResult } from "./types.js";
5
5
  * longest unchanged PREFIX of these (keyed by the deterministic {@link workflowAgentCallKey}) and runs only the
6
6
  * first changed/new call + everything after it live. Same script + same args → 100% cache hit.
7
7
  */
8
- export interface WorkflowJournalEntry {
8
+ export type WorkflowJournalEntry = {
9
9
  /** The agent's deterministic call key (`ordinal:specIdentityHash`) — the replay match key. */
10
10
  callKey: string;
11
11
  /** The agent's TaskResult, replayed verbatim when a resume's call key matches at the same ordinal. */
12
12
  result: TaskResult;
13
- }
13
+ parked?: never;
14
+ } | {
15
+ callKey: string;
16
+ result?: never;
17
+ /**
18
+ * #642 — the ordinal's leg is PARKED at a durable approval gate: the child's own paused `TaskResult`
19
+ * (its `terminal` carries the checkpoint token + gate, its `sessionId` the pinned session). A resume
20
+ * reaching this ordinal with a matching call key does NOT run the call live (the parked child's
21
+ * session is pinned under a pending checkpoint; a fresh spawn would duplicate it) — it drives the
22
+ * parked child's resume when the caller hands it a decision for the token, else re-parks. Deliberately
23
+ * a DIFFERENT key from `result`: a journal reader from before this entry kind existed finds no
24
+ * `result.terminal` and refuses the whole resume (`WorkflowJournalIncompatibleError`) instead of
25
+ * admitting a paused cause it would re-run live — one-way, loud, like every journal schema step.
26
+ */
27
+ parked: TaskResult & {
28
+ terminal: Extract<TaskResult["terminal"], {
29
+ kind: "paused";
30
+ }>;
31
+ };
32
+ };
14
33
  /** REF-D5: the resume-claim key triple — ONE shape for the interface pair and every implementation
15
34
  * (it was re-inlined seven times; a key-field rename must red every leg at once). */
16
35
  export interface ResumeClaimArgs {
@@ -65,7 +65,7 @@ export class InMemoryWorkflowJournalStore {
65
65
  return [...rec.byOrdinal.entries()].sort((a, b) => a[0] - b[0]).map(([, e]) => snapshot(e));
66
66
  }
67
67
  async append(runId, scope, entry) {
68
- if (oversizeJournalResult(JSON.stringify(entry.result)))
68
+ if (oversizeJournalResult(JSON.stringify(entry.parked !== undefined ? entry.parked : entry.result)))
69
69
  return;
70
70
  let rec = this.runs.get(runId);
71
71
  if (!rec) {
@@ -79,6 +79,8 @@ export declare function openSpoolPair(base: string): {
79
79
  };
80
80
  export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCapability, SchedulerCapability {
81
81
  cwd: string;
82
+ /** #644 — this adapter runs on the engine host, so the host's home IS the environment's (`ExecutionEnv.homeDir`). */
83
+ readonly homeDir: string;
82
84
  private shellPath?;
83
85
  private shellEnv?;
84
86
  private inheritEnv;
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { closeSync, constants, createReadStream, mkdtempSync, openSync, readSync, statSync, truncateSync, unlinkSync } from "node:fs";
4
4
  import { access, appendFile, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, realpath, rename, rm, unlink, writeFile, } from "node:fs/promises";
5
- import { tmpdir } from "node:os";
5
+ import { homedir, tmpdir } from "node:os";
6
6
  import { isAbsolute, join, resolve } from "node:path";
7
7
  import { createInterface } from "node:readline";
8
8
  import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.js";
@@ -284,6 +284,7 @@ export function openSpoolPair(base) {
284
284
  }
285
285
  export class NodeExecutionEnv {
286
286
  cwd;
287
+ homeDir = homedir();
287
288
  shellPath;
288
289
  shellEnv;
289
290
  inheritEnv;
@@ -498,6 +498,17 @@ export interface ExecutionEnv extends FileSystem, Shell {
498
498
  * SSH env to an organization-managed build host can be non-isolated yet omit this flag.
499
499
  */
500
500
  readonly externalContentTarget?: boolean;
501
+ /**
502
+ * #644 — declared by the ADAPTER: the HOME DIRECTORY of the user this environment runs as, as an absolute
503
+ * path in the environment's own namespace. The base a `~/`-relative permission rule (`Edit(~/.ssh/**)`)
504
+ * resolves against for calls executing in this env. Omitted ⇒ the environment declares no home, and every
505
+ * `~/` deny/ask rule reads UNREADABLE for its calls (a fail-closed ask a person clears), because the only
506
+ * other candidate — the ENGINE PROCESS's home — is the wrong directory on any leg whose environment is not
507
+ * the engine host (a remote executor, a sandbox), and guarding the wrong directory silently was the
508
+ * defect. A local Node environment declares `os.homedir()` (it IS the host). A declared value that is not
509
+ * an absolute path is refused at prepare (`config.execution_env_home_dir_invalid`), never read as absent.
510
+ */
511
+ readonly homeDir?: string;
501
512
  }
502
513
  /** Base fields shared by append-only session tree entries. */
503
514
  export interface SessionTreeEntryBase {
package/dist/index.d.ts CHANGED
@@ -54,6 +54,7 @@ export { pgQuery, mysqlQuery, sqliteQuery } from "./tools/sql-adapters.js";
54
54
  export { materializeMcpTools, MCP_PREFIX, type MaterializedMcp, type McpServerStatus, type McpRefreshResult } from "./core/mcp.js";
55
55
  export { MCP_FAILURE_KINDS, MCP_DELIVERY_VERDICTS, classifyMcpFailure, type McpFailure, type McpFailureKind, type McpDelivered } from "./core/mcp-failure.js";
56
56
  export { MCP_INJECTION_DROP_REASONS, MCP_INJECTION_DROP_TEXT, mcpInjectionDroppedNotice, type McpInjectionDropReason, type McpInjectionDropFacts } from "./core/mcp-injection-drop.js";
57
+ export { askUnresolvableNotice, type AskUnresolvableFacts } from "./core/ask-unresolvable-notice.js";
57
58
  export { materializeA2aTools, A2aRpcError, type MaterializedA2a, type A2aPeerStatus, type A2aRefreshResult, type A2aToolAxis } from "./core/a2a.js";
58
59
  export { PROTOCOL_TABLE, MCP_NAMESPACE, A2A_NAMESPACE, protocolOf, type ProtocolNamespace, type ProtocolId } from "./core/protocol-table.js";
59
60
  export { InMemorySessionPolicyStore, SessionPolicyError, loosenReasons, normalizeRules, stripRev, type SessionPolicyStore, type SessionPermissionRules, type StoredSessionRules, type SessionRulesRecord, type PutRulesOptions, } from "./core/session-policy-store.js";
@@ -166,7 +167,8 @@ export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, create
166
167
  export { type AskOrigin, ASK_ORIGINS, isAskOrigin, classifierMayAnswer, ORIGIN_IMPLIES_REAL_APPROVAL } from "./core/ask-origin.js";
167
168
  export { SETTLEMENT_KINDS, type SettlementKind, isSettlementKind, type Settlement, SETTLEMENT_IS_REFUSAL, DENIED_BY_VALUES, type DeniedBy, isDeniedBy, DENIED_BY_MAY_VETO, type GateDisposition, type GateOutcome, screenGateOutcome } from "./core/gate-outcome.js";
168
169
  export { type AskCarry } from "./core/hooks.js";
169
- export { parseAutoModeResponse, createAutoModeDecider, type AutoModeVerdict, type AutoModeDecider, type AutoModeDeciderOptions, type AutoModeClassified, type AutoModeClassifyFn, type AutoModeClassifyInput, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow, type AutoModeDenialTracker, type AutoModeDenialLimitOptions, type DenialLimitCounts, type DenialLimitFallbackFace, type UnarmedDenialLimitFallback, type DenialLimitFallback, type DenialLimitVerdict, } from "./core/auto-mode.js";
170
+ export { SWAPPABLE_DEP_SEATS, SWAP_SEAT_NOTICE, isSwappableDepSeat, screenSwappableDeps, type SwappableDepSeat, type SwappableDeps } from "./core/swappable-deps.js";
171
+ export { parseAutoModeResponse, createAutoModeDecider, AUTO_MODE_UNAVAILABLE_CAUSES, isAutoModeUnavailableCause, AUTO_MODE_BREAKER_CAUSES, isAutoModeBreakerCause, AutoModeBreakerLedger, type AutoModeUnavailableCause, type AutoModeBreakerCause, type AutoModeBreakerTrip, type AutoModeVerdict, type AutoModeDecider, type AutoModeDeciderOptions, type AutoModeClassified, type AutoModeClassifyFn, type AutoModeClassifyInput, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow, type AutoModeDenialTracker, type AutoModeDenialLimitOptions, type DenialLimitCounts, type DenialLimitFallbackFace, type UnarmedDenialLimitFallback, type DenialLimitFallback, type DenialLimitVerdict, } from "./core/auto-mode.js";
170
172
  export { AUTO_MODE_DENIAL_LIMIT_DEFAULTS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_CLASSIFIER_MAX_TOKENS } from "./core/auto-mode-defaults.js";
171
173
  export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_MODE_DEFAULTS_SENTINEL, type AutoModeRules, type BuildAutoModePromptOptions, type AutoModeWindowOptions, } from "./core/auto-mode-prompt.js";
172
174
  export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
@@ -188,7 +190,7 @@ export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRul
188
190
  * and nothing more. Removal is exported without ceremony, because narrowing on a user's behalf is
189
191
  * allowed and widening is not.
190
192
  */
191
- export { RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, RULE_BEHAVIORS_BY_PRECEDENCE, isRuleBehavior, readRowBehavior, adjudicatePersistedRules, adjudicatePersistedPathRules, ruleReachesProgramRun, programRunReachOf, PROGRAM_RUN_REACHES, type ProgramRunReach, type ProgramRunReachOutcome, pathRuleReaches, ruleToolGrammarOf, COMMAND_RULE_TOOL, READ_RULE_TOOL, type PathRuleBases, type RuleBehavior, type PersistedRuleVerdict, parseRuleText, formatRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, renderUntrustedCommandText, stripFormatCharacters, UNCOVERED_SEGMENT_REASON_BASELINE, RULE_OFFERS_ABSENCE_BASELINE, type UncoveredSegmentDetail, type EditedRuleBreadthWarning, directoryRuleAdmits, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, type PersistedRule, type RuleTombstone, type RuleScope, type RuleDot, type RuleAdd, type RuleAddOrigin, type RuleOffer, type RuleOfferBatchMember, type SegmentRuleSuggestion, type SegmentCoverage, type RuleReject, type RuleRejectCode, type ParsedRule, type PersistedRuleTool, type PersistedRuleMatch, } from "./core/permission-rule-model.js";
193
+ export { RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, RULE_BEHAVIORS_BY_PRECEDENCE, isRuleBehavior, readRowBehavior, adjudicatePersistedRules, adjudicatePersistedPathRules, ruleReachesProgramRun, programRunReachOf, PROGRAM_RUN_REACHES, type ProgramRunReach, type ProgramRunReachOutcome, pathRuleReachOf, ruleBasesNeeded, PATH_RULE_BASES, PATH_RULE_BASE_LABEL, ruleToolGrammarOf, COMMAND_RULE_TOOL, READ_RULE_TOOL, type PathRuleBase, type PathRuleBases, type RuleBehavior, type PersistedRuleVerdict, parseRuleText, formatRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, renderUntrustedCommandText, stripFormatCharacters, UNCOVERED_SEGMENT_REASON_BASELINE, RULE_OFFERS_ABSENCE_BASELINE, type UncoveredSegmentDetail, type EditedRuleBreadthWarning, directoryRuleAdmits, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, type PersistedRule, type RuleTombstone, type RuleScope, type RuleDot, type RuleAdd, type RuleAddOrigin, type RuleOffer, type RuleOfferBatchMember, type SegmentRuleSuggestion, type SegmentCoverage, type RuleReject, type RuleRejectCode, type ParsedRule, type PersistedRuleTool, type PersistedRuleMatch, } from "./core/permission-rule-model.js";
192
194
  export { removePersistedRule, applyTombstones, sameScope, sameRuleIdentity, isValidConsentScope, isValidDurableScope, InMemoryDurableRulePartition, EMPTY_DURABLE_RULE_PARTITION, type DurableRulePartition, type DurableRulePartitionProvider, type RuleWriteOutcome, type StoredRules, type RemoveResult, type PutResult, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, type RuleSyncState, type RuleSyncFrontier, type RuleSyncDrop, type RuleSyncLandingReport, type RuleOwner, type QuarantinedRuleAdd, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, type PermissionRuleWriter, type WritableDurableRulePartition, type RuleWriteDelta, type RuleAddDelta, type RuleDeleteDelta, type RuleSyncJoinDelta, type RawRuleSyncState, type RedemptionAuthorization, } from "./core/permission-rule-store.js";
193
195
  export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH, LOCAL_OWNER_UNSYNCABLE_CODE, type PermissionRuleSyncTransport, type PermissionRuleSyncResult, type RuleSyncRequestBody, type RuleSyncResponseBody, } from "./core/permission-rule-sync.js";
194
196
  export { createPermissionRuleStoreProvider, effectivePermissionRules, effectiveOrThrow, ruleSourceOf, type PermissionRuleStore, type PermissionRuleStoreProvider, type PermissionRuleStoreConfig, type EffectivePermissionRules, type EffectivePermissionRule, type RemovedPermissionRule, type RuleSource, } from "./core/permission-rule-provider.js";
@@ -228,7 +230,7 @@ export { type ReasoningIntensity, type ReasoningResolution, type ResolvedReasoni
228
230
  export { DESIGN_REVIEW_PROMPTS, CODE_REVIEW_PROMPT, SCENARIO_REGISTRY, runScenario, type ScenarioId, type CodeReviewMode, type ScenarioProfile, type RunScenarioOptions, type RunScenarioResult, } from "./scenarios/scenario-registry.js";
229
231
  export { teacherMode, TEACHER_PROFILE, type TeacherModePair, type TeacherProfile, } from "./scenarios/teacher-quickstart.js";
230
232
  export { loadOrchestrationEnv, DEFAULT_REASONING_INTENSITY, type OrchestrationMode, type OrchestrationEnv, } from "./scenarios/env.js";
231
- export { runWorkflow, startWorkflow, workflowAgentCallKey, WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowAgentBlockedError, WORKFLOW_SPAWN_BLOCKED_ERROR_CODE, type WorkflowFanOutSlotError, type WorkflowFanOutOptions, WORKFLOW_SUBAGENT_PROMPT, WORKFLOW_SUBAGENT_PROMPT_SCHEMA, WORKFLOW_SUBAGENT_APPEND, WORKFLOW_SUBAGENT_APPEND_SCHEMA, type WorkflowHandle, MAX_WORKFLOW_ITEMS, type WorkflowRun, type WorkflowRunStatus, type WorkflowItemStatus, type WorkflowPhase, type WorkflowGroup, type WorkflowAgentRun, type WorkflowAgentHandle, type WorkflowRunStats, type WorkflowEvent, type WorkflowBudget, type WorkflowAgentOptions, type WorkflowRunContext, type WorkflowInternals, type RunWorkflowOptions, type RunWorkflowResult, type WorkflowTimers, } from "./orchestration/workflow.js";
233
+ export { runWorkflow, startWorkflow, workflowAgentCallKey, WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowAgentBlockedError, WORKFLOW_SPAWN_BLOCKED_ERROR_CODE, WorkflowAgentParkedError, WORKFLOW_AGENT_PARKED_ERROR_CODE, type WorkflowParkedResume, type WorkflowFanOutSlotError, type WorkflowFanOutOptions, WORKFLOW_SUBAGENT_PROMPT, WORKFLOW_SUBAGENT_PROMPT_SCHEMA, WORKFLOW_SUBAGENT_APPEND, WORKFLOW_SUBAGENT_APPEND_SCHEMA, type WorkflowHandle, MAX_WORKFLOW_ITEMS, type WorkflowRun, type WorkflowRunStatus, type WorkflowItemStatus, type WorkflowPhase, type WorkflowGroup, type WorkflowAgentRun, type WorkflowAgentHandle, type WorkflowRunStats, type WorkflowEvent, type WorkflowBudget, type WorkflowAgentOptions, type WorkflowRunContext, type WorkflowInternals, type RunWorkflowOptions, type RunWorkflowResult, type WorkflowTimers, } from "./orchestration/workflow.js";
232
234
  export { listWorkflowRuns, getWorkflowRun, subscribeWorkflow, deriveAgentDisplayStatus, type AgentDisplayStatus } from "./orchestration/workflow-observe.js";
233
235
  export { runGoal, DECLARE_DONE_TOOL_NAME, type GoalSpec, type GoalResult, type GoalStatus, type GoalBudgetCause, type GoalVerdict, type GoalTurnState, type GoalVerificationKind, } from "./orchestration/goal.js";
234
236
  export { emitTaskOutcome, type TaskOutcome } from "./core/task-outcome.js";
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ export { pgQuery, mysqlQuery, sqliteQuery } from "./tools/sql-adapters.js";
41
41
  export { materializeMcpTools, MCP_PREFIX } from "./core/mcp.js";
42
42
  export { MCP_FAILURE_KINDS, MCP_DELIVERY_VERDICTS, classifyMcpFailure } from "./core/mcp-failure.js";
43
43
  export { MCP_INJECTION_DROP_REASONS, MCP_INJECTION_DROP_TEXT, mcpInjectionDroppedNotice } from "./core/mcp-injection-drop.js";
44
+ export { askUnresolvableNotice } from "./core/ask-unresolvable-notice.js";
44
45
  export { materializeA2aTools, A2aRpcError } from "./core/a2a.js";
45
46
  export { PROTOCOL_TABLE, MCP_NAMESPACE, A2A_NAMESPACE, protocolOf } from "./core/protocol-table.js";
46
47
  export { InMemorySessionPolicyStore, SessionPolicyError, loosenReasons, normalizeRules, stripRev, } from "./core/session-policy-store.js";
@@ -140,14 +141,15 @@ export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, create
140
141
  export { ASK_ORIGINS, isAskOrigin, classifierMayAnswer, ORIGIN_IMPLIES_REAL_APPROVAL } from "./core/ask-origin.js";
141
142
  export { SETTLEMENT_KINDS, isSettlementKind, SETTLEMENT_IS_REFUSAL, DENIED_BY_VALUES, isDeniedBy, DENIED_BY_MAY_VETO, screenGateOutcome } from "./core/gate-outcome.js";
142
143
  export {} from "./core/hooks.js";
143
- export { parseAutoModeResponse, createAutoModeDecider, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow, } from "./core/auto-mode.js";
144
+ export { SWAPPABLE_DEP_SEATS, SWAP_SEAT_NOTICE, isSwappableDepSeat, screenSwappableDeps } from "./core/swappable-deps.js";
145
+ export { parseAutoModeResponse, createAutoModeDecider, AUTO_MODE_UNAVAILABLE_CAUSES, isAutoModeUnavailableCause, AUTO_MODE_BREAKER_CAUSES, isAutoModeBreakerCause, AutoModeBreakerLedger, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow, } from "./core/auto-mode.js";
144
146
  export { AUTO_MODE_DENIAL_LIMIT_DEFAULTS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_CLASSIFIER_MAX_TOKENS } from "./core/auto-mode-defaults.js";
145
147
  export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_MODE_DEFAULTS_SENTINEL, } from "./core/auto-mode-prompt.js";
146
148
  export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
147
149
  export { AUTO_MODE_ARMING_RECIPE_VERSION, autoModeArmingRecipeOf, sanitizeAutoModeArmingRecipe, foldAutoModeArming, } from "./core/auto-mode-arming.js";
148
150
  export { rebuildAutoModeDecider, } from "./core/auto-mode-rebuild.js";
149
151
  export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, isNamespacedCoveringRuleName, namespacedRuleNameCovers, } from "./core/permission-rules.js";
150
- export { RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, RULE_BEHAVIORS_BY_PRECEDENCE, isRuleBehavior, readRowBehavior, adjudicatePersistedRules, adjudicatePersistedPathRules, ruleReachesProgramRun, programRunReachOf, PROGRAM_RUN_REACHES, pathRuleReaches, ruleToolGrammarOf, COMMAND_RULE_TOOL, READ_RULE_TOOL, parseRuleText, formatRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, renderUntrustedCommandText, stripFormatCharacters, UNCOVERED_SEGMENT_REASON_BASELINE, RULE_OFFERS_ABSENCE_BASELINE, directoryRuleAdmits, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, } from "./core/permission-rule-model.js";
152
+ export { RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, RULE_BEHAVIORS_BY_PRECEDENCE, isRuleBehavior, readRowBehavior, adjudicatePersistedRules, adjudicatePersistedPathRules, ruleReachesProgramRun, programRunReachOf, PROGRAM_RUN_REACHES, pathRuleReachOf, ruleBasesNeeded, PATH_RULE_BASES, PATH_RULE_BASE_LABEL, ruleToolGrammarOf, COMMAND_RULE_TOOL, READ_RULE_TOOL, parseRuleText, formatRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, renderUntrustedCommandText, stripFormatCharacters, UNCOVERED_SEGMENT_REASON_BASELINE, RULE_OFFERS_ABSENCE_BASELINE, directoryRuleAdmits, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, } from "./core/permission-rule-model.js";
151
153
  export { removePersistedRule, applyTombstones, sameScope, sameRuleIdentity, isValidConsentScope, isValidDurableScope, InMemoryDurableRulePartition, EMPTY_DURABLE_RULE_PARTITION, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, } from "./core/permission-rule-store.js";
152
154
  export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH, LOCAL_OWNER_UNSYNCABLE_CODE, } from "./core/permission-rule-sync.js";
153
155
  export { createPermissionRuleStoreProvider, effectivePermissionRules, effectiveOrThrow, ruleSourceOf, } from "./core/permission-rule-provider.js";
@@ -186,7 +188,7 @@ export { DEFAULT_EFFORT_LEVELS, REASONING_BUDGET_SHARE, isThinkingLevel, rankOf,
186
188
  export { DESIGN_REVIEW_PROMPTS, CODE_REVIEW_PROMPT, SCENARIO_REGISTRY, runScenario, } from "./scenarios/scenario-registry.js";
187
189
  export { teacherMode, TEACHER_PROFILE, } from "./scenarios/teacher-quickstart.js";
188
190
  export { loadOrchestrationEnv, DEFAULT_REASONING_INTENSITY, } from "./scenarios/env.js";
189
- export { runWorkflow, startWorkflow, workflowAgentCallKey, WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowAgentBlockedError, WORKFLOW_SPAWN_BLOCKED_ERROR_CODE, WORKFLOW_SUBAGENT_PROMPT, WORKFLOW_SUBAGENT_PROMPT_SCHEMA, WORKFLOW_SUBAGENT_APPEND, WORKFLOW_SUBAGENT_APPEND_SCHEMA, MAX_WORKFLOW_ITEMS, } from "./orchestration/workflow.js";
191
+ export { runWorkflow, startWorkflow, workflowAgentCallKey, WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowAgentBlockedError, WORKFLOW_SPAWN_BLOCKED_ERROR_CODE, WorkflowAgentParkedError, WORKFLOW_AGENT_PARKED_ERROR_CODE, WORKFLOW_SUBAGENT_PROMPT, WORKFLOW_SUBAGENT_PROMPT_SCHEMA, WORKFLOW_SUBAGENT_APPEND, WORKFLOW_SUBAGENT_APPEND_SCHEMA, MAX_WORKFLOW_ITEMS, } from "./orchestration/workflow.js";
190
192
  export { listWorkflowRuns, getWorkflowRun, subscribeWorkflow, deriveAgentDisplayStatus } from "./orchestration/workflow-observe.js";
191
193
  export { runGoal, DECLARE_DONE_TOOL_NAME, } from "./orchestration/goal.js";
192
194
  export { emitTaskOutcome } from "./core/task-outcome.js";
@@ -307,6 +307,23 @@ export interface RunWorkflowToolDeps {
307
307
  * Absent on both seats ⇒ byte-identical baseline: a child's asks keep the pre-#342 resolution
308
308
  * (`RunnerDeps.onAsk`, else the fail-closed headless auto-deny). */
309
309
  parentOnAsk?: import("../core/tool-policy.js").OnAsk;
310
+ /** #642 — the HOST run's `durableApproval` opt-in (value copy; the Runner mints it from `spec.durableApproval`
311
+ * beside `parentOnAsk`, the same seat the delegation tool reads as `ctx.durableApprovalForChildren`). Folded
312
+ * into `RunWorkflowOptions.defaultDurableApproval` at execute — ONLY when a `store` is wired (the wa* row's
313
+ * durable home; without it the child keeps the pre-#642 lifecycle: its unavailable ask denies fail-closed
314
+ * and the `delegation.ask_unresolvable` notice says so). `ctx.durableApprovalForChildren` wins when a
315
+ * wrapping path provides it; this dep covers the auto-mounted tool's minimal execute ctx, like `parentOnAsk`.
316
+ * Absent on both seats ⇒ byte-identical: a workflow child's ask keeps the host-seat resolution alone. */
317
+ parentDurableApproval?: {
318
+ scope: string;
319
+ ttlMs?: number;
320
+ };
321
+ /** #642 — the decisions a host holds for PARKED wa* rows of a prior run, read at execute when the model
322
+ * re-invokes with `resumeFromRunId` (a call-time getter over the run id: the decisions arrive on the host's
323
+ * trusted run channel, `RunInternals.workflowParkedResume`, and only the ones naming THIS resume's source run
324
+ * apply). Threaded into `RunWorkflowOptions.parkedResume`: the resume drives the parked child on with the
325
+ * decision instead of re-parking. Never a tool argument — a model cannot decide an approval. */
326
+ parkedResume?: (resumeFromRunId: string) => ReadonlyArray<import("./workflow.js").WorkflowParkedResume> | undefined;
310
327
  /** The HOST run's display sink (its `RunInternals.onForwardEvent` behind the runner's ctx wrapper:
311
328
  * `task_progress` always, plus the children's content events — `text_delta`/`text_end`/
312
329
  * `reasoning_delta`/`tool_start`/`tool_end`, UNTRUSTED-RAW: the consumer must redact — when the HOST spec set
@@ -5,6 +5,7 @@ import { redactSecrets, redactHostLeaks, boundedRedactedSummary } from "../core/
5
5
  import { withDelegationProvenance } from "../core/tool-policy.js";
6
6
  import { LAUNCH_RECEIPT_OWN_WORDS_CLAUSE, launchReceiptNoQuoteClause } from "../agents/launch-receipt-contract.js";
7
7
  import { startWorkflow } from "./workflow.js";
8
+ import { WORKFLOW_AGENT_PARKED_ERROR_CODE } from "./workflow-types.js";
8
9
  import { buildWorkflowPrimitives } from "./workflow-primitives.js";
9
10
  import { parseWorkflowMeta, splitWorkflowMeta, workflowScriptReadsClockOrRandom } from "./workflow-meta.js";
10
11
  import { mergeWorkflowArgs, normalizeStringArg } from "./workflow-script-store.js";
@@ -51,6 +52,7 @@ function workflowUsageBlock(run) {
51
52
  agent_count: agents.length,
52
53
  agents_done: agents.filter((a) => a.status === "completed").length,
53
54
  agents_error: agents.filter((a) => a.status === "failed").length,
55
+ ...(agents.some((a) => a.status === "parked") ? { agents_parked: agents.filter((a) => a.status === "parked").length } : {}),
54
56
  agents_empty_result: agents.filter((a) => a.status === "completed" && (a.output === undefined || a.output === "")).length,
55
57
  agents_replayed: agents.filter((a) => a.replayed === true).length,
56
58
  ...(run.journalSkips !== undefined && run.journalSkips > 0 ? { journal_skipped: run.journalSkips } : {}),
@@ -76,6 +78,7 @@ function workflowDiagnostics(runId, journalRef, resumeMiss) {
76
78
  }
77
79
  const SCRIPT_ERROR_CODES = new Set([
78
80
  "workflow.script_error",
81
+ "workflow.parked_call_changed",
79
82
  "workflow.nesting",
80
83
  "workflow.model_not_allowed",
81
84
  "workflow.agent_schema",
@@ -86,6 +89,12 @@ const SCRIPT_ERROR_CODES = new Set([
86
89
  function failureSummary(err, runId) {
87
90
  const e = err;
88
91
  const code = typeof e?.code === "string" ? e.code : undefined;
92
+ if (code === WORKFLOW_AGENT_PARKED_ERROR_CODE) {
93
+ const msg = typeof e?.message === "string" ? redactSecrets(e.message) : "";
94
+ return (`workflow parked (${code}): ${msg.length > 200 ? `${msg.slice(0, 200)}…` : msg} — it did not fail and is not finished` +
95
+ (runId ? `; per-agent rows via TaskOutput("${runId}") (the parked row reads status "parked")` : "") +
96
+ `; do not re-issue the same call — once the approval is decided, the deployment resumes this run${runId ? ` (resumeFromRunId: "${runId}")` : ""} and the script continues from the parked call`);
97
+ }
89
98
  const guide = runId
90
99
  ? `; per-agent rows via TaskOutput("${runId}"); after fixing, re-invoke with resumeFromRunId: "${runId}" (completed agents replay from the journal)`
91
100
  : "";
@@ -465,6 +474,7 @@ export async function createRunWorkflowTool(d) {
465
474
  return structuredError(`workflow script failed to compile: ${err instanceof Error ? err.message : String(err)}`);
466
475
  }
467
476
  const hostOnAsk = ctx.onAsk ?? d.parentOnAsk;
477
+ const hostDurableApproval = ctx.durableApprovalForChildren ?? d.parentDurableApproval;
468
478
  const runGovernance = hostOnAsk !== undefined && governance.baseline.base.onAsk === undefined
469
479
  ? {
470
480
  ...governance,
@@ -537,6 +547,8 @@ export async function createRunWorkflowTool(d) {
537
547
  })(),
538
548
  ...(d.parentModel !== undefined ? { defaultModel: d.parentModel } : {}),
539
549
  ...(d.parentGetApiKeyAndHeaders !== undefined ? { defaultGetApiKeyAndHeaders: d.parentGetApiKeyAndHeaders } : {}),
550
+ ...(hostDurableApproval !== undefined && d.store !== undefined ? { defaultDurableApproval: { ...hostDurableApproval } } : {}),
551
+ ...(resumeFromRunId !== undefined && d.parkedResume !== undefined && d.parkedResume(resumeFromRunId) !== undefined ? { parkedResume: d.parkedResume(resumeFromRunId) } : {}),
540
552
  ...(parentCenterArtifactDigest !== undefined ? { parentCenterArtifactDigest } : {}),
541
553
  ...(parentCenterSourceRevision !== undefined ? { parentCenterSourceRevision } : {}),
542
554
  ...((ctx.forwardEvent ?? d.forwardEvent) !== undefined ? { onForwardEvent: (ctx.forwardEvent ?? d.forwardEvent) } : {}),
@@ -56,7 +56,7 @@ export declare function listWorkflowRuns(store: WorkflowRunStore, scope: string,
56
56
  */
57
57
  export declare function getWorkflowRun(store: WorkflowRunStore, id: string, scope: string): Promise<WorkflowRun | null>;
58
58
  /** design/99 MF-W display status. */
59
- export type AgentDisplayStatus = "queued" | "running" | "done" | "failed" | "interrupted";
59
+ export type AgentDisplayStatus = "queued" | "running" | "done" | "failed" | "interrupted" | "parked";
60
60
  /**
61
61
  * design/99 MF-W (design-review DoR ⑤+⑥): the SHARED, anti-drift projection of a workflow agent's record-level
62
62
  * status → a CC-style display status, derived PURELY from the persisted record (the agent's `status` + `startedAt`
@@ -66,6 +66,8 @@ export function deriveAgentDisplayStatus(agent, runStatus) {
66
66
  return "done";
67
67
  if (agent.status === "failed")
68
68
  return "failed";
69
+ if (agent.status === "parked")
70
+ return "parked";
69
71
  if (runStatus !== "running")
70
72
  return "interrupted";
71
73
  return agent.startedAt === undefined ? "queued" : "running";