@sema-agent/core 7.6.3 → 7.8.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 +70 -10
- package/dist/agents/agent-transcript-tool.js +2 -2
- package/dist/agents/list-agents-tool.js +2 -3
- package/dist/agents/observer.js +2 -2
- package/dist/agents/peer-admission.d.ts +1 -1
- package/dist/agents/send-message-tool.js +2 -1
- package/dist/agents/subagent-steps.js +5 -4
- package/dist/agents/subagent.js +2 -3
- 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/ask-question.js +2 -2
- 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 +114 -2
- package/dist/core/checkpoint-store.js +35 -2
- package/dist/core/context-edit.d.ts +0 -22
- package/dist/core/context-edit.js +2 -11
- package/dist/core/engine-notice.d.ts +17 -0
- package/dist/core/fs-write-gate-policy.d.ts +7 -1
- package/dist/core/fs-write-gate-policy.js +7 -7
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/gate-lanes.js +4 -3
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +8 -0
- package/dist/core/hooks.d.ts +8 -2
- package/dist/core/hooks.js +1 -1
- package/dist/core/lsp.js +2 -2
- package/dist/core/mcp-server-spec.d.ts +17 -0
- package/dist/core/mcp.js +3 -1
- package/dist/core/memory-engine/tools.js +4 -12
- package/dist/core/permission-rules.d.ts +32 -2
- package/dist/core/permission-rules.js +43 -37
- package/dist/core/present-plan-tool.js +3 -4
- package/dist/core/runner/active-skill-scope.js +7 -4
- 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/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/contracts.d.ts +37 -3
- 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-ask-lane.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
- package/dist/core/runner/prepare-caps-and-workflow.js +33 -11
- package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
- package/dist/core/runner/prepare-defer-classify.js +10 -10
- package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
- package/dist/core/runner/prepare-delegation-surface.js +15 -17
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
- package/dist/core/runner/prepare-gate-stations.js +3 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
- package/dist/core/runner/prepare-hands-readface.js +3 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-lsp.d.ts +4 -3
- package/dist/core/runner/prepare-lsp.js +3 -2
- package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
- package/dist/core/runner/prepare-memory-engine-session.js +4 -2
- package/dist/core/runner/prepare-offload-wrappers.js +7 -5
- package/dist/core/runner/prepare-park-ask.d.ts +4 -0
- package/dist/core/runner/prepare-park-ask.js +4 -1
- package/dist/core/runner/prepare-project-context.d.ts +5 -23
- package/dist/core/runner/prepare-project-context.js +7 -5
- package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
- package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
- package/dist/core/runner/prepare-protocol-tools.js +38 -21
- package/dist/core/runner/prepare-question-face.d.ts +4 -3
- package/dist/core/runner/prepare-question-face.js +5 -3
- package/dist/core/runner/prepare-safety-scan.js +42 -2
- package/dist/core/runner/prepare-task.js +37 -19
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
- package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
- package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
- package/dist/core/runner/prepare-wiring-manifest.js +3 -8
- package/dist/core/runner/run-harness-handlers.d.ts +46 -0
- package/dist/core/runner/run-harness-handlers.js +345 -0
- package/dist/core/runner/run-turn-boundary.d.ts +69 -0
- package/dist/core/runner/run-turn-boundary.js +694 -0
- package/dist/core/runner/runtask.d.ts +0 -68
- package/dist/core/runner/runtask.js +66 -1486
- package/dist/core/runner/session-rule-policy.js +9 -4
- package/dist/core/runner/steer-admission.d.ts +17 -0
- package/dist/core/runner/steer-admission.js +17 -0
- package/dist/core/runner/synthetic-tools.js +5 -8
- package/dist/core/runner/tool-disclosure.d.ts +4 -0
- package/dist/core/runner/tool-disclosure.js +9 -5
- package/dist/core/runner/tool-end-body.d.ts +71 -0
- package/dist/core/runner/tool-end-body.js +75 -0
- package/dist/core/runner/tool-face-overlay.d.ts +40 -0
- package/dist/core/runner/tool-face-overlay.js +125 -0
- package/dist/core/runner/tool-output-projection.d.ts +10 -0
- package/dist/core/runner/tool-output-projection.js +33 -10
- package/dist/core/sensitive-path-policy.d.ts +3 -0
- package/dist/core/sensitive-path-policy.js +6 -5
- package/dist/core/shared-memory/tools.js +3 -6
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +108 -0
- package/dist/core/task-event.d.ts +10 -0
- package/dist/core/task-registry.js +4 -5
- package/dist/core/task-spec.d.ts +8 -0
- package/dist/core/tool-catalog-entries.d.ts +48 -0
- package/dist/core/tool-catalog-entries.js +174 -0
- package/dist/core/tool-conformance.d.ts +22 -0
- package/dist/core/tool-conformance.js +37 -0
- package/dist/core/tool-face.d.ts +171 -0
- package/dist/core/tool-face.js +25 -0
- package/dist/core/tool-policy.d.ts +24 -0
- package/dist/core/tool-policy.js +4 -2
- package/dist/core/tool-registry.d.ts +114 -0
- package/dist/core/tool-registry.js +114 -0
- package/dist/core/tool-result-budget.d.ts +0 -3
- package/dist/core/tool-result-budget.js +4 -3
- package/dist/core/tool-result-store.d.ts +0 -18
- package/dist/core/tool-result-store.js +2 -18
- package/dist/core/tool-roster.d.ts +450 -0
- package/dist/core/tool-roster.js +497 -0
- package/dist/core/tool-spec.d.ts +54 -3
- package/dist/core/tools.js +7 -0
- package/dist/core/trace.d.ts +24 -0
- package/dist/core/wiring-manifest.d.ts +13 -0
- package/dist/core/wiring-manifest.js +2 -1
- package/dist/core/write-protect.d.ts +2 -1
- package/dist/core/write-protect.js +5 -4
- package/dist/engine/compaction/utils.d.ts +6 -0
- package/dist/engine/compaction/utils.js +19 -19
- package/dist/engine/loop/types.d.ts +6 -0
- package/dist/index.d.ts +20 -8
- package/dist/index.js +19 -7
- package/dist/orchestration/goal.js +2 -1
- package/dist/orchestration/run-spec.js +3 -5
- package/dist/orchestration/run-workflow-tool.js +2 -2
- package/dist/prompt-assembly/event-registry.js +3 -3
- package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
- package/dist/stores/file/checkpoint-store.d.ts +7 -0
- package/dist/stores/file/checkpoint-store.js +20 -1
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +7 -11
- package/dist/tools/fs/fs-read.js +2 -2
- package/dist/tools/fs/fs-search-tools.d.ts +0 -7
- package/dist/tools/fs/fs-search-tools.js +3 -16
- package/dist/tools/fs/fs-write.js +4 -6
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/safety.d.ts +2 -1
- package/dist/tools/fs/safety.js +6 -5
- package/dist/tools/monitor.js +2 -2
- package/dist/tools/scheduler-tools.js +5 -8
- package/dist/tools/task-list.js +5 -8
- package/dist/tools/todo.js +2 -2
- package/dist/tools/web.js +3 -6
- package/dist/tools/worktree.js +3 -4
- package/package.json +4 -1
- package/test/export-surface.snapshot.json +183 -7
package/dist/core/auto-mode.js
CHANGED
|
@@ -32,67 +32,85 @@ export function createAutoModeDecider(opts) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (open)
|
|
40
|
-
return { kind: "unavailable", cause: "breaker_open" };
|
|
41
|
-
let timer;
|
|
42
|
-
const inner = new AbortController();
|
|
43
|
-
const onOuterAbort = () => inner.abort();
|
|
44
|
-
if (signal !== undefined) {
|
|
45
|
-
if (signal.aborted)
|
|
46
|
-
inner.abort();
|
|
47
|
-
else
|
|
48
|
-
signal.addEventListener("abort", onOuterAbort, { once: true });
|
|
49
|
-
}
|
|
35
|
+
const decide = async (input, signal) => {
|
|
36
|
+
const startedAt = performance.now();
|
|
37
|
+
const verdict = await decideUnreported(input, signal);
|
|
38
|
+
if (opts.onClassified !== undefined) {
|
|
50
39
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
inner.abort();
|
|
57
|
-
reject(new AutoModeTimeout());
|
|
58
|
-
}
|
|
59
|
-
}, timeoutMs);
|
|
60
|
-
opts.classify(input, inner.signal).then((v) => {
|
|
61
|
-
if (!settled) {
|
|
62
|
-
settled = true;
|
|
63
|
-
resolve(v);
|
|
64
|
-
}
|
|
65
|
-
}, (e) => {
|
|
66
|
-
if (!settled) {
|
|
67
|
-
settled = true;
|
|
68
|
-
reject(e);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
40
|
+
opts.onClassified({
|
|
41
|
+
toolCallId: input.req.toolCallId,
|
|
42
|
+
ms: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
43
|
+
verdict: verdict.kind,
|
|
44
|
+
...(verdict.kind === "unavailable" ? { cause: verdict.cause } : {}),
|
|
71
45
|
});
|
|
72
|
-
if (open)
|
|
73
|
-
return { kind: "unavailable", cause: "breaker_open" };
|
|
74
|
-
const verdict = parseAutoModeResponse(raw);
|
|
75
|
-
if (verdict.kind === "parse_error")
|
|
76
|
-
recordFailure("parse_error");
|
|
77
|
-
else
|
|
78
|
-
consecutiveFailures = 0;
|
|
79
|
-
return verdict;
|
|
80
46
|
}
|
|
81
|
-
catch
|
|
82
|
-
if (signal?.aborted)
|
|
83
|
-
return { kind: "unavailable", cause: "error" };
|
|
84
|
-
const timedOut = e instanceof AutoModeTimeout;
|
|
85
|
-
recordFailure(timedOut ? "timeout" : "error");
|
|
86
|
-
return { kind: "unavailable", cause: timedOut ? "timeout" : "error" };
|
|
87
|
-
}
|
|
88
|
-
finally {
|
|
89
|
-
if (timer !== undefined)
|
|
90
|
-
clearTimeout(timer);
|
|
91
|
-
if (signal !== undefined)
|
|
92
|
-
signal.removeEventListener("abort", onOuterAbort);
|
|
47
|
+
catch {
|
|
93
48
|
}
|
|
94
|
-
}
|
|
49
|
+
}
|
|
50
|
+
return verdict;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
breakerOpen: () => open,
|
|
54
|
+
consecutiveFailures: () => consecutiveFailures,
|
|
55
|
+
decide,
|
|
95
56
|
};
|
|
57
|
+
async function decideUnreported(input, signal) {
|
|
58
|
+
if (open)
|
|
59
|
+
return { kind: "unavailable", cause: "breaker_open" };
|
|
60
|
+
let timer;
|
|
61
|
+
const inner = new AbortController();
|
|
62
|
+
const onOuterAbort = () => inner.abort();
|
|
63
|
+
if (signal !== undefined) {
|
|
64
|
+
if (signal.aborted)
|
|
65
|
+
inner.abort();
|
|
66
|
+
else
|
|
67
|
+
signal.addEventListener("abort", onOuterAbort, { once: true });
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const raw = await new Promise((resolve, reject) => {
|
|
71
|
+
let settled = false;
|
|
72
|
+
timer = setTimeout(() => {
|
|
73
|
+
if (!settled) {
|
|
74
|
+
settled = true;
|
|
75
|
+
inner.abort();
|
|
76
|
+
reject(new AutoModeTimeout());
|
|
77
|
+
}
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
opts.classify(input, inner.signal).then((v) => {
|
|
80
|
+
if (!settled) {
|
|
81
|
+
settled = true;
|
|
82
|
+
resolve(v);
|
|
83
|
+
}
|
|
84
|
+
}, (e) => {
|
|
85
|
+
if (!settled) {
|
|
86
|
+
settled = true;
|
|
87
|
+
reject(e);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
if (open)
|
|
92
|
+
return { kind: "unavailable", cause: "breaker_open" };
|
|
93
|
+
const verdict = parseAutoModeResponse(raw);
|
|
94
|
+
if (verdict.kind === "parse_error")
|
|
95
|
+
recordFailure("parse_error");
|
|
96
|
+
else
|
|
97
|
+
consecutiveFailures = 0;
|
|
98
|
+
return verdict;
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
if (signal?.aborted)
|
|
102
|
+
return { kind: "unavailable", cause: "error" };
|
|
103
|
+
const timedOut = e instanceof AutoModeTimeout;
|
|
104
|
+
recordFailure(timedOut ? "timeout" : "error");
|
|
105
|
+
return { kind: "unavailable", cause: timedOut ? "timeout" : "error" };
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
if (timer !== undefined)
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
if (signal !== undefined)
|
|
111
|
+
signal.removeEventListener("abort", onOuterAbort);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
96
114
|
}
|
|
97
115
|
class AutoModeTimeout extends Error {
|
|
98
116
|
constructor() {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The execution-outcome record's vocabulary (layer 0, beside the checkpoint store it serves): the closed
|
|
3
|
+
* answer set of `CheckpointStore.recordExecutionOutcome`, its one disposition table, the conflict error,
|
|
4
|
+
* the shared write disposition and reopen predicate every backend routes through, the structural
|
|
5
|
+
* equality the idempotency arm uses, and the one read face of the column. The column itself
|
|
6
|
+
* (`CheckpointRow.executionOutcome` / `executionAtMs`) and the verb's contract are declared on the store
|
|
7
|
+
* (`checkpoint-store.ts`); this module names no store type — the row facts it reads are spelled
|
|
8
|
+
* structurally — so the store reaches DOWN for it and nothing here reaches back.
|
|
9
|
+
*/
|
|
10
|
+
import { type AssertAllKeysHandled } from "./ask-origin.js";
|
|
11
|
+
import type { GateOutcome } from "./gate-outcome.js";
|
|
12
|
+
/**
|
|
13
|
+
* The closed answer set of `CheckpointStore.recordExecutionOutcome` — what the store did with the
|
|
14
|
+
* execution record it was handed. One word per way the write can end; a caller keys on the word, never
|
|
15
|
+
* on an absence:
|
|
16
|
+
* - `recorded` — the CAS won: the row was `resolved` with no record yet, and now carries this one.
|
|
17
|
+
* - `already_recorded` — the row already carries a record EQUAL to the one supplied (a retry of the
|
|
18
|
+
* same write; idempotent, nothing changed). A record that DIFFERS is not a word — it is the
|
|
19
|
+
* {@link ExecutionOutcomeConflictError} throw.
|
|
20
|
+
* - `not_resolved` — the row exists in this scope but is not `resolved` (still pending, or expired):
|
|
21
|
+
* there is no decision an execution could be the outcome of, so nothing was written.
|
|
22
|
+
* - `absent` — no row under this token in this scope (a missing row and a wrong-scope row alike — the
|
|
23
|
+
* isolation rule every verb of the store keeps).
|
|
24
|
+
*/
|
|
25
|
+
export declare const EXECUTION_OUTCOME_RECORD_WORDS: readonly ["recorded", "already_recorded", "not_resolved", "absent"];
|
|
26
|
+
export type ExecutionOutcomeRecordWord = (typeof EXECUTION_OUTCOME_RECORD_WORDS)[number];
|
|
27
|
+
/** Membership test for {@link ExecutionOutcomeRecordWord} — a deployment store answers across a process
|
|
28
|
+
* boundary, so the consumer validating its word must not hand-roll the set. */
|
|
29
|
+
export declare function isExecutionOutcomeRecordWord(v: unknown): v is ExecutionOutcomeRecordWord;
|
|
30
|
+
/**
|
|
31
|
+
* The one disposition table over {@link ExecutionOutcomeRecordWord}: does this answer mean the row now
|
|
32
|
+
* carries NO execution record — i.e. the leg's disposition of the decided action is not on file? The
|
|
33
|
+
* engine's belt reads it to decide whether to announce `checkpoint.execution_outcome_unrecorded` (the
|
|
34
|
+
* execution result itself is never changed by the answer: the record is an account of what ran, not a
|
|
35
|
+
* gate on it). A new word must take a row here (the fence below reds `tsc` otherwise).
|
|
36
|
+
*/
|
|
37
|
+
export declare const EXECUTION_RECORD_LEAVES_ROW_UNRECORDED: {
|
|
38
|
+
readonly recorded: false;
|
|
39
|
+
readonly already_recorded: false;
|
|
40
|
+
readonly not_resolved: true;
|
|
41
|
+
readonly absent: true;
|
|
42
|
+
};
|
|
43
|
+
export type ExecutionRecordTableCoversEveryWord = AssertAllKeysHandled<Exclude<ExecutionOutcomeRecordWord, keyof typeof EXECUTION_RECORD_LEAVES_ROW_UNRECORDED>>;
|
|
44
|
+
/**
|
|
45
|
+
* The read face of `CheckpointRow.executionOutcome`: what a reader of a resolved row can say about how
|
|
46
|
+
* the decided action was disposed. `executed` carries the record the resumed leg filed; `unknown` is the
|
|
47
|
+
* ONE meaning of the column's absence (the leg has not reached its resolution yet, the row was consumed
|
|
48
|
+
* by a worker that could not record, or the store answered `not_resolved`/`absent` and the engine
|
|
49
|
+
* announced it). Never `allowed` by default — the pre-execution decision lives on `resolvedOutcome`.
|
|
50
|
+
*/
|
|
51
|
+
export type ExecutionVerdict = {
|
|
52
|
+
kind: "executed";
|
|
53
|
+
gate: GateOutcome;
|
|
54
|
+
} | {
|
|
55
|
+
kind: "unknown";
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* `CheckpointStore.recordExecutionOutcome` was handed a record for a row that already carries a DIFFERENT
|
|
59
|
+
* one. The record is physically immutable (see `CheckpointRow.executionOutcome`), so this is never
|
|
60
|
+
* resolved by overwriting: two different accounts of one execution mean the engine ran, or believes it
|
|
61
|
+
* ran, the same decided action twice — an engine defect the belt must surface, not a store condition it
|
|
62
|
+
* may absorb. Carries both records so the operator can read what was on file and what tried to replace
|
|
63
|
+
* it. Code `checkpoint.execution_outcome_conflict`; a deployment store may throw any error carrying the
|
|
64
|
+
* same `code` — the code is the contract, this class is its in-tree form.
|
|
65
|
+
*/
|
|
66
|
+
export declare class ExecutionOutcomeConflictError extends Error {
|
|
67
|
+
readonly code: "checkpoint.execution_outcome_conflict";
|
|
68
|
+
/** The record on file — a COPY: a backend hands this constructor its live row value, and a caller that
|
|
69
|
+
* catches the error must not be able to rewrite the row through it (the record is immutable by CAS,
|
|
70
|
+
* not by convention). */
|
|
71
|
+
readonly recorded: GateOutcome;
|
|
72
|
+
/** The record that tried to replace it — copied for the same reason. */
|
|
73
|
+
readonly attempted: GateOutcome;
|
|
74
|
+
constructor(recorded: GateOutcome, attempted: GateOutcome);
|
|
75
|
+
}
|
|
76
|
+
/** The row facts the record's disposition reads — spelled structurally so a backend can hand a partial
|
|
77
|
+
* read-back (a Pg row) as easily as a full `Checkpoint`. */
|
|
78
|
+
export interface ExecutionRecordRowFacts {
|
|
79
|
+
scope: string;
|
|
80
|
+
status: "pending" | "resolved" | "expired";
|
|
81
|
+
executionOutcome?: GateOutcome;
|
|
82
|
+
}
|
|
83
|
+
/** `true` iff the row carries an execution record — the reopen verbs' extra predicate (a settled action
|
|
84
|
+
* is not re-openable) and the record verb's own idempotency read. Pure; shared by every backend. */
|
|
85
|
+
export declare function checkpointExecutionRecorded(cp: Pick<ExecutionRecordRowFacts, "executionOutcome">): boolean;
|
|
86
|
+
/** Structural equality of two execution records as the JSON they round-trip as — key-order insensitive
|
|
87
|
+
* (a jsonb column reorders keys) and indifferent to freezing (the engine's record is minted frozen; a
|
|
88
|
+
* store's copy is not). A record is a plain data graph by construction, so nothing here needs the
|
|
89
|
+
* accessor/topology defences a caller-supplied value would. */
|
|
90
|
+
export declare function sameExecutionRecord(a: GateOutcome, b: GateOutcome): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* THE one disposition of an execution-record write, shared by every backend so the CAS predicate
|
|
93
|
+
* `status = 'resolved' AND execution_outcome IS NULL` and its two idempotency/conflict arms cannot drift
|
|
94
|
+
* between stores (the same posture as the shared row predicate `checkpointRowMatches`). Pure: it decides
|
|
95
|
+
* what the write means for THIS row and this record; the backend keeps its own commit half (an in-place
|
|
96
|
+
* assignment, an appended ledger event, an `UPDATE … WHERE` that folds the same predicate into SQL).
|
|
97
|
+
* - `commit` — the row is `resolved` in this scope with no record: write it (the answer is `recorded`).
|
|
98
|
+
* - `already_recorded` / `not_resolved` / `absent` — the {@link ExecutionOutcomeRecordWord} to answer.
|
|
99
|
+
* - `conflict` — the row carries a different record: throw {@link ExecutionOutcomeConflictError}.
|
|
100
|
+
* Order is load-bearing: a record on file is judged BEFORE the status arm, so a retry against a row whose
|
|
101
|
+
* status a concurrent verb has since moved still answers by the record it carries.
|
|
102
|
+
*/
|
|
103
|
+
export declare function executionRecordDisposition(cp: ExecutionRecordRowFacts | undefined, scope: string, gate: GateOutcome): "commit" | "conflict" | Exclude<ExecutionOutcomeRecordWord, "recorded">;
|
|
104
|
+
/**
|
|
105
|
+
* The ONE read of `CheckpointRow.executionOutcome`: `executed` with the record when the row carries one,
|
|
106
|
+
* `unknown` otherwise. A consumer rebuilding a run from its rows (an audit face, a supervisor inbox after
|
|
107
|
+
* a stream break) reads this rather than the column, so "no column" and "column not yet written" have
|
|
108
|
+
* exactly one spelling between them and no reader invents a default disposition.
|
|
109
|
+
*/
|
|
110
|
+
export declare function executionVerdict(cp: Pick<ExecutionRecordRowFacts, "executionOutcome">): ExecutionVerdict;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {} from "./ask-origin.js";
|
|
2
|
+
export const EXECUTION_OUTCOME_RECORD_WORDS = ["recorded", "already_recorded", "not_resolved", "absent"];
|
|
3
|
+
const EXECUTION_OUTCOME_RECORD_WORD_SET = new Set(EXECUTION_OUTCOME_RECORD_WORDS);
|
|
4
|
+
export function isExecutionOutcomeRecordWord(v) {
|
|
5
|
+
return EXECUTION_OUTCOME_RECORD_WORD_SET.has(v);
|
|
6
|
+
}
|
|
7
|
+
export const EXECUTION_RECORD_LEAVES_ROW_UNRECORDED = {
|
|
8
|
+
recorded: false,
|
|
9
|
+
already_recorded: false,
|
|
10
|
+
not_resolved: true,
|
|
11
|
+
absent: true,
|
|
12
|
+
};
|
|
13
|
+
export class ExecutionOutcomeConflictError extends Error {
|
|
14
|
+
code = "checkpoint.execution_outcome_conflict";
|
|
15
|
+
recorded;
|
|
16
|
+
attempted;
|
|
17
|
+
constructor(recorded, attempted) {
|
|
18
|
+
super("the checkpoint already carries an execution outcome that differs from the one being recorded — the record is immutable, so this is an engine defect (one decided action settled twice), not a retry");
|
|
19
|
+
this.name = "ExecutionOutcomeConflictError";
|
|
20
|
+
this.recorded = structuredClone(recorded);
|
|
21
|
+
this.attempted = structuredClone(attempted);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function checkpointExecutionRecorded(cp) {
|
|
25
|
+
return cp.executionOutcome !== undefined;
|
|
26
|
+
}
|
|
27
|
+
export function sameExecutionRecord(a, b) {
|
|
28
|
+
return canonicalJson(a) === canonicalJson(b);
|
|
29
|
+
}
|
|
30
|
+
function canonicalJson(v) {
|
|
31
|
+
if (v === null || typeof v !== "object")
|
|
32
|
+
return JSON.stringify(v) ?? "undefined";
|
|
33
|
+
if (Array.isArray(v))
|
|
34
|
+
return `[${v.map(canonicalJson).join(",")}]`;
|
|
35
|
+
const keys = Object.keys(v).filter((k) => v[k] !== undefined).sort();
|
|
36
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(v[k])}`).join(",")}}`;
|
|
37
|
+
}
|
|
38
|
+
export function executionRecordDisposition(cp, scope, gate) {
|
|
39
|
+
if (cp === undefined || cp.scope !== scope)
|
|
40
|
+
return "absent";
|
|
41
|
+
if (cp.executionOutcome !== undefined)
|
|
42
|
+
return sameExecutionRecord(cp.executionOutcome, gate) ? "already_recorded" : "conflict";
|
|
43
|
+
if (cp.status !== "resolved")
|
|
44
|
+
return "not_resolved";
|
|
45
|
+
return "commit";
|
|
46
|
+
}
|
|
47
|
+
export function executionVerdict(cp) {
|
|
48
|
+
return cp.executionOutcome !== undefined ? { kind: "executed", gate: cp.executionOutcome } : { kind: "unknown" };
|
|
49
|
+
}
|
|
@@ -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";
|
|
@@ -820,6 +822,36 @@ export type PendingAction = {
|
|
|
820
822
|
* pre-1a.2 checkpoint (deserialized without it), in which case the resume skips the hash check.
|
|
821
823
|
*/
|
|
822
824
|
boundInputHash: string;
|
|
825
|
+
/**
|
|
826
|
+
* design/388 §2.5 boundary ② (B18; additive — no checkpoint-version bump, the `previewWithheld`
|
|
827
|
+
* precedent: an optional group an older reader ignores): the IDENTITY of the row the park minted
|
|
828
|
+
* this action against, read off the leg's tool roster at the suspend station — `capabilityId`
|
|
829
|
+
* (engine-minted over {source, peer, name, contractId}), `contractId`, `shapeDigest`,
|
|
830
|
+
* `implementationRevision` — plus the roster `digest` of the leg that parked. The resume finds the
|
|
831
|
+
* same-name row on ITS leg and compares the FOUR identity members (`judgeParkedToolIdentity`): any
|
|
832
|
+
* mismatch is a replacement wearing the name and the resume refuses it
|
|
833
|
+
* (`resume.tool_contract_mismatch`, the `tool_unavailable` reopen family). All four ABSENT ⇒ a row
|
|
834
|
+
* minted before the members existed ⇒ the name-only path, never widened by their absence.
|
|
835
|
+
*/
|
|
836
|
+
capabilityId?: string;
|
|
837
|
+
contractId?: string;
|
|
838
|
+
shapeDigest?: string;
|
|
839
|
+
implementationRevision?: string;
|
|
840
|
+
rosterDigest?: string;
|
|
841
|
+
/**
|
|
842
|
+
* design/388 B6 × B18 (additive; the identity group's precedent): the FACE the parked call was judged under —
|
|
843
|
+
* display `family` + declared `pathTarget` — off the same roster row at the same mint. The pre-CAS resume-edit
|
|
844
|
+
* recheck has no roster yet; it rebuilds the call face from these (`parkedCallFaceOf`) and judges the edit as
|
|
845
|
+
* the live gate judged the original (merge-seam H1: without them a caller slot fell to the catalog's slot for
|
|
846
|
+
* the NAME). Any member absent ⇒ pre-388 row ⇒ the recheck falls to the catalog, never a widened grant.
|
|
847
|
+
*/
|
|
848
|
+
family?: import("./tool-face.js").ToolFamily;
|
|
849
|
+
pathTarget?: {
|
|
850
|
+
param: string;
|
|
851
|
+
aliases: string[];
|
|
852
|
+
access: "read" | "create" | "edit";
|
|
853
|
+
skillScopeEligible: boolean;
|
|
854
|
+
};
|
|
823
855
|
/**
|
|
824
856
|
* #438 (additive; no checkpoint-version bump — the `reminderMark` precedent: an optional field an
|
|
825
857
|
* older reader ignores. Unlike that precedent seat — which resume DOES read, through its
|
|
@@ -1062,7 +1094,7 @@ export interface CheckpointState {
|
|
|
1062
1094
|
activeWorktree?: ActiveWorktreeSession;
|
|
1063
1095
|
/**
|
|
1064
1096
|
* Parent effective-policy inheritance (tighten-only): the durable projection of the run's
|
|
1065
|
-
* {@link import("./runner/
|
|
1097
|
+
* {@link import("./runner/contracts.js").InheritedGate}. Only the serializable DATA half persists —
|
|
1066
1098
|
* `ancestorRules` (each ancestor's session-rule snapshot: sessionId/principal/rev/rules, re-compiled on
|
|
1067
1099
|
* resume against the resumed leg's env) and `shellGate` (the chain's max-rank shell doctrine). The OPAQUE
|
|
1068
1100
|
* half (`parentConstraints` — live ToolPolicy/onAsk closures) CANNOT persist; `requiresParentConstraint`
|
|
@@ -1202,7 +1234,7 @@ export interface CheckpointState {
|
|
|
1202
1234
|
/**
|
|
1203
1235
|
* #281 件B follow-up — `true` iff the suspended leg ran as a DELEGATED CHILD (the trusted
|
|
1204
1236
|
* `RunInternals.isDelegatedChild` fact, read off the leg's own minted
|
|
1205
|
-
* {@link import("./runner/
|
|
1237
|
+
* {@link import("./runner/contracts.js").Prepared.hookIdentity} so a re-suspend chain carries it
|
|
1206
1238
|
* forward). Without it a deps-only resume (`resume(token, outcome, config)` — no trusted internals
|
|
1207
1239
|
* re-supplied) re-minted the resumed leg's identity with `isDelegatedChild: false`, so the
|
|
1208
1240
|
* delegation-lifecycle observer's emission gate stayed shut and the resumed leg emitted NEITHER
|
|
@@ -1743,6 +1775,26 @@ export interface CheckpointRow {
|
|
|
1743
1775
|
scope: string;
|
|
1744
1776
|
ttlMs?: number;
|
|
1745
1777
|
};
|
|
1778
|
+
/**
|
|
1779
|
+
* @contract checkpoint.execution_outcome — the settlement record of how the DECIDED action was
|
|
1780
|
+
* finally disposed on the resumed leg: the {@link GateOutcome} the resumed call's `tool_end` frame
|
|
1781
|
+
* carried (an executed allow, a person's refusal delivered, or a post-CAS belt's veto of a person's
|
|
1782
|
+
* yes), written by {@link CheckpointStore.recordExecutionOutcome} exactly once per row, on every
|
|
1783
|
+
* resolution (an `allowed` disposition is recorded too — the column's absence has ONE meaning:
|
|
1784
|
+
* the execution outcome is not known, never "it was allowed"). Independent of
|
|
1785
|
+
* {@link resolvedOutcome}, which stays the immutable record of the DECISION: a row can read
|
|
1786
|
+
* `resolvedOutcome.gateOutcome.disposition.kind === "allowed"` beside
|
|
1787
|
+
* `executionOutcome.disposition = { kind: "denied", deniedBy: "policy" }` — the person approved,
|
|
1788
|
+
* the deployment's belt refused the edited call — and a reader rebuilding the run from the row
|
|
1789
|
+
* sees both facts instead of the pre-execution one alone. The record is physically immutable:
|
|
1790
|
+
* a second write of a different record is a conflict, never an overwrite, and a row carrying one
|
|
1791
|
+
* cannot be {@link CheckpointStore.reopen | reopened} (the action is settled; a reopen would
|
|
1792
|
+
* re-run or re-decide work whose outcome is already on file). Read through
|
|
1793
|
+
* `executionVerdict` (checkpoint-execution-record.ts). Present together with {@link executionAtMs}.
|
|
1794
|
+
*/
|
|
1795
|
+
executionOutcome?: GateOutcome;
|
|
1796
|
+
/** Epoch ms the store stamped {@link executionOutcome} at. Present iff `executionOutcome` is. */
|
|
1797
|
+
executionAtMs?: number;
|
|
1746
1798
|
}
|
|
1747
1799
|
/**
|
|
1748
1800
|
* design/80 assistant-scheduler seam #1: a LIGHTWEIGHT projection of one PENDING
|
|
@@ -1967,6 +2019,10 @@ export declare class CheckpointError extends Error {
|
|
|
1967
2019
|
* checkpoint exists. Surfaced instead of silently treating the failed reopen as committed, so the
|
|
1968
2020
|
* caller's compensation path settles the token as terminally consumed. */
|
|
1969
2021
|
| "checkpoint.reopen_failed"
|
|
2022
|
+
/** {@link CheckpointStore.recordExecutionOutcome} met a row that already carries a DIFFERENT
|
|
2023
|
+
* execution record — see `ExecutionOutcomeConflictError` (checkpoint-execution-record.ts), the
|
|
2024
|
+
* in-tree form of this code. Never a retry condition. */
|
|
2025
|
+
| "checkpoint.execution_outcome_conflict"
|
|
1970
2026
|
/** design/80 D-A: {@link CheckpointStore.setPendingSteer} was handed `text` containing a
|
|
1971
2027
|
* `</system-reminder>` close tag — it would escape the model-facing `<system-reminder>` wrapper a
|
|
1972
2028
|
* trusted steer rides on resume. Rejected fail-closed so a dirty steer NEVER enters {@link CheckpointState}
|
|
@@ -2151,6 +2207,10 @@ export declare class CheckpointError extends Error {
|
|
|
2151
2207
|
* checkpoint exists. Surfaced instead of silently treating the failed reopen as committed, so the
|
|
2152
2208
|
* caller's compensation path settles the token as terminally consumed. */
|
|
2153
2209
|
| "checkpoint.reopen_failed"
|
|
2210
|
+
/** {@link CheckpointStore.recordExecutionOutcome} met a row that already carries a DIFFERENT
|
|
2211
|
+
* execution record — see `ExecutionOutcomeConflictError` (checkpoint-execution-record.ts), the
|
|
2212
|
+
* in-tree form of this code. Never a retry condition. */
|
|
2213
|
+
| "checkpoint.execution_outcome_conflict"
|
|
2154
2214
|
/** design/80 D-A: {@link CheckpointStore.setPendingSteer} was handed `text` containing a
|
|
2155
2215
|
* `</system-reminder>` close tag — it would escape the model-facing `<system-reminder>` wrapper a
|
|
2156
2216
|
* trusted steer rides on resume. Rejected fail-closed so a dirty steer NEVER enters {@link CheckpointState}
|
|
@@ -2456,6 +2516,20 @@ export interface CheckpointStore {
|
|
|
2456
2516
|
reopen: boolean;
|
|
2457
2517
|
validatingLease?: boolean;
|
|
2458
2518
|
};
|
|
2519
|
+
/**
|
|
2520
|
+
* The EXECUTION-OUTCOME declaration — REQUIRED, and required to be exactly `{ outcome: true }`: the
|
|
2521
|
+
* store promises that {@link recordExecutionOutcome} is the real CAS this contract describes (the
|
|
2522
|
+
* predicate, the four words, the conflict throw) and that {@link reopen} honours the record. Same
|
|
2523
|
+
* "declaration, never duck-typing" posture as the three optional axes above, but not optional: the
|
|
2524
|
+
* durable resume lane records how every decided action was disposed, and a store that cannot keep
|
|
2525
|
+
* that record would leave every resolved row it holds silently unreadable on that axis. A store
|
|
2526
|
+
* without the declaration is refused at the FIRST read of the store seat
|
|
2527
|
+
* ({@link resolveCheckpointStore}, code `config.invalid_checkpoint_store`) — a JavaScript caller that
|
|
2528
|
+
* satisfies no type is told at prepare what the type would have told it.
|
|
2529
|
+
*/
|
|
2530
|
+
readonly execution: {
|
|
2531
|
+
readonly outcome: true;
|
|
2532
|
+
};
|
|
2459
2533
|
/** Create-once. Throws {@link CheckpointError} `already_exists` on a token collision (never a silent
|
|
2460
2534
|
* overwrite — a reused token would clobber a live suspension). */
|
|
2461
2535
|
put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
|
|
@@ -2505,8 +2579,35 @@ export interface CheckpointStore {
|
|
|
2505
2579
|
* `tool_unavailable` (P-7: the approved tool vanished, the action may now be invalid → a human re-decides,
|
|
2506
2580
|
* so a FRESH decision is allowed). The store RECORDS the reason on the row as {@link Checkpoint.reopenReason}
|
|
2507
2581
|
* and PRESERVES the persisted winner across the reopen (never clears it).
|
|
2582
|
+
*
|
|
2583
|
+
* **A row that carries an execution record is NOT reopenable** (`… AND execution_outcome IS NULL` in the
|
|
2584
|
+
* WHERE; `checkpointExecutionRecorded` is the shared predicate): the decided action's disposition is on
|
|
2585
|
+
* file — see {@link CheckpointRow.executionOutcome} — and reopening it would either re-run an action that
|
|
2586
|
+
* already ran or re-decide one already refused. Answers `false` like every other predicate miss; the
|
|
2587
|
+
* reasons and the winner are untouched.
|
|
2508
2588
|
*/
|
|
2509
2589
|
reopen?(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
|
|
2590
|
+
/**
|
|
2591
|
+
* Record how the DECIDED action was finally disposed on the resumed leg — the {@link GateOutcome} its
|
|
2592
|
+
* `tool_end` frame carried (see {@link CheckpointRow.executionOutcome} for what the record means and why
|
|
2593
|
+
* it is a column of its own beside the immutable decision). REQUIRED (declared by {@link execution}).
|
|
2594
|
+
*
|
|
2595
|
+
* The CAS: `UPDATE … SET execution_outcome=?, execution_at_ms=? WHERE token=? AND scope=? AND
|
|
2596
|
+
* status='resolved' AND execution_outcome IS NULL`. The answer is a word of the closed
|
|
2597
|
+
* `ExecutionOutcomeRecordWord` set — `recorded` (the CAS won), `already_recorded` (the row already
|
|
2598
|
+
* carries an EQUAL record: a retry, idempotent), `not_resolved` (pending/expired: no decision to be the
|
|
2599
|
+
* outcome of), `absent` (no such row in this scope). A row carrying a DIFFERENT record throws
|
|
2600
|
+
* `ExecutionOutcomeConflictError` (`checkpoint.execution_outcome_conflict`) — the record is immutable
|
|
2601
|
+
* and two accounts of one execution are an engine defect, never a store overwrite. Equality is
|
|
2602
|
+
* structural over the record's JSON (key order and freezing are not part of the value —
|
|
2603
|
+
* `sameExecutionRecord`); every backend takes its disposition from `executionRecordDisposition`
|
|
2604
|
+
* (checkpoint-execution-record.ts) and keeps only the commit half itself.
|
|
2605
|
+
*
|
|
2606
|
+
* Never touches `status`, `rev`, `resolvedOutcome` or `reopenReason`: the record is a fact about the
|
|
2607
|
+
* execution, not a transition of the decision. Its one effect on the row's other verbs is that
|
|
2608
|
+
* {@link reopen} refuses a recorded row.
|
|
2609
|
+
*/
|
|
2610
|
+
recordExecutionOutcome(token: CheckpointToken, scope: string, gate: GateOutcome): Promise<ExecutionOutcomeRecordWord>;
|
|
2510
2611
|
/**
|
|
2511
2612
|
* design/80 D-A: park a durable mid-task STEER on a DURABLY-SUSPENDED task's checkpoint. CAS-style update
|
|
2512
2613
|
* `SET state.pendingSteerQueue=? WHERE token=? AND scope=? AND status='pending'`: returns `true` iff the
|
|
@@ -2692,6 +2793,12 @@ export declare function resolveCheckpointStore(spec: {
|
|
|
2692
2793
|
}, deps: {
|
|
2693
2794
|
checkpointStore?: CheckpointStore;
|
|
2694
2795
|
}): CheckpointStore | undefined;
|
|
2796
|
+
/** The ONE refusal of a store seat whose {@link CheckpointStore} does not declare `execution = { outcome: true }`
|
|
2797
|
+
* (named by seat and by the store's constructor when it has one). Same door, same code, as the retired-`null`
|
|
2798
|
+
* refusal above: a configuration the run cannot honour is refused at the first read, never carried. */
|
|
2799
|
+
export declare function checkpointStoreExecutionUndeclared(seat: "TaskSpec.checkpointStore" | "RunnerDeps.checkpointStore", store: object): Error & {
|
|
2800
|
+
code: "config.invalid_checkpoint_store";
|
|
2801
|
+
};
|
|
2695
2802
|
/** The ONE refusal of the retired `checkpointStore: null` spelling (the resolver and the workflow baseline overlay both throw it). */
|
|
2696
2803
|
export declare function retiredCheckpointStoreNull(): Error & {
|
|
2697
2804
|
code: "config.invalid_checkpoint_store";
|
|
@@ -2803,6 +2910,10 @@ export declare class InMemoryCheckpointStore implements CheckpointStore {
|
|
|
2803
2910
|
readonly redecision: {
|
|
2804
2911
|
readonly reopen: true;
|
|
2805
2912
|
};
|
|
2913
|
+
/** The execution-outcome record verb below is the contract's CAS, and `reopen` honours the record. */
|
|
2914
|
+
readonly execution: {
|
|
2915
|
+
readonly outcome: true;
|
|
2916
|
+
};
|
|
2806
2917
|
private cps;
|
|
2807
2918
|
private fault;
|
|
2808
2919
|
put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
|
|
@@ -2855,6 +2966,7 @@ export declare class InMemoryCheckpointStore implements CheckpointStore {
|
|
|
2855
2966
|
* atomic unit this method exists to provide. (A wrapper that OMITS the member opts out
|
|
2856
2967
|
* cleanly: the consumer's presence probe then keeps the two-step path, hooks intact.) */
|
|
2857
2968
|
claimTerminal(token: CheckpointToken, scope: string, intent: TerminalClaimIntent): Promise<TerminalClaimOutcome>;
|
|
2969
|
+
recordExecutionOutcome(token: CheckpointToken, scope: string, gate: GateOutcome): Promise<ExecutionOutcomeRecordWord>;
|
|
2858
2970
|
reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
|
|
2859
2971
|
setPendingSteer(token: CheckpointToken, scope: string, steer: PendingSteerInput): Promise<boolean>;
|
|
2860
2972
|
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";
|
|
@@ -130,28 +130,6 @@ export declare const MIN_CLEAR_SAVINGS_TOKENS = 20000;
|
|
|
130
130
|
/** Minimum kept results — CC `EUs` :397710 `Math.max(1, keepRecent)`. See
|
|
131
131
|
* {@link ContextEditOptions.keepRecentToolResults} for why both degenerate ends are unusable. */
|
|
132
132
|
export declare const MIN_KEEP_RECENT_TOOL_RESULTS = 1;
|
|
133
|
-
/**
|
|
134
|
-
* checklist #54 (CC microCompact.ts:41-50 `COMPACTABLE_TOOLS` parity): the INCLUSION whitelist of
|
|
135
|
-
* tools whose results may be content-cleared by the stale-result pass. CC's set is exactly
|
|
136
|
-
* { Read, Bash, PowerShell (SHELL_TOOL_NAMES), Grep, Glob, WebSearch, WebFetch, Edit, Write } —
|
|
137
|
-
* cheap-to-regenerate I/O.
|
|
138
|
-
*
|
|
139
|
-
* RE-VERIFIED against the 2.1.250 corpus (design/374 slice 4, member-by-member — a set is exactly
|
|
140
|
-
* the shape where "the count still matches" is not evidence): `eQt = new Set([wt, ...VD, Yo, Xo,
|
|
141
|
-
* yO, Yr, tn, er])` at cli250.js:41714, whose aliases resolve to `wt`="Read", `VD`=[`Xe`="Bash",
|
|
142
|
-
* `Nt`="PowerShell"], `Yo`="Grep", `Xo`="Glob", `yO`="WebSearch", `Yr`="WebFetch", `tn`="Edit",
|
|
143
|
-
* `er`="Write" — nine members, byte-identical to the set below, zero drift from the 223 reading.
|
|
144
|
-
* The exclusion side is the load-bearing half and is unchanged too: everything NOT here is
|
|
145
|
-
* PRESERVED (Agent/Task/Skill/ToolSearch/TodoWrite/Monitor/SendMessage/NotebookEdit and ALL MCP
|
|
146
|
-
* tools are never content-cleared by this pass, upstream or here).
|
|
147
|
-
*
|
|
148
|
-
* Everything else is PRESERVED: expensive/decision-bearing results
|
|
149
|
-
* (Agent subagent reports, Task* registry state, TodoWrite acks, Monitor, MCP tools, custom
|
|
150
|
-
* ToolSpecs) survive to the harder defenses (compaction summarizes them instead of blanking them).
|
|
151
|
-
* PowerShell is kept for CC fidelity even though sema does not mount it. Overridable per call via
|
|
152
|
-
* {@link ContextEditOptions.compactableTools} for custom-tool-heavy embedders.
|
|
153
|
-
*/
|
|
154
|
-
export declare const COMPACTABLE_TOOLS: ReadonlySet<string>;
|
|
155
133
|
export interface ContextEditOptions {
|
|
156
134
|
/** Start clearing once estimated context tokens exceed this. */
|
|
157
135
|
budgetTokens: number;
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens, estimateTokens } from "../internal/harness.js";
|
|
3
3
|
import { isToolResult } from "./message-utils.js";
|
|
4
4
|
import { PERSISTED_OUTPUT_PREFIX, offloadPagebackHint } from "./tool-result-store.js";
|
|
5
|
+
import { compactableToolNames } from "./tool-registry.js";
|
|
5
6
|
const CLEARED_MARKER = "[tool result cleared to save context]";
|
|
6
7
|
const CC_CLEARED_MARKER = "[Old tool result content cleared]";
|
|
7
8
|
const LEGACY_CLEARED_EXTENDED_PREFIX = "[tool result cleared to save context —";
|
|
@@ -46,17 +47,7 @@ function resolveKeepRecentToolResults(value, defaultValue) {
|
|
|
46
47
|
}
|
|
47
48
|
return Math.max(MIN_KEEP_RECENT_TOOL_RESULTS, value);
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
"Read",
|
|
51
|
-
"Bash",
|
|
52
|
-
"PowerShell",
|
|
53
|
-
"Grep",
|
|
54
|
-
"Glob",
|
|
55
|
-
"WebSearch",
|
|
56
|
-
"WebFetch",
|
|
57
|
-
"Edit",
|
|
58
|
-
"Write",
|
|
59
|
-
]);
|
|
50
|
+
const COMPACTABLE_TOOLS = compactableToolNames();
|
|
60
51
|
function soleMarkerText(m) {
|
|
61
52
|
const content = m.content;
|
|
62
53
|
if (!Array.isArray(content) || content.length !== 1)
|