@sema-agent/core 7.6.2 → 7.7.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 (47) hide show
  1. package/CHANGELOG.md +36 -6
  2. package/dist/agents/peer-admission.d.ts +1 -1
  3. package/dist/brain/anthropic.js +8 -2
  4. package/dist/brain/open-responses.js +5 -3
  5. package/dist/brain/openai.js +31 -8
  6. package/dist/brain/reasoning.d.ts +32 -0
  7. package/dist/brain/reasoning.js +18 -0
  8. package/dist/core/auto-mode-defaults.d.ts +16 -0
  9. package/dist/core/auto-mode-defaults.js +1 -0
  10. package/dist/core/auto-mode.d.ts +19 -0
  11. package/dist/core/auto-mode.js +74 -56
  12. package/dist/core/checkpoint-execution-record.d.ts +110 -0
  13. package/dist/core/checkpoint-execution-record.js +49 -0
  14. package/dist/core/checkpoint-store.d.ts +88 -10
  15. package/dist/core/checkpoint-store.js +35 -2
  16. package/dist/core/engine-notice.d.ts +11 -0
  17. package/dist/core/governance-codes.d.ts +1 -1
  18. package/dist/core/governance-codes.js +2 -0
  19. package/dist/core/runner/clock-and-limits.d.ts +117 -0
  20. package/dist/core/runner/clock-and-limits.js +118 -0
  21. package/dist/core/runner/contracts.d.ts +10 -0
  22. package/dist/core/runner/decide-continuation.d.ts +98 -0
  23. package/dist/core/runner/decide-continuation.js +133 -0
  24. package/dist/core/runner/execution-record.d.ts +26 -0
  25. package/dist/core/runner/execution-record.js +19 -0
  26. package/dist/core/runner/git-leg-delivery.d.ts +28 -0
  27. package/dist/core/runner/git-leg-delivery.js +94 -0
  28. package/dist/core/runner/initial-run-state.d.ts +14 -0
  29. package/dist/core/runner/initial-run-state.js +11 -0
  30. package/dist/core/runner/prepare-caps-and-workflow.js +17 -0
  31. package/dist/core/runner/prepare-run-refs.d.ts +0 -12
  32. package/dist/core/runner/prepare-run-refs.js +0 -5
  33. package/dist/core/runner/runtask.d.ts +0 -68
  34. package/dist/core/runner/runtask.js +28 -450
  35. package/dist/core/runner/steer-admission.d.ts +17 -0
  36. package/dist/core/runner/steer-admission.js +17 -0
  37. package/dist/core/runner/tool-end-body.d.ts +71 -0
  38. package/dist/core/runner/tool-end-body.js +74 -0
  39. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -0
  40. package/dist/core/store-contracts/checkpoint-store-contract.js +85 -0
  41. package/dist/core/trace.d.ts +24 -0
  42. package/dist/index.d.ts +5 -4
  43. package/dist/index.js +4 -3
  44. package/dist/stores/file/checkpoint-store.d.ts +7 -0
  45. package/dist/stores/file/checkpoint-store.js +20 -1
  46. package/package.json +1 -1
  47. package/test/export-surface.snapshot.json +37 -1
@@ -1,3 +1,5 @@
1
+ import type { GateOutcome } from "./gate-outcome.js";
2
+ import { type ExecutionOutcomeRecordWord } from "./checkpoint-execution-record.js";
1
3
  import type { CheckpointPause } from "./pause-registry.js";
2
4
  import type { SystemInjectionPriority } from "./task-notification.js";
3
5
  import { type QuestionAnswer } from "./ask-question.js";
@@ -1062,7 +1064,7 @@ export interface CheckpointState {
1062
1064
  activeWorktree?: ActiveWorktreeSession;
1063
1065
  /**
1064
1066
  * Parent effective-policy inheritance (tighten-only): the durable projection of the run's
1065
- * {@link import("./runner/prepare-task.js").InheritedGate}. Only the serializable DATA half persists —
1067
+ * {@link import("./runner/contracts.js").InheritedGate}. Only the serializable DATA half persists —
1066
1068
  * `ancestorRules` (each ancestor's session-rule snapshot: sessionId/principal/rev/rules, re-compiled on
1067
1069
  * resume against the resumed leg's env) and `shellGate` (the chain's max-rank shell doctrine). The OPAQUE
1068
1070
  * half (`parentConstraints` — live ToolPolicy/onAsk closures) CANNOT persist; `requiresParentConstraint`
@@ -1202,7 +1204,7 @@ export interface CheckpointState {
1202
1204
  /**
1203
1205
  * #281 件B follow-up — `true` iff the suspended leg ran as a DELEGATED CHILD (the trusted
1204
1206
  * `RunInternals.isDelegatedChild` fact, read off the leg's own minted
1205
- * {@link import("./runner/prepare-task.js").Prepared.hookIdentity} so a re-suspend chain carries it
1207
+ * {@link import("./runner/contracts.js").Prepared.hookIdentity} so a re-suspend chain carries it
1206
1208
  * forward). Without it a deps-only resume (`resume(token, outcome, config)` — no trusted internals
1207
1209
  * re-supplied) re-minted the resumed leg's identity with `isDelegatedChild: false`, so the
1208
1210
  * delegation-lifecycle observer's emission gate stayed shut and the resumed leg emitted NEITHER
@@ -1743,6 +1745,26 @@ export interface CheckpointRow {
1743
1745
  scope: string;
1744
1746
  ttlMs?: number;
1745
1747
  };
1748
+ /**
1749
+ * @contract checkpoint.execution_outcome — the settlement record of how the DECIDED action was
1750
+ * finally disposed on the resumed leg: the {@link GateOutcome} the resumed call's `tool_end` frame
1751
+ * carried (an executed allow, a person's refusal delivered, or a post-CAS belt's veto of a person's
1752
+ * yes), written by {@link CheckpointStore.recordExecutionOutcome} exactly once per row, on every
1753
+ * resolution (an `allowed` disposition is recorded too — the column's absence has ONE meaning:
1754
+ * the execution outcome is not known, never "it was allowed"). Independent of
1755
+ * {@link resolvedOutcome}, which stays the immutable record of the DECISION: a row can read
1756
+ * `resolvedOutcome.gateOutcome.disposition.kind === "allowed"` beside
1757
+ * `executionOutcome.disposition = { kind: "denied", deniedBy: "policy" }` — the person approved,
1758
+ * the deployment's belt refused the edited call — and a reader rebuilding the run from the row
1759
+ * sees both facts instead of the pre-execution one alone. The record is physically immutable:
1760
+ * a second write of a different record is a conflict, never an overwrite, and a row carrying one
1761
+ * cannot be {@link CheckpointStore.reopen | reopened} (the action is settled; a reopen would
1762
+ * re-run or re-decide work whose outcome is already on file). Read through
1763
+ * `executionVerdict` (checkpoint-execution-record.ts). Present together with {@link executionAtMs}.
1764
+ */
1765
+ executionOutcome?: GateOutcome;
1766
+ /** Epoch ms the store stamped {@link executionOutcome} at. Present iff `executionOutcome` is. */
1767
+ executionAtMs?: number;
1746
1768
  }
