@sema-agent/core 7.6.3 → 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.
- package/CHANGELOG.md +33 -10
- package/dist/agents/peer-admission.d.ts +1 -1
- package/dist/brain/anthropic.js +8 -2
- package/dist/brain/open-responses.js +5 -3
- package/dist/brain/openai.js +31 -8
- package/dist/brain/reasoning.d.ts +32 -0
- package/dist/brain/reasoning.js +18 -0
- package/dist/core/auto-mode-defaults.d.ts +16 -0
- package/dist/core/auto-mode-defaults.js +1 -0
- package/dist/core/auto-mode.d.ts +19 -0
- package/dist/core/auto-mode.js +74 -56
- package/dist/core/checkpoint-execution-record.d.ts +110 -0
- package/dist/core/checkpoint-execution-record.js +49 -0
- package/dist/core/checkpoint-store.d.ts +88 -10
- package/dist/core/checkpoint-store.js +35 -2
- package/dist/core/engine-notice.d.ts +11 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/runner/clock-and-limits.d.ts +117 -0
- package/dist/core/runner/clock-and-limits.js +118 -0
- package/dist/core/runner/contracts.d.ts +10 -0
- package/dist/core/runner/decide-continuation.d.ts +98 -0
- package/dist/core/runner/decide-continuation.js +133 -0
- package/dist/core/runner/execution-record.d.ts +26 -0
- package/dist/core/runner/execution-record.js +19 -0
- package/dist/core/runner/git-leg-delivery.d.ts +28 -0
- package/dist/core/runner/git-leg-delivery.js +94 -0
- package/dist/core/runner/initial-run-state.d.ts +14 -0
- package/dist/core/runner/initial-run-state.js +11 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +17 -0
- package/dist/core/runner/prepare-run-refs.d.ts +0 -16
- package/dist/core/runner/prepare-run-refs.js +0 -6
- package/dist/core/runner/runtask.d.ts +0 -68
- package/dist/core/runner/runtask.js +28 -452
- package/dist/core/runner/steer-admission.d.ts +17 -0
- package/dist/core/runner/steer-admission.js +17 -0
- package/dist/core/runner/tool-end-body.d.ts +71 -0
- package/dist/core/runner/tool-end-body.js +74 -0
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +85 -0
- package/dist/core/trace.d.ts +24 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/stores/file/checkpoint-store.d.ts +7 -0
- package/dist/stores/file/checkpoint-store.js +20 -1
- package/package.json +1 -1
- 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/
|
|
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/
|
|
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)
|
|
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" | "
|
|
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)
|
|
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" | "
|
|
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
|
-
|
|
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
|