1747
1769
  /**
1748
1770
  * design/80 assistant-scheduler seam #1: a LIGHTWEIGHT projection of one PENDING
@@ -1967,6 +1989,10 @@ export declare class CheckpointError extends Error {
1967
1989
  * checkpoint exists. Surfaced instead of silently treating the failed reopen as committed, so the
1968
1990
  * caller's compensation path settles the token as terminally consumed. */
1969
1991
  | "checkpoint.reopen_failed"
1992
+ /** {@link CheckpointStore.recordExecutionOutcome} met a row that already carries a DIFFERENT
1993
+ * execution record — see `ExecutionOutcomeConflictError` (checkpoint-execution-record.ts), the
1994
+ * in-tree form of this code. Never a retry condition. */
1995
+ | "checkpoint.execution_outcome_conflict"
1970
1996
  /** design/80 D-A: {@link CheckpointStore.setPendingSteer} was handed `text` containing a
1971
1997
  * `</system-reminder>` close tag — it would escape the model-facing `<system-reminder>` wrapper a
1972
1998
  * trusted steer rides on resume. Rejected fail-closed so a dirty steer NEVER enters {@link CheckpointState}
@@ -2093,13 +2119,11 @@ export declare class CheckpointError extends Error {
2093
2119
  * additive and optional like `field` — `code` remains the only REQUIRED discriminant.
2094
2120
  * · `unsupported_version` arms: `"version_newer"` (retryable on a newer worker),
2095
2121
  * `"env_factory_missing"` (retryable on a factory-wired worker), `"governed_unwired"`
2096
- * (retryable on an org-wired worker), `"origin_missing"` (a pending approval row minted
2097
- * before the ask's origin was persisted on the park — terminal for the row's bytes; the host
2098
- * decides the orphaned card itself, no worker version redeems it).
2122
+ * (retryable on an org-wired worker).
2099
2123
  * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
2100
2124
  * `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
2101
2125
  * current bytes — no worker version redeems a damaged row). */
2102
- reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
2126
+ reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
2103
2127
  } | undefined;
2104
2128
  constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
2105
2129
  /** `runner.resume` was handed an {@link ResumeOutcome} whose `gate` arm does not match the
@@ -2151,6 +2175,10 @@ export declare class CheckpointError extends Error {
2151
2175
  * checkpoint exists. Surfaced instead of silently treating the failed reopen as committed, so the
2152
2176
  * caller's compensation path settles the token as terminally consumed. */
2153
2177
  | "checkpoint.reopen_failed"
2178
+ /** {@link CheckpointStore.recordExecutionOutcome} met a row that already carries a DIFFERENT
2179
+ * execution record — see `ExecutionOutcomeConflictError` (checkpoint-execution-record.ts), the
2180
+ * in-tree form of this code. Never a retry condition. */
2181
+ | "checkpoint.execution_outcome_conflict"
2154
2182
  /** design/80 D-A: {@link CheckpointStore.setPendingSteer} was handed `text` containing a
2155
2183
  * `</system-reminder>` close tag — it would escape the model-facing `<system-reminder>` wrapper a
2156
2184
  * trusted steer rides on resume. Rejected fail-closed so a dirty steer NEVER enters {@link CheckpointState}
@@ -2277,13 +2305,11 @@ export declare class CheckpointError extends Error {
2277
2305
  * additive and optional like `field` — `code` remains the only REQUIRED discriminant.
2278
2306
  * · `unsupported_version` arms: `"version_newer"` (retryable on a newer worker),
2279
2307
  * `"env_factory_missing"` (retryable on a factory-wired worker), `"governed_unwired"`
2280
- * (retryable on an org-wired worker), `"origin_missing"` (a pending approval row minted
2281
- * before the ask's origin was persisted on the park — terminal for the row's bytes; the host
2282
- * decides the orphaned card itself, no worker version redeems it).
2308
+ * (retryable on an org-wired worker).
2283
2309
  * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
2284
2310
  * `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
2285
2311
  * current bytes — no worker version redeems a damaged row). */
2286
- reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
2312
+ reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
2287
2313
  } | undefined);
2288
2314
  }
2289
2315
  /**
@@ -2456,6 +2482,20 @@ export interface CheckpointStore {
2456
2482
  reopen: boolean;
2457
2483
  validatingLease?: boolean;
2458
2484
  };
2485
+ /**
2486
+ * The EXECUTION-OUTCOME declaration — REQUIRED, and required to be exactly `{ outcome: true }`: the
2487
+ * store promises that {@link recordExecutionOutcome} is the real CAS this contract describes (the
2488
+ * predicate, the four words, the conflict throw) and that {@link reopen} honours the record. Same
2489
+ * "declaration, never duck-typing" posture as the three optional axes above, but not optional: the
2490
+ * durable resume lane records how every decided action was disposed, and a store that cannot keep
2491
+ * that record would leave every resolved row it holds silently unreadable on that axis. A store
2492
+ * without the declaration is refused at the FIRST read of the store seat
2493
+ * ({@link resolveCheckpointStore}, code `config.invalid_checkpoint_store`) — a JavaScript caller that
2494
+ * satisfies no type is told at prepare what the type would have told it.
2495
+ */
2496
+ readonly execution: {
2497
+ readonly outcome: true;
2498
+ };
2459
2499
  /** Create-once. Throws {@link CheckpointError} `already_exists` on a token collision (never a silent
2460
2500
  * overwrite — a reused token would clobber a live suspension). */
2461
2501
  put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
@@ -2505,8 +2545,35 @@ export interface CheckpointStore {
2505
2545
  * `tool_unavailable` (P-7: the approved tool vanished, the action may now be invalid → a human re-decides,
2506
2546
  * so a FRESH decision is allowed). The store RECORDS the reason on the row as {@link Checkpoint.reopenReason}
2507
2547
  * and PRESERVES the persisted winner across the reopen (never clears it).
2548
+ *
2549
+ * **A row that carries an execution record is NOT reopenable** (`… AND execution_outcome IS NULL` in the
2550
+ * WHERE; `checkpointExecutionRecorded` is the shared predicate): the decided action's disposition is on
2551
+ * file — see {@link CheckpointRow.executionOutcome} — and reopening it would either re-run an action that
2552
+ * already ran or re-decide one already refused. Answers `false` like every other predicate miss; the
2553
+ * reasons and the winner are untouched.
2508
2554
  */
2509
2555
  reopen?(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
2556
+ /**
2557
+ * Record how the DECIDED action was finally disposed on the resumed leg — the {@link GateOutcome} its
2558
+ * `tool_end` frame carried (see {@link CheckpointRow.executionOutcome} for what the record means and why
2559
+ * it is a column of its own beside the immutable decision). REQUIRED (declared by {@link execution}).
2560
+ *
2561
+ * The CAS: `UPDATE … SET execution_outcome=?, execution_at_ms=? WHERE token=? AND scope=? AND
2562
+ * status='resolved' AND execution_outcome IS NULL`. The answer is a word of the closed
2563
+ * `ExecutionOutcomeRecordWord` set — `recorded` (the CAS won), `already_recorded` (the row already
2564
+ * carries an EQUAL record: a retry, idempotent), `not_resolved` (pending/expired: no decision to be the
2565
+ * outcome of), `absent` (no such row in this scope). A row carrying a DIFFERENT record throws
2566
+ * `ExecutionOutcomeConflictError` (`checkpoint.execution_outcome_conflict`) — the record is immutable
2567
+ * and two accounts of one execution are an engine defect, never a store overwrite. Equality is
2568
+ * structural over the record's JSON (key order and freezing are not part of the value —
2569
+ * `sameExecutionRecord`); every backend takes its disposition from `executionRecordDisposition`
2570
+ * (checkpoint-execution-record.ts) and keeps only the commit half itself.
2571
+ *
2572
+ * Never touches `status`, `rev`, `resolvedOutcome` or `reopenReason`: the record is a fact about the
2573
+ * execution, not a transition of the decision. Its one effect on the row's other verbs is that
2574
+ * {@link reopen} refuses a recorded row.
2575
+ */
2576
+ recordExecutionOutcome(token: CheckpointToken, scope: string, gate: GateOutcome): Promise<ExecutionOutcomeRecordWord>;
2510
2577
  /**
2511
2578
  * design/80 D-A: park a durable mid-task STEER on a DURABLY-SUSPENDED task's checkpoint. CAS-style update
2512
2579
  * `SET state.pendingSteerQueue=? WHERE token=? AND scope=? AND status='pending'`: returns `true` iff the
@@ -2692,6 +2759,12 @@ export declare function resolveCheckpointStore(spec: {
2692
2759
  }, deps: {
2693
2760
  checkpointStore?: CheckpointStore;
2694
2761
  }): CheckpointStore | undefined;
2762
+ /** The ONE refusal of a store seat whose {@link CheckpointStore} does not declare `execution = { outcome: true }`
2763
+ * (named by seat and by the store's constructor when it has one). Same door, same code, as the retired-`null`
2764
+ * refusal above: a configuration the run cannot honour is refused at the first read, never carried. */
2765
+ export declare function checkpointStoreExecutionUndeclared(seat: "TaskSpec.checkpointStore" | "RunnerDeps.checkpointStore", store: object): Error & {
2766
+ code: "config.invalid_checkpoint_store";
2767
+ };
2695
2768
  /** The ONE refusal of the retired `checkpointStore: null` spelling (the resolver and the workflow baseline overlay both throw it). */
2696
2769
  export declare function retiredCheckpointStoreNull(): Error & {
2697
2770
  code: "config.invalid_checkpoint_store";
@@ -2803,6 +2876,10 @@ export declare class InMemoryCheckpointStore implements CheckpointStore {
2803
2876
  readonly redecision: {
2804
2877
  readonly reopen: true;
2805
2878
  };
2879
+ /** The execution-outcome record verb below is the contract's CAS, and `reopen` honours the record. */
2880
+ readonly execution: {
2881
+ readonly outcome: true;
2882
+ };
2806
2883
  private cps;
2807
2884
  private fault;
2808
2885
  put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
@@ -2855,6 +2932,7 @@ export declare class InMemoryCheckpointStore implements CheckpointStore {
2855
2932
  * atomic unit this method exists to provide. (A wrapper that OMITS the member opts out
2856
2933
  * cleanly: the consumer's presence probe then keeps the two-step path, hooks intact.) */
2857
2934
  claimTerminal(token: CheckpointToken, scope: string, intent: TerminalClaimIntent): Promise<TerminalClaimOutcome>;
2935
+ recordExecutionOutcome(token: CheckpointToken, scope: string, gate: GateOutcome): Promise<ExecutionOutcomeRecordWord>;
2858
2936
  reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
2859
2937
  setPendingSteer(token: CheckpointToken, scope: string, steer: PendingSteerInput): Promise<boolean>;
2860
2938
  expire(token: CheckpointToken, scope: string): Promise<boolean>;
@@ -4,6 +4,7 @@ import { PROBE_CAUSE_PATH_MAX, inlineUntrusted, sanitizeUntrustedText } from "./
4
4
  import { carriesBidiControls } from "./tool-policy.js";
5
5
  import { readDenialLimitFallback } from "./auto-mode.js";
6
6
  import { isAskOrigin } from "./ask-origin.js";
7
+ import { checkpointExecutionRecorded, executionRecordDisposition, ExecutionOutcomeConflictError, } from "./checkpoint-execution-record.js";
7
8
  import { renderUntrustedCommandText, stripFormatCharacters } from "./permission-rule-model.js";
8
9
  import { redactSecrets } from "./untrusted-egress.js";
9
10
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
@@ -367,7 +368,26 @@ export function resolveCheckpointStore(spec, deps) {
367
368
  throw retiredCheckpointStoreNull();
368
369
  if (spec.checkpointStore === "disabled")
369
370
  return undefined;
370
- return spec.checkpointStore ?? deps.checkpointStore;
371
+ if (spec.checkpointStore !== undefined) {
372
+ if (spec.checkpointStore.execution?.outcome !== true)
373
+ throw checkpointStoreExecutionUndeclared("TaskSpec.checkpointStore", spec.checkpointStore);
374
+ return spec.checkpointStore;
375
+ }
376
+ if (deps.checkpointStore !== undefined && deps.checkpointStore.execution?.outcome !== true) {
377
+ throw checkpointStoreExecutionUndeclared("RunnerDeps.checkpointStore", deps.checkpointStore);
378
+ }
379
+ return deps.checkpointStore;
380
+ }
381
+ export function checkpointStoreExecutionUndeclared(seat, store) {
382
+ const ctor = store.constructor?.name;
383
+ const name = typeof ctor === "string" && ctor.length > 0 && ctor !== "Object" ? ` (${ctor})` : "";
384
+ const e = new Error(`${seat}: the wired CheckpointStore${name} does not declare \`execution = { outcome: true }\` — the execution-outcome ` +
385
+ "record (recordExecutionOutcome, and reopen refusing a recorded row) is a required member of the store contract; " +
386
+ "a store without it cannot record how a resumed approval was disposed, so the run is refused here rather than " +
387
+ "resolving rows no reader can settle. Implement the verb and add the declaration (the in-tree stores and the " +
388
+ "store-contract kit are the reference).");
389
+ e.code = "config.invalid_checkpoint_store";
390
+ return e;
371
391
  }
372
392
  export function retiredCheckpointStoreNull() {
373
393
  const e = new Error('TaskSpec.checkpointStore: null is not a value any more — write checkpointStore: "disabled" to run without a ' +
@@ -540,6 +560,7 @@ export class InMemoryCheckpointStore {
540
560
  durability = "process-local";
541
561
  fidelity = "structured-clone";
542
562
  redecision = { reopen: true };
563
+ execution = { outcome: true };
543
564
  cps = new Map();
544
565
  fault = null;
545
566
  async put(token, cp) {
@@ -618,9 +639,21 @@ export class InMemoryCheckpointStore {
618
639
  }
619
640
  return out;
620
641
  }
642
+ async recordExecutionOutcome(token, scope, gate) {
643
+ const cp = this.cps.get(token);
644
+ const disposition = executionRecordDisposition(cp, scope, gate);
645
+ if (disposition === "conflict")
646
+ throw new ExecutionOutcomeConflictError(cp.executionOutcome, gate);
647
+ if (disposition !== "commit")
648
+ return disposition;
649
+ const record = structuredClone(gate);
650
+ cp.executionOutcome = record;
651
+ cp.executionAtMs = Date.now();
652
+ return "recorded";
653
+ }
621
654
  async reopen(token, scope, reason) {
622
655
  const cp = this.cps.get(token);
623
- if (!checkpointRowMatches(cp, scope, "resolved")) {
656
+ if (!checkpointRowMatches(cp, scope, "resolved") || checkpointExecutionRecorded(cp)) {
624
657
  return false;
625
658
  }
626
659
  cp.status = "pending";
@@ -211,6 +211,17 @@ export interface EngineNotice {
211
211
  * (whoever answered the card is the one entitled to hear the answer ran nothing); a host may
212
212
  * forward it on its own wire.
213
213
  *
214
+ * - `"checkpoint.execution_outcome_unrecorded"` — the resumed leg's pending-call resolver disposed
215
+ * the decided action (executed it, delivered the refusal, or vetoed the person's yes) and filed
216
+ * the frame's record through `CheckpointStore.recordExecutionOutcome`, and the store answered
217
+ * `not_resolved` or `absent` — the row was reopened, expired or removed under the leg by another
218
+ * actor, so the row now reads `unknown` on its execution axis (`executionVerdict`). The
219
+ * execution result is NOT changed by the answer (the record is an account of what ran, never a
220
+ * gate on it); the notice is the loud half. Once per such resolution;
221
+ * `detail: { sessionId, runId, scope, checkpointId?, word }` (`word` = the store's answer; the
222
+ * secret token never rides a notice). A DIFFERENT record already on file is not this notice — it
223
+ * is the `checkpoint.execution_outcome_conflict` throw (an engine defect, propagated).
224
+ *
214
225
  * - `"classifier.denial_limit"` (#548, CC 2.1.250 `FO`/"too many classifier denials in headless
215
226
  * mode") — the auto-mode classifier's DENIAL LIMIT was reached (3 consecutive blocks, or 20 in the
216
227
  * run; `RunnerDeps.autoMode.denialLimit`) and the fallback ask it turns into had NO approver to go
@@ -103,7 +103,7 @@ export type NoticeAudience = "user" | "operator";
103
103
  * src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
104
104
  * no longer minted.
105
105
  */
106
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
106
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
107
107
  /** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
108
108
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
109
109
  * sink is a supported shape, and narrowing that field would break it. */
@@ -108,6 +108,7 @@ export const ENGINE_NOTICE_CODES = [
108
108
  "peer.held_settled",
109
109
  "peer.idle_subscription",
110
110
  "classifier.denial_limit",
111
+ "checkpoint.execution_outcome_unrecorded",
111
112
  "delegation.transcript_integrity",
112
113
  "mcp.revocation_probe_failed",
113
114
  "workflow.governance_key_stripped",
@@ -169,6 +170,7 @@ const NOTICE_AUDIENCE_TABLE = {
169
170
  "peer.held_settled": "user",
170
171
  "peer.idle_subscription": "user",
171
172
  "delegation.transcript_integrity": "operator",
173
+ "checkpoint.execution_outcome_unrecorded": "operator",
172
174
  "mcp.revocation_probe_failed": "operator",
173
175
  "workflow.governance_key_stripped": "operator",
174
176
  "workflow.agent_option_ignored": "operator",
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The run loop's CLOCK and LIMIT machinery — the pieces that turn a budget, a deadline or a ceiling into
3
+ * something a lane can act on, shared by the turn boundary, the run body and the end-of-task lane:
4
+ * · the limit-approach frames (budget-measured, advisory) and the turn-cap read;
5
+ * · the governance-read budget against the environment's suspend deadline, the chunked absolute-deadline
6
+ * race that enforces it, and the never-abandoned ledger charge with its slow-settle disclosure;
7
+ * · the typed terminal for an external stop cause that could not be suspended durably;
8
+ * · the once-per-task disclosure of an auto-compaction trigger that cannot be evaluated (a WeakSet keyed
9
+ * on the task's `Prepared`, so the latch dies with the task and never touches the contract surface);
10
+ * · the hard walltime backstop timer, chunked past the largest delay a timer honours.
11
+ *
12
+ * Every clock read is `Date.now()` or a timer; every host read is through the arguments a caller hands in.
13
+ * Layer-1 machinery rather than a driven lane — initial-run-state.ts says why no run-loop machinery module
14
+ * wears a family prefix.
15
+ */
16
+ import { type AgentHarness } from "../../internal/harness.js";
17
+ import type { PlatformLimitReason } from "../checkpoint-store.js";
18
+ import type { TaskSpec } from "../types.js";
19
+ import type { Prepared } from "./contracts.js";
20
+ /**
21
+ * design/164 — the LIMIT-APPROACH frames, the successor to the retired deadline-nudge family. Two facts
22
+ * make them a different mechanism, not a rename:
23
+ * - they are measured in BUDGET (the tightest armed axis's fill ratio), not in wall-clock, so they need
24
+ * no throughput estimate and cannot mis-fire on a starved event loop;
25
+ * - they are ADVISORY. Neither frame stops anything; the run ends when a ceiling is actually crossed.
26
+ *
27
+ * Rendered by {@link limitApproachFrames}; registered in the prompt-assembly event registry under
28
+ * `limit_approach_converge` / `limit_approach_deliver`.
29
+ */
30
+ export declare const LIMIT_APPROACH_DEFAULT_THRESHOLDS: readonly [number, number];
31
+ /** The output-token figure the pre-call worst-case assumes when the model declares no cap of its own.
32
+ * Shared by the $ and token arms so the two gates cannot disagree about the same hypothetical call. */
33
+ export declare const DEFAULT_PRECALL_OUTPUT_TOKENS = 4096;
34
+ /** The two frame bodies, indexed by how many have already been sent (0 → converge, 1 → deliver now). */
35
+ export declare function limitApproachFrames(index: 0 | 1, axis: string, percent: number, mark: string | undefined): string;
36
+ /**
37
+ * Resolve the effective turn cap. `undefined` = no turn cap; `0` (explicit) = unbounded.
38
+ *
39
+ * design/164: this is now a pure read of what the caller wrote. The two clauses it used to carry —
40
+ * "a wall-clock bound was set, so drop the turn cap" and "neither was set, so apply the engine's
41
+ * safety net" — are both gone: the axes no longer interact (each binds on its own), and there is no
42
+ * engine-side net to apply. A task with no `maxTurns` has no turn ceiling.
43
+ */
44
+ export declare function resolveMaxTurns(limits?: {
45
+ maxTurns?: number;
46
+ }): number | undefined;
47
+ /**
48
+ * How long past the environment's suspend deadline the turn-boundary lane will keep waiting for the
49
+ * usage ledger's READ.
50
+ *
51
+ * The precedence ruling (usage_window > env_lifetime) means the ledger must be read BEFORE the env
52
+ * cause can be adopted — you cannot rank a cause you have not read. But that read is store I/O with no
53
+ * bound of its own, and past this instant every millisecond it spends is spent out of the margin that
54
+ * exists to mint the env checkpoint. A stalled ledger must not cost a deployment the checkpoint it
55
+ * could still have had, so the read runs against an ABSOLUTE deadline (`envLifetimeSuspendAt + this`)
56
+ * and a run that gets no answer by then stops for the cause it can still prove.
57
+ *
58
+ * 5s against the 60s suspend margin: two orders of magnitude above any healthy ledger read (in-memory
59
+ * and file stores answer in ms; a remote one in tens of ms), and it still leaves 55s — more than the
60
+ * margin was sized for — to pause the workspace, mint the checkpoint and commit it. Not a knob: it is a
61
+ * fraction of an engine constant, and a deployment that wants a wider band declares a shorter lifetime.
62
+ *
63
+ * ABSOLUTE, never a fresh per-boundary allowance: a boundary that finds the deadline one millisecond
64
+ * away must not hand a stalled ledger the whole margin, and a boundary that is already overdue must get
65
+ * LESS than the budget, not the same. {@link raceUntilDeadline} is what keeps that true for a deadline
66
+ * of any distance, without an unrepresentable timer.
67
+ */
68
+ export declare const ENV_DUE_GOVERNANCE_READ_BUDGET_MS = 5000;
69
+ export declare const GOVERNANCE_READ_STALLED: unique symbol;
70
+ /** Await a ledger charge, firing `onSlow` ONCE if it has not settled after
71
+ * {@link CHARGE_SETTLE_DISCLOSE_MS}. The charge itself is always awaited to completion. */
72
+ export declare function awaitChargeWithSlowDisclosure<T>(charge: Promise<T>, onSlow: () => void, discloseAfterMs?: number): Promise<T>;
73
+ /**
74
+ * Await `p` until the absolute epoch `deadline`, then give up with {@link GOVERNANCE_READ_STALLED}.
75
+ *
76
+ * Chunked, and deliberately so: the deadline is derived from a caller-declared environment lifetime, so
77
+ * the distance to it can exceed {@link MAX_TIMER_DELAY_MS} — a single timer armed for that distance
78
+ * would fire at once and abandon a perfectly healthy read. Each chunk re-reads the wall clock, so the
79
+ * decision is always made against the deadline itself rather than against an allowance computed once.
80
+ *
81
+ * Exported for its own unit pin (the same posture as the brain-call guardrail primitive): the
82
+ * behaviors below are clock behaviors, and only a virtual clock can assert them without spending the
83
+ * wall-clock time they describe. Not re-exported from the package index.
84
+ *
85
+ * An already-overdue deadline still gets ONE zero-delay pass: a promise that is settled (or settles in a
86
+ * microtask, which is every in-process store) must be allowed to win, because giving up on an answer we
87
+ * already hold would be a fabricated stall. The second pass is what makes the loop terminate.
88
+ */
89
+ export declare function raceUntilDeadline<T>(p: Promise<T>, deadline: number): Promise<T | typeof GOVERNANCE_READ_STALLED>;
90
+ /**
91
+ * design/164 件四/件五 — the TYPED terminal for an external stop cause that could NOT be suspended durably.
92
+ * Carried on `rs.limits.platformTerminal` and adopted as the run's `threw`, so the caller reads the cause
93
+ * on `TaskResult.errorCode` (`errorCodeOf` lifts `.code`) instead of the anonymous error an abort produces.
94
+ * `retryAfterMs` rides the error object because a wait hint has no `TaskResult` seat — a host reads it from
95
+ * the `RunnerDeps.onError` sink this is also delivered to.
96
+ */
97
+ export declare function platformLimitTerminal(reason: PlatformLimitReason, retryAfterMs: number | undefined, moment?: "entry" | "turn_boundary" | "resume"): Error & {
98
+ code: string;
99
+ retryAfterMs?: number;
100
+ };
101
+ export declare function discloseUnevaluableWindow(u: {
102
+ estTokens: number;
103
+ windowField: string;
104
+ windowValue?: number;
105
+ modelId?: string;
106
+ }, prepared: Prepared, tracer: TaskSpec["tracer"], taskId: string, onError: ((err: unknown, context: {
107
+ phase: "config";
108
+ sessionId: string;
109
+ }) => void) | undefined): void;
110
+ /** 1.296 件2a — report the hard-abort timer's lateness on `task.end` only above this bar: sub-5s
111
+ * jitter is ordinary scheduling noise (GC pauses, sync work) and would only add trace lint. */
112
+ export declare const TIMER_LATENESS_REPORT_MS = 5000;
113
+ export declare function startTimeout(harness: AgentHarness, abortController: AbortController, remainingMs?: number, softSuspendable?: boolean): {
114
+ fired: boolean;
115
+ latenessMs?: number;
116
+ clear: () => void;
117
+ };
@@ -0,0 +1,118 @@
1
+ import {} from "../../internal/harness.js";
2
+ import { openSystemReminder } from "../reminder-mint.js";
3
+ import { emitTrace } from "../trace.js";
4
+ export const LIMIT_APPROACH_DEFAULT_THRESHOLDS = [0.8, 0.95];
5
+ export const DEFAULT_PRECALL_OUTPUT_TOKENS = 4096;
6
+ export function limitApproachFrames(index, axis, percent, mark) {
7
+ return index === 0
8
+ ? `${openSystemReminder(mark)}[limit approach] This task has used about ${percent}% of its ${axis}. ` +
9
+ `Start converging: finish the step you are on, drop work that is not required for the deliverable, and ` +
10
+ `avoid starting anything you cannot complete within what is left.</system-reminder>`
11
+ : `${openSystemReminder(mark)}[limit approach] About ${percent}% of the ${axis} is gone. Deliver now: ` +
12
+ `stop exploring, write up what you have — including what is incomplete and what you would do next — ` +
13
+ `and end the task. Work that is not reported before the allowance runs out is lost.</system-reminder>`;
14
+ }
15
+ export function resolveMaxTurns(limits) {
16
+ return limits?.maxTurns;
17
+ }
18
+ export const ENV_DUE_GOVERNANCE_READ_BUDGET_MS = 5_000;
19
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
20
+ const DEADLINE_TICK = Symbol("deadline-tick");
21
+ export const GOVERNANCE_READ_STALLED = Symbol("governance-read-stalled");
22
+ const CHARGE_SETTLE_DISCLOSE_MS = 10_000;
23
+ export async function awaitChargeWithSlowDisclosure(charge, onSlow, discloseAfterMs = CHARGE_SETTLE_DISCLOSE_MS) {
24
+ let timer = setTimeout(() => {
25
+ timer = undefined;
26
+ try {
27
+ onSlow();
28
+ }
29
+ catch {
30
+ }
31
+ }, discloseAfterMs);
32
+ try {
33
+ return await charge;
34
+ }
35
+ finally {
36
+ if (timer !== undefined)
37
+ clearTimeout(timer);
38
+ }
39
+ }
40
+ export async function raceUntilDeadline(p, deadline) {
41
+ for (let firstPass = true;; firstPass = false) {
42
+ const remaining = deadline - Date.now();
43
+ if (remaining <= 0 && !firstPass)
44
+ return GOVERNANCE_READ_STALLED;
45
+ let timer;
46
+ const tick = new Promise((res) => {
47
+ timer = setTimeout(() => res(DEADLINE_TICK), Math.max(0, Math.min(remaining, MAX_TIMER_DELAY_MS)));
48
+ });
49
+ const out = await Promise.race([p, tick]).finally(() => clearTimeout(timer));
50
+ if (out !== DEADLINE_TICK)
51
+ return out;
52
+ }
53
+ }
54
+ export function platformLimitTerminal(reason, retryAfterMs, moment = "turn_boundary") {
55
+ const message = moment === "entry"
56
+ ? `a deployment usage window is exhausted (RunnerDeps.usageWindows), so the task was refused before its first model call — nothing ran and nothing was spent. There is no checkpoint to suspend into at this point, so the caller re-submits after the window frees, in ${String(retryAfterMs ?? 0)}ms.`
57
+ : moment === "resume"
58
+ ? `a deployment usage window is exhausted (RunnerDeps.usageWindows), so the resumed leg was stopped BEFORE its first model call — nothing was delivered or executed on this leg. The window frees in ${String(retryAfterMs ?? 0)}ms.`
59
+ : reason === "env_lifetime"
60
+ ? "the execution environment's declared lifetime is expiring and the run could not be suspended durably (not eligible, or the suspend was refused — see remoteEnvFailures / onError). It was stopped at a clean turn boundary instead of being killed mid-turn when the platform reclaims the environment."
61
+ : `a deployment usage window is exhausted (RunnerDeps.usageWindows) and the run could not be suspended durably, so it was stopped at a clean turn boundary. The window frees in ${String(retryAfterMs ?? 0)}ms.`;
62
+ const e = new Error(message);
63
+ e.code = reason === "env_lifetime" ? "env.lifetime_expired" : "usage.window_exhausted";
64
+ if (retryAfterMs !== undefined)
65
+ e.retryAfterMs = retryAfterMs;
66
+ return e;
67
+ }
68
+ const disclosedUnevaluableWindow = new WeakSet();
69
+ export function discloseUnevaluableWindow(u, prepared, tracer, taskId, onError) {
70
+ if (disclosedUnevaluableWindow.has(prepared))
71
+ return;
72
+ disclosedUnevaluableWindow.add(prepared);
73
+ emitTrace(tracer, () => ({ kind: "compaction.unevaluable", version: 1, taskId, estTokens: u.estTokens, ts: Date.now() }));
74
+ const modelClause = u.modelId !== undefined ? ` "${u.modelId}"` : "";
75
+ const cause = u.windowValue !== undefined
76
+ ? `its \`${u.windowField}\` is ${u.windowValue}, which is not a positive number of tokens — that field takes precedence over the ` +
77
+ `others, so fixing or removing it is what restores the threshold`
78
+ : `it declares no context window at all (\`autoCompactTokens\` / \`contextTokens\` / \`contextWindow\` are all absent) — declare one to restore the threshold`;
79
+ try {
80
+ onError?.(new Error(`auto-compaction cannot run for model${modelClause}: ${cause}. Until then there is no threshold to compare against, so the ` +
81
+ `context will grow until the provider refuses the request. The conversation is already about ${u.estTokens} estimated tokens.`), { phase: "config", sessionId: prepared.sessionId });
82
+ }
83
+ catch {
84
+ }
85
+ }
86
+ const WALLTIME_SUSPEND_GRACE_SEC = 120;
87
+ export const TIMER_LATENESS_REPORT_MS = 5_000;
88
+ export function startTimeout(harness, abortController, remainingMs, softSuspendable = false) {
89
+ const state = { fired: false, clear: () => { } };
90
+ if (remainingMs !== undefined) {
91
+ const hardMs = Math.max(0, remainingMs + (softSuspendable ? WALLTIME_SUSPEND_GRACE_SEC * 1000 : 0));
92
+ const scheduledAtMs = Date.now() + hardMs;
93
+ let leftMs = hardMs;
94
+ let timer;
95
+ const armChunk = () => {
96
+ const chunk = Math.max(0, Math.min(leftMs, MAX_TIMER_DELAY_MS));
97
+ timer = setTimeout(() => {
98
+ leftMs -= chunk;
99
+ if (leftMs > 0) {
100
+ armChunk();
101
+ return;
102
+ }
103
+ fireHardAbort();
104
+ }, chunk);
105
+ };
106
+ const fireHardAbort = () => {
107
+ if (state.fired)
108
+ return;
109
+ state.fired = true;
110
+ state.latenessMs = Math.max(0, Date.now() - scheduledAtMs);
111
+ abortController.abort();
112
+ void harness.abort();
113
+ };
114
+ armChunk();
115
+ state.clear = () => clearTimeout(timer);
116
+ }
117
+ return state;
118
+ }
@@ -27,6 +27,7 @@ import type { AutoModeArmingRecipe } from "../auto-mode-arming.js";
27
27
  import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
28
28
  import type { CacheBreakDetector, ToolFingerprintInput } from "../cache-break-detector.js";
29
29
  import type { ActorAssertion, Checkpoint, CheckpointState, CheckpointStore, CheckpointToken, PendingSteerEntry, PlatformLimitReason, ReopenReason, ResourceLedger, ResourceLimitReason, ResumeOutcome, SerializedCheckpointState } from "../checkpoint-store.js";
30
+ import type { ExecutionOutcomeRecordWord } from "../checkpoint-execution-record.js";
30
31
  import type { ClearedProjectionLedger, ContextEditMachine, OccurrenceIndex } from "../context-edit.js";
31
32
  import type { HookInvocationIdentity, Hooks, OrgGateVerdict, PermissionDeniedPayload, PostToolBatchCall, PostToolBatchResult, ToolGateInput } from "../hooks.js";
32
33
  import type { MaterializedMcp } from "../mcp.js";
@@ -2196,6 +2197,15 @@ export interface ResumeRun {
2196
2197
  * replay the persisted winner (a system retry of the approved action), while a `tool_unavailable` reopen
2197
2198
  * lets a human re-decide with the tool present (a fresh decision is allowed — preserves P-7). */
2198
2199
  onEnvRestoreFailed?: (reason: ReopenReason) => Promise<void>;
2200
+ /** The row's execution-record verb, bound to THIS resume's token and scope by `resumeStream` (the
2201
+ * same closure form as {@link onEnvRestoreFailed}, but REQUIRED: the store contract requires the
2202
+ * verb, so there is no store this seat could be absent for). The pending-call resolver calls it
2203
+ * exactly once per resolution, right after it emits the resolved call's `tool_end` frame, with the
2204
+ * {@link import("../gate-outcome.js").GateOutcome} that frame carried — every disposition, the
2205
+ * executed allow included (see `CheckpointRow.executionOutcome`: absence means unknown, never
2206
+ * allowed). The word it answers is read through `EXECUTION_RECORD_LEAVES_ROW_UNRECORDED`; a
2207
+ * conflict throw propagates (an engine defect, not a store condition). */
2208
+ recordExecutionOutcome: (gate: import("../gate-outcome.js").GateOutcome) => Promise<ExecutionOutcomeRecordWord>;
2199
2209
  /** RB-471/FR-C1 — set in the run body right after `applyResumeDecision` completes: the negative-
2200
2210
  * decision twin of `pendingActionStarted`. A reject/deny consumes its gate BY BEING DELIVERED, and
2201
2211
  * this bit is the delivery fact — every throw-arm cause (prepare failure, `session_not_found`, a