@sema-agent/core 5.43.0 → 5.45.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 +108 -6
- package/dist/agents/subagent.d.ts +3 -1
- package/dist/brain/reasoning.d.ts +50 -4
- package/dist/brain/reasoning.js +28 -7
- package/dist/brain/request-params.d.ts +0 -12
- package/dist/brain/request-params.js +1 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/memory-engine/content-origin.d.ts +6 -3
- package/dist/core/memory-engine/delegation-provenance.d.ts +12 -7
- package/dist/core/memory-engine/engine.d.ts +94 -2
- package/dist/core/memory-engine/engine.js +46 -4
- package/dist/core/memory-engine/layout.d.ts +11 -4
- package/dist/core/memory-engine/layout.js +3 -3
- package/dist/core/memory-engine/tools.d.ts +12 -0
- package/dist/core/memory-engine/tools.js +30 -11
- package/dist/core/runner/assemble-result.d.ts +5 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/prepare-config-doors.d.ts +6 -0
- package/dist/core/runner/prepare-config-doors.js +17 -0
- package/dist/core/runner/prepare-memory.js +24 -3
- package/dist/core/runner/prepare-task.d.ts +3 -1
- package/dist/core/runner/prepare-task.js +25 -3
- package/dist/core/runner/runtask.js +20 -17
- package/dist/core/trace.d.ts +17 -2
- package/dist/core/types.d.ts +100 -1
- package/dist/tools/fs/fs-search-tools.d.ts +4 -3
- package/dist/tools/fs/search.d.ts +43 -7
- package/dist/tools/fs/search.js +75 -31
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,104 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.45.0 — 2026-08-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `RunnerDeps.memoryDelegationEvidence?: "static-face" | "attested-only"` (design/324, ruling-1
|
|
7
|
+
stopgap): the deployment's evidence standard for delegation-driven memory pollution.
|
|
8
|
+
`"attested-only"` retires ONLY the static capability-face mark (the "a child COULD reach
|
|
9
|
+
external content" over-approximation); the attestation-observed mark (`att === "external"`),
|
|
10
|
+
the chain's `incomplete` honesty, `recordExternal`, non-delegation classification and the
|
|
11
|
+
pre-call direct-class mark all stay. Key absent ≡ `"static-face"` ≡ byte-identical behavior.
|
|
12
|
+
A malformed value is refused loudly (`config.memory_delegation_evidence`, exact-spelling gate).
|
|
13
|
+
ACCEPTED COST (the deployment's to own): with `"attested-only"`, a BACKGROUND child's real
|
|
14
|
+
external contact does not mark this session — its content re-enters via TaskOutput /
|
|
15
|
+
task-notification / AgentTranscript, none of which carries an attestation.
|
|
16
|
+
- `memory.delegation_static_mark_waived` EngineNotice — mandatory companion: minted at most once
|
|
17
|
+
per prepared leg when the knob actually waives a static-face mark on a pollution-armed chain
|
|
18
|
+
(`detail: {subagentType?, sessionId?}`; reason carries the waived mark's own sentence, bounded).
|
|
19
|
+
- `TaskResult.effectiveReasoning?: ResolvedReasoning` (#327b): the leg's effective reasoning
|
|
20
|
+
resolution echoed on the result — same object the `reasoning.resolved` trace event carries
|
|
21
|
+
(two faces, one mint); rides the backstop/`resume.tool_unavailable` carrier like its
|
|
22
|
+
precedents. `ReasoningFormat` gains `"responses"` (dispatched by api family, single-source
|
|
23
|
+
`RESPONSES_APIS`).
|
|
24
|
+
- Cross-session recall pins (#260 verdict: engine clean): the consumer contract is pinned —
|
|
25
|
+
the model's final answer text exists BEFORE the terminal harvest commits, `TaskStream.result()`
|
|
26
|
+
is the commit boundary, and a consumer that prints the answer and kills the worker without
|
|
27
|
+
awaiting `result()` strands the entry id-less on disk (the incident's exact evidence shape).
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- The reasoning resolver mirrors `thinkingLevelMap`'s null arm (5.45 rescan): a clamped tier the
|
|
31
|
+
model maps to `null` is reported tier-not-honored (`graded:false`, intent echo) — matching what
|
|
32
|
+
the wire appliers actually send (nothing) instead of claiming "honored exactly"; the r3/r4
|
|
33
|
+
mirror comment now names all three arms. `resolveEffort` reads a malformed non-array
|
|
34
|
+
`reasoningEffortLevels` as undeclared (member-level filtering: garbage/sparse/"off" entries can
|
|
35
|
+
no longer become clamp targets) — a catalog typo no longer fails the whole thinking-on leg with
|
|
36
|
+
a raw TypeError (5.44.0 regression window closed before release).
|
|
37
|
+
- The #260 pins tell the bounded-teardown truth: `result()` settles after the harvest LEG settled
|
|
38
|
+
(committed, or its 15s timeout REPORTED — never silent), the discriminative arm polls 50 ticks
|
|
39
|
+
(the abandon arm sat inside the old 1-tick slack), and the incident-scale argument is stated.
|
|
40
|
+
- `reasoning.resolved` now fires a LOUD DROP frame (`dropped: true`, `effective: "off"`) on
|
|
41
|
+
models whose `Model.reasoning` is falsy — a user-requested effort tier no longer evaporates
|
|
42
|
+
silently (#325). The public `resolveReasoning` reports the drop arm for such models; the
|
|
43
|
+
capable arm is byte-identical. The capability predicate is truthiness (matching every adapter),
|
|
44
|
+
and `supportsReasoningEffort === false` mirrors the ungraded intent-only arm.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- The two 5.44.0 pollution notices are DISPOSITIONED in the governance registry and the
|
|
48
|
+
error-surface cap vocabulary gained `inlineUntrusted`/`pollutionSentence` (both bounded by
|
|
49
|
+
contract) — landed as gate-family debts caught by the full-suite leg.
|
|
50
|
+
|
|
51
|
+
### Notes
|
|
52
|
+
- Commit `2f78edb7` (the delegation-evidence knob) carries the `TaskResult.effectiveReasoning`
|
|
53
|
+
declaration hunk of the parallel effort car (shared-checkout staging race; HEAD is consistent).
|
|
54
|
+
Do not cherry-pick that commit alone.
|
|
55
|
+
|
|
56
|
+
## 5.44.0 — 2026-08-19
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
- Memory pollution is LOUD (#324a): two EngineNotice codes — `memory.session_polluted` (once per
|
|
60
|
+
session, minted on the wx-atomic first transition; states the VERDICT: content written after the
|
|
61
|
+
mark is not admitted to the long-term library at harvest) and `memory.harvest_quarantined`
|
|
62
|
+
(at most once per harvest when a polluted session's harvest withheld ≥1 entry file;
|
|
63
|
+
`detail: {count, moved, escalated, reason?, sessionId?}` — deduplicated-path counts, `moved` =
|
|
64
|
+
files that actually left the model-visible plane, `escalated` = quarantine failures). Both ride
|
|
65
|
+
`deliverEngineNotice` (seat / absent-warn / malformed-seat fallback, unchanged). The notices
|
|
66
|
+
state facts, not promises: writeScope-null and persistence-incapable arms take the zero-adoption
|
|
67
|
+
return and are NOT described as quarantined.
|
|
68
|
+
- `MemoryEngine.markSessionPolluted` now returns `SessionPollutionMarkOutcome` (was `void`) —
|
|
69
|
+
additive; the wx-atomic marker distinguishes `"created" | "existed" | "unpersisted"`.
|
|
70
|
+
- Release-criteria gate hardening (5.44 rescan disposal, 8 findings): the EngineNotice MINT
|
|
71
|
+
judgment is checker-based (the `code` value inside an object literal whose non-nullable
|
|
72
|
+
contextual type carries `code`+`message` on EVERY union arm, in files the shipped build
|
|
73
|
+
compiles) — a literal type, the `err.code=` refusal dialect, a comment, an examples-only mint
|
|
74
|
+
and a bare `{code,message}` const no longer count; a public CLASS is a legal citation root
|
|
75
|
+
(instance + declared statics, minus private/write-only/language-supplied); a disclosed release
|
|
76
|
+
(CHANGELOG section present) with no criteria document reds the gate.
|
|
77
|
+
- design/278 recipe pins: the coordinator fresh-spawn recipe core arms (spawn-fresh imperative,
|
|
78
|
+
verbatim user quote, literal command, execute-only, relayed-consent line) are pinned six-ways —
|
|
79
|
+
the recipe the G6 classifier exception depends on can no longer drift silently. (The #278
|
|
80
|
+
product fix itself shipped in 5.38.0; the ledger row was stale and is now closed.)
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
- Grep target-spelling flip completion (#318): `jsGrepDetailed` answers — including the rescan
|
|
84
|
+
legs an rg-present host falls back to — carry the single explicit FILE target's own spelling
|
|
85
|
+
(family-aware dot-segment collapse, one family judgment for the whole anchor; `..` cannot climb
|
|
86
|
+
past a namespace root). The two engines no longer disagree on path shape for the same call.
|
|
87
|
+
- Grep structured rows completion (#313 remainder): the JS legs hand served rows too; the served-row
|
|
88
|
+
contract states its one measured exception.
|
|
89
|
+
- `memory_get` not-found no longer blindly suggests `memory_search`; when the missing slug was
|
|
90
|
+
written THIS session the text states the mount-snapshot fact (new writes become retrievable
|
|
91
|
+
next session) and, in a polluted session, states that the write will be withheld at harvest.
|
|
92
|
+
`memory_search` zero-hit output appends the pollution disclosure line in polluted sessions only
|
|
93
|
+
(clean sessions byte-identical). Tool contract revisions move: search 1→2, get 2→3.
|
|
94
|
+
- The 5.43.0 `-H` entry erratum (recorded in that section): the flip covers count mode too, and
|
|
95
|
+
the "without a deny judge" arm is not a configuration any public route produces.
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
- `docs/release-criteria/5.43.0.json` four unexecutable/false arms repaired (deny-less arm no
|
|
99
|
+
public route can produce; the deny-note flip F5 declared and claimed by G3; the per-process
|
|
100
|
+
console latch construction made blackbox-executable; the rg-leg note wording quoted as minted).
|
|
101
|
+
|
|
3
102
|
## 5.43.0 — 2026-08-18
|
|
4
103
|
|
|
5
104
|
### Added
|
|
@@ -27,12 +126,15 @@
|
|
|
27
126
|
- Grep's ripgrep legs always pass `-H` (#311): a single explicit file target keeps its path
|
|
28
127
|
field, so a deny-guarded single-file grep keeps the rg fast path instead of abandoning rg for
|
|
29
128
|
the JS scanner's explicit-file lane (an engine swap over that one file, losing rg's
|
|
30
|
-
streaming/large-file handling). Observable flip
|
|
31
|
-
explicit file target now carries a path prefix in the TARGET's spelling as passed (the
|
|
32
|
-
layer hands an absolute canonical path, so the prefix is ABSOLUTE
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
129
|
+
streaming/large-file handling). Observable flip: content-mode AND count-mode output for a
|
|
130
|
+
single explicit file target now carries a path prefix in the TARGET's spelling as passed (the
|
|
131
|
+
tool layer hands an absolute canonical path, so the prefix is ABSOLUTE; count mode reads
|
|
132
|
+
`<path>:<n>`); `files_with_matches` did not move. **Erratum (5.44 rescan)**: this entry
|
|
133
|
+
originally scoped the flip to content mode and described a "without a deny judge" arm — no
|
|
134
|
+
public route produces that arm (every route to the tool compiles a read-deny judge), and the
|
|
135
|
+
previous release answered the call from the JS lane with a ROOT-RELATIVE prefix in every deny
|
|
136
|
+
configuration a caller can produce; count mode flipped the same way. Multi-file and directory
|
|
137
|
+
targets: byte-identical (measured). The two engines still disagree on path shape for the same call (jsGrep emits
|
|
36
138
|
root-relative) — that alignment is tracked separately (#318), not silently changed here.
|
|
37
139
|
- The deny-withholding note no longer fabricates on single-file scopes (5.43 rescan): `rg --files`
|
|
38
140
|
lists an explicitly named file argument regardless of glob filters, so the existence probe
|
|
@@ -297,7 +297,9 @@ export declare function completedAgentCard(child: {
|
|
|
297
297
|
* recorder aggregate at delivery. Present only when the parent chain armed the recorder; the
|
|
298
298
|
* parent-side judgment (the content-origin wrap) reads it off this card — `clean` retires the
|
|
299
299
|
* static face verdict for this call, `external` marks with the observed-event reason, `unknown`
|
|
300
|
-
* (or absence) keeps the static floor
|
|
300
|
+
* (or absence) keeps the static floor, whose mark action follows the parent run's deployment
|
|
301
|
+
* evidence standard (design/324, `RunnerDeps.memoryDelegationEvidence`). Deliberate whitelist
|
|
302
|
+
* admission (see the boundary note). */
|
|
301
303
|
attestation?: import("../core/memory-engine/delegation-provenance.js").DelegationAttestation;
|
|
302
304
|
}): Record<string, unknown>;
|
|
303
305
|
/**
|
|
@@ -34,7 +34,20 @@ export type ReasoningFormat = "openai" | "openrouter" | "deepseek" | "together"
|
|
|
34
34
|
/** Anthropic `output_config.effort` (CC 2.1.198 / Fable-5 class, design/119): the tier is a named
|
|
35
35
|
* effort string, clamped to `compat.effortLevels` — the anthropic-side twin of the OpenAI
|
|
36
36
|
* `reasoning_effort` path (thinking FORM is `thinkingMode`; effort carries the intensity). */
|
|
37
|
-
| "effort"
|
|
37
|
+
| "effort"
|
|
38
|
+
/** OpenAI Responses API (`openai-responses` / `azure-openai-responses`, design/163): the tier rides
|
|
39
|
+
* the NESTED `reasoning: { effort }` request field (open-responses.ts `resolveWireEffort`), clamped
|
|
40
|
+
* by the same {@link resolveEffort} against `compat.reasoningEffortLevels` — NOT the completions
|
|
41
|
+
* top-level `reasoning_effort` the `"openai"` tag names. #325 review r3: before this member the
|
|
42
|
+
* resolver fell through to the completions default and labeled Responses resolutions `"openai"`. */
|
|
43
|
+
| "responses";
|
|
44
|
+
/**
|
|
45
|
+
* The Open Responses API family — every `Api` value `createOpenResponsesBrain` serves (design/163).
|
|
46
|
+
* The SINGLE family discriminant: request-params.ts (reserved-key set selection) and
|
|
47
|
+
* {@link resolveReasoning} (format dispatch, #325 review r4) both key off THIS set, so a new
|
|
48
|
+
* Responses api value added here reaches every family-scoped behavior instead of one of two twins.
|
|
49
|
+
*/
|
|
50
|
+
export declare const RESPONSES_APIS: ReadonlySet<string>;
|
|
38
51
|
/** How a requested intensity resolved against one endpoint's real capability. */
|
|
39
52
|
export interface ReasoningResolution {
|
|
40
53
|
/** The level the caller asked for. */
|
|
@@ -43,9 +56,10 @@ export interface ReasoningResolution {
|
|
|
43
56
|
effective: ThinkingLevel;
|
|
44
57
|
/**
|
|
45
58
|
* True when the endpoint varies behavior by intensity (effort tiers / token budget). False = binary
|
|
46
|
-
* enable-only (qwen / zai / qwen-chat-template,
|
|
47
|
-
* `supportsReasoningEffort:false
|
|
48
|
-
*
|
|
59
|
+
* enable-only (qwen / zai / qwen-chat-template, an effort endpoint that declares
|
|
60
|
+
* `supportsReasoningEffort:false`, or a tier `Model.thinkingLevelMap` maps to `null` — the wire then
|
|
61
|
+
* carries NO effort value for it): thinking is on or off and the tier is NOT honored — read this
|
|
62
|
+
* alongside {@link effective}, which then echoes the request as intent only.
|
|
49
63
|
*/
|
|
50
64
|
graded: boolean;
|
|
51
65
|
/** True when {@link effective} differs from {@link requested} (the request couldn't be honored exactly). */
|
|
@@ -62,6 +76,23 @@ export interface ResolvedReasoning extends ReasoningResolution {
|
|
|
62
76
|
format: ReasoningFormat;
|
|
63
77
|
/** A coarse endpoint label for the trace (`model.api` — e.g. `openai-completions`, `anthropic-messages`). */
|
|
64
78
|
endpoint: string;
|
|
79
|
+
/**
|
|
80
|
+
* Present (true) only when the model declares NO reasoning capability (`Model.reasoning` falsy): every
|
|
81
|
+
* brain early-returns on that flag (openai.ts applyThinking / anthropic.ts / open-responses.ts
|
|
82
|
+
* resolveWireEffort), so NO thinking
|
|
83
|
+
* parameter reaches the wire at all — the requested tier is DROPPED entirely, not clamped or
|
|
84
|
+
* downgraded-to-binary. `effective:"off"` here states the ENGINE side of that fact (nothing was
|
|
85
|
+
* requested), NOT a measured gateway state: on the binary enable-only formats (qwen / zai /
|
|
86
|
+
* qwen-chat-template) parameter ABSENCE is provider-default, and a default-on gateway may still
|
|
87
|
+
* reason at its own tier — the brains deliberately send no disable key for a model declared
|
|
88
|
+
* non-reasoning, so the engine cannot claim a measured off there; `format` tells a consumer which
|
|
89
|
+
* family applies (on every other format, absence is the closest the wire has to off — see
|
|
90
|
+
* applyThinking's own contract). `format`/`endpoint` report the family that WOULD have carried the
|
|
91
|
+
* tier (the why-shape a consumer diagnoses with, not a claim about the request). Absent on every
|
|
92
|
+
* reasoning-capable resolution, so existing consumers see byte-identical objects. Loud-bad-values
|
|
93
|
+
* rule: the one arm where the request evaporates must not be the one arm with no report.
|
|
94
|
+
*/
|
|
95
|
+
dropped?: true;
|
|
65
96
|
}
|
|
66
97
|
/**
|
|
67
98
|
* Resolve a requested intensity against a model END-TO-END, dispatching on its wire format the SAME way the
|
|
@@ -74,6 +105,17 @@ export interface ResolvedReasoning extends ReasoningResolution {
|
|
|
74
105
|
* - Binary enable-only formats (qwen / zai / qwen-chat-template) → `graded:false` (tier not honored).
|
|
75
106
|
* - An effort endpoint with `supportsReasoningEffort:false` → `graded:false` (enable key only, no effort tier).
|
|
76
107
|
* - Otherwise effort-based → clamp DOWN to the endpoint's `reasoningEffortLevels` (default minimal|low|medium|high).
|
|
108
|
+
* - A CLAMPED tier that `model.thinkingLevelMap` maps to `null` (declared unsupported on this model) →
|
|
109
|
+
* `graded:false`: both effort-wire appliers key the map by the post-clamp tier and then carry NO effort
|
|
110
|
+
* value (thinking still enables via the format's own enable key where one exists), so the tier is not
|
|
111
|
+
* honored. Previously this resolver never read the map and reported such a request as exactly honored
|
|
112
|
+
* (`graded:true`, `clamped:false`) while the wire dropped the value — trace/result-face drift.
|
|
113
|
+
*
|
|
114
|
+
* - A model whose `reasoning` capability flag is FALSY drops the request ENTIRELY (neither brain emits any
|
|
115
|
+
* thinking parameter, whatever the format) → `effective:"off"`, `graded:false`, `clamped:true`,
|
|
116
|
+
* `dropped:true` — the loud-drop arm. Previously this resolver described the capability dispatch for such
|
|
117
|
+
* a model (a resolution the request never carried), and the runner's trace guard skipped the frame — the
|
|
118
|
+
* one arm where the request evaporates was the one arm with no disclosure.
|
|
77
119
|
*
|
|
78
120
|
* `off`/falsy never enables thinking, so it resolves trivially (no clamp, graded:true) — the caller decides
|
|
79
121
|
* whether to emit at all.
|
|
@@ -82,12 +124,16 @@ export declare function resolveReasoning(requested: ThinkingLevel, model: {
|
|
|
82
124
|
api?: string;
|
|
83
125
|
reasoning?: boolean;
|
|
84
126
|
compat?: unknown;
|
|
127
|
+
thinkingLevelMap?: Readonly<Partial<Record<ThinkingLevel, string | null>>>;
|
|
85
128
|
}): ResolvedReasoning;
|
|
86
129
|
/**
|
|
87
130
|
* Resolve a requested intensity for an effort-based endpoint (`reasoning_effort` / `reasoning.effort`). Picks
|
|
88
131
|
* the requested tier when supported; otherwise the highest supported tier ≤ requested (clamp DOWN, never
|
|
89
132
|
* silently up); if none is ≤ requested (the endpoint's set is entirely higher — unusual), the lowest supported
|
|
90
133
|
* tier. Never returns a value outside `allowed`, so the brain can't emit a tier the endpoint would 422.
|
|
134
|
+
* A malformed `allowed` reads as UNDECLARED → the conservative default set (non-array, empty, or no
|
|
135
|
+
* valid member); individually invalid MEMBERS (garbage strings, sparse holes, `"off"` — not a legal
|
|
136
|
+
* member of the 6-tier compat effort sets) are dropped, so they can never become a clamp target.
|
|
91
137
|
*/
|
|
92
138
|
export declare function resolveEffort(requested: ThinkingLevel, allowed?: readonly ThinkingLevel[]): ReasoningResolution;
|
|
93
139
|
/**
|
package/dist/brain/reasoning.js
CHANGED
|
@@ -15,7 +15,18 @@ export function rankOf(level) {
|
|
|
15
15
|
}
|
|
16
16
|
export const DEFAULT_EFFORT_LEVELS = ["minimal", "low", "medium", "high"];
|
|
17
17
|
const BINARY_FORMATS = new Set(["qwen", "zai", "qwen-chat-template"]);
|
|
18
|
+
export const RESPONSES_APIS = new Set(["openai-responses", "azure-openai-responses", "openai-chatgpt-responses"]);
|
|
18
19
|
export function resolveReasoning(requested, model) {
|
|
20
|
+
const resolved = dispatchReasoning(requested, model);
|
|
21
|
+
if (requested !== "off" && !model.reasoning) {
|
|
22
|
+
return { requested, effective: "off", graded: false, clamped: true, format: resolved.format, endpoint: resolved.endpoint, dropped: true };
|
|
23
|
+
}
|
|
24
|
+
return resolved;
|
|
25
|
+
}
|
|
26
|
+
function effortTierUnmapped(requested, effective, model) {
|
|
27
|
+
return requested !== "off" && model.thinkingLevelMap?.[effective] === null;
|
|
28
|
+
}
|
|
29
|
+
function dispatchReasoning(requested, model) {
|
|
19
30
|
const endpoint = model.api ?? "unknown";
|
|
20
31
|
const compat = (model.compat ?? {});
|
|
21
32
|
if (model.api === "anthropic-messages") {
|
|
@@ -24,6 +35,16 @@ export function resolveReasoning(requested, model) {
|
|
|
24
35
|
}
|
|
25
36
|
return { requested, effective: requested, graded: true, clamped: false, format: "budget", endpoint };
|
|
26
37
|
}
|
|
38
|
+
if (model.api !== undefined && RESPONSES_APIS.has(model.api)) {
|
|
39
|
+
if (compat.supportsReasoningEffort === false) {
|
|
40
|
+
return { requested, effective: requested, graded: false, clamped: false, format: "responses", endpoint };
|
|
41
|
+
}
|
|
42
|
+
const responsesResolved = resolveEffort(requested, compat.reasoningEffortLevels ?? DEFAULT_EFFORT_LEVELS);
|
|
43
|
+
if (effortTierUnmapped(requested, responsesResolved.effective, model)) {
|
|
44
|
+
return { requested, effective: requested, graded: false, clamped: false, format: "responses", endpoint };
|
|
45
|
+
}
|
|
46
|
+
return { ...responsesResolved, format: "responses", endpoint };
|
|
47
|
+
}
|
|
27
48
|
const format = compat.thinkingFormat ?? "openai";
|
|
28
49
|
if (BINARY_FORMATS.has(format)) {
|
|
29
50
|
return { ...resolveBinary(requested), format, endpoint };
|
|
@@ -32,15 +53,15 @@ export function resolveReasoning(requested, model) {
|
|
|
32
53
|
if (!supportsEffort && format !== "openrouter") {
|
|
33
54
|
return { requested, effective: requested, graded: false, clamped: false, format, endpoint };
|
|
34
55
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
format,
|
|
38
|
-
|
|
39
|
-
};
|
|
56
|
+
const resolved = resolveEffort(requested, compat.reasoningEffortLevels ?? DEFAULT_EFFORT_LEVELS);
|
|
57
|
+
if (effortTierUnmapped(requested, resolved.effective, model)) {
|
|
58
|
+
return { requested, effective: requested, graded: false, clamped: false, format, endpoint };
|
|
59
|
+
}
|
|
60
|
+
return { ...resolved, format, endpoint };
|
|
40
61
|
}
|
|
41
62
|
export function resolveEffort(requested, allowed = DEFAULT_EFFORT_LEVELS) {
|
|
42
|
-
|
|
43
|
-
|
|
63
|
+
const declared = Array.isArray(allowed) ? allowed.filter((lvl) => isThinkingLevel(lvl) && lvl !== "off") : [];
|
|
64
|
+
allowed = declared.length > 0 ? declared : DEFAULT_EFFORT_LEVELS;
|
|
44
65
|
if (requested === "off" || allowed.includes(requested)) {
|
|
45
66
|
return { requested, effective: requested, graded: true, clamped: false };
|
|
46
67
|
}
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Request-parameter passthrough (design/40). A deployment can set official provider params it wants but
|
|
3
|
-
* core doesn't model as first-class fields (OpenAI `frequency_penalty`/`presence_penalty`/`top_p`/`seed`/
|
|
4
|
-
* `logit_bias`; Anthropic `top_k`/`top_p`/`stop_sequences`; …) via `Model.extraBody`, and core merges them
|
|
5
|
-
* into the brain's request body. Anchored at the **Model** level (not per-turn) so the serialized body
|
|
6
|
-
* stays byte-stable across turns → the prefix cache holds (design/9/31).
|
|
7
|
-
*
|
|
8
|
-
* Symmetric: each brain owns its structural + tuning keys (`reserved`) and ALWAYS wins — `extraBody` can
|
|
9
|
-
* only supply the long tail, never clobber `messages`/`tools`/`temperature`/etc. Stripping is the safety
|
|
10
|
-
* enforcement (the brains are standalone-usable); `prepareTask` separately warns on a collision (it has
|
|
11
|
-
* `onError`, the brain doesn't — council design/40).
|
|
12
|
-
*/
|
|
13
1
|
/** OpenAI request-body keys core owns; `extraBody` cannot override these. */
|
|
14
2
|
export declare const OPENAI_RESERVED: ReadonlySet<string>;
|
|
15
3
|
/** Anthropic request-body keys core owns; `extraBody` cannot override these. (Note the asymmetry vs
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RESPONSES_APIS } from "./reasoning.js";
|
|
1
2
|
export const OPENAI_RESERVED = new Set([
|
|
2
3
|
"model",
|
|
3
4
|
"messages",
|
|
@@ -38,7 +39,6 @@ export const RESPONSES_RESERVED = new Set([
|
|
|
38
39
|
"previous_response_id",
|
|
39
40
|
"conversation",
|
|
40
41
|
]);
|
|
41
|
-
const RESPONSES_APIS = new Set(["openai-responses", "azure-openai-responses", "openai-chatgpt-responses"]);
|
|
42
42
|
export function reservedFor(api) {
|
|
43
43
|
if (RESPONSES_APIS.has(api))
|
|
44
44
|
return RESPONSES_RESERVED;
|
|
@@ -14,6 +14,9 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
|
|
|
14
14
|
"memory.partition_split",
|
|
15
15
|
"memory.tail",
|
|
16
16
|
"memory.pollution_mark_failed",
|
|
17
|
+
"memory.session_polluted",
|
|
18
|
+
"memory.harvest_quarantined",
|
|
19
|
+
"memory.delegation_static_mark_waived",
|
|
17
20
|
"memory.challenge_sweep_failed",
|
|
18
21
|
"memory.lineage_settle_failed",
|
|
19
22
|
"memory.challenge_ledger_corrupt",
|
|
@@ -66,9 +66,12 @@ export interface AgentPoolTool {
|
|
|
66
66
|
* A child that can reach a network or protocol tool returns content from outside the trust boundary
|
|
67
67
|
* just as surely as calling that tool in this session would, and the parent's transcript is where it
|
|
68
68
|
* lands. The judgment is STATIC — the child's face is known from its definition before the call — so
|
|
69
|
-
* nothing here tracks what the child actually did (runtime provenance is
|
|
70
|
-
* it conservative in the safe direction: a child that COULD reach external content counts as
|
|
71
|
-
* done so.
|
|
69
|
+
* nothing here tracks what the child actually did (runtime provenance is design/180 half A). That
|
|
70
|
+
* makes it conservative in the safe direction: a child that COULD reach external content counts as
|
|
71
|
+
* having done so. Whether this verdict MARKS the judging session's memory when no attestation
|
|
72
|
+
* arrives is the deployment's evidence standard (design/324,
|
|
73
|
+
* `RunnerDeps.memoryDelegationEvidence`) — the classification itself is value-independent, and the
|
|
74
|
+
* wrap computes it on every standard (the waiver announcement keys on it).
|
|
72
75
|
*
|
|
73
76
|
* `pool` is the CHILD'S pool, not the parent's roster: a delegation tool carries its own tool set, and
|
|
74
77
|
* the two are explicitly allowed to differ — a parent mounting nothing but the delegation tool can
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* design/180 half A — delegation RUNTIME provenance: the three-value attestation and its monotonic
|
|
3
3
|
* aggregate. The static tool-face judgment ({@link import("./content-origin.js").delegationCallIsExternal})
|
|
4
|
-
* stays the
|
|
5
|
-
* clean proof is in hand; any evidence gap falls back to the floor. "We could not
|
|
6
|
-
* and "it is clean" are different answers — replacing the floor outright would fail
|
|
7
|
-
* evidence gap (old child vintages, crashes, un-armed sessions).
|
|
4
|
+
* stays the FLOOR of the attestation reduction: runtime evidence may only retire the static verdict
|
|
5
|
+
* when a COMPLETE clean proof is in hand; any evidence gap falls back to the floor. "We could not
|
|
6
|
+
* prove it clean" and "it is clean" are different answers — replacing the floor outright would fail
|
|
7
|
+
* OPEN on every evidence gap (old child vintages, crashes, un-armed sessions). Whether the floor's
|
|
8
|
+
* verdict MARKS the judging session's memory is a separate, deployment-governed question
|
|
9
|
+
* (design/324, `RunnerDeps.memoryDelegationEvidence`): the reduction semantics here never change
|
|
10
|
+
* with that key — only the mark action at the judging run's wrap does.
|
|
8
11
|
*
|
|
9
12
|
* The aggregate is two MONOTONIC bits plus versioning:
|
|
10
13
|
* - `sawExternal` — an actually-observed polluting-class invocation (own recorder, or a delivered
|
|
@@ -17,8 +20,9 @@
|
|
|
17
20
|
* "clean" unprovable; it can never make an observed event disappear).
|
|
18
21
|
*/
|
|
19
22
|
/** The three-value delegation attestation. Only `clean` changes behavior (the parent skips the
|
|
20
|
-
* static mark); `unknown` is byte-equivalent to absence (static floor governs
|
|
21
|
-
*
|
|
23
|
+
* static mark); `unknown` is byte-equivalent to absence (the static floor governs — its mark
|
|
24
|
+
* action subject to the judging run's deployment evidence standard, design/324); `external` marks
|
|
25
|
+
* unconditionally on every evidence standard, with the observed-event reason. */
|
|
22
26
|
export type DelegationAttestation = "clean" | "external" | "unknown";
|
|
23
27
|
/** The monotonic per-run provenance aggregate (in-process this slice; the durable-checkpoint
|
|
24
28
|
* persistence leg rides the background-face slice — absence after a resume reads `incomplete`,
|
|
@@ -58,5 +62,6 @@ export declare function reduceDelegationAttestation(agg: DelegationProvenanceAgg
|
|
|
58
62
|
completed: boolean;
|
|
59
63
|
}): DelegationAttestation;
|
|
60
64
|
/** Read a delivered card's attestation field (the explicit-whitelist handback seat). Anything but
|
|
61
|
-
* the three spellings — including absence — reads as undefined, i.e. the static floor governs
|
|
65
|
+
* the three spellings — including absence — reads as undefined, i.e. the static floor governs
|
|
66
|
+
* (whether that floor MARKS is the judging run's deployment evidence standard, design/324). */
|
|
62
67
|
export declare function readCardAttestation(details: unknown): DelegationAttestation | undefined;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { EngineNotice } from "../types.js";
|
|
1
2
|
import { type CommittedBinding, type EraseMemoryEntriesInput, type MemoryErasureAttestation, type TransferEvidence } from "./file-backend.js";
|
|
2
3
|
import { type MemoryExportBundle, type MemoryImportReport } from "./export-bundle.js";
|
|
3
|
-
import { type ChallengeAssignment, type ChallengeEvent, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, type MemoryPartitionIncidentSink, type RetrievedAccountRow, type SessionPollutionRecord } from "./layout.js";
|
|
4
|
+
import { type ChallengeAssignment, type ChallengeEvent, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, type MemoryPartitionIncidentSink, type RetrievedAccountRow, type SessionPollutionMarkOutcome, type SessionPollutionRecord } from "./layout.js";
|
|
4
5
|
import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemorySessionHandle, ScanFinding } from "./types.js";
|
|
5
6
|
/**
|
|
6
7
|
* The CC `# Memory` instruction section — VERBATIM from the live capture
|
|
@@ -255,6 +256,91 @@ export interface EntryProvenanceAccount {
|
|
|
255
256
|
reason?: string;
|
|
256
257
|
};
|
|
257
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* The pollution mark's loud half ({@link EngineNotice}). The engine's own `onIncident` seat carries the
|
|
261
|
+
* FAILURES of this machinery (`memory.pollution_mark_failed`, `memory.challenge_sweep_failed`); this is
|
|
262
|
+
* the other fact — the machinery worked, and its verdict silently changed what every memory write in
|
|
263
|
+
* this session will do. Before this code existed the transition had no face at all: the session went on
|
|
264
|
+
* writing memory files, the harvest quarantined every one of them, and nothing said so while a host
|
|
265
|
+
* could still act on it.
|
|
266
|
+
*
|
|
267
|
+
* SCOPE OF THE CLAIM (adversarial-review round 1, P1 — the first cut PROMISED quarantine on every mark).
|
|
268
|
+
* This notice states the VERDICT, not the mechanism's outcome: "no longer eligible", plus what a harvest
|
|
269
|
+
* does WHEN IT COLLECTS. That distinction is load-bearing, because several real paths reach a polluted
|
|
270
|
+
* harvest that quarantines nothing — a `writeScope`-null layering and a declared-unavailable session
|
|
271
|
+
* both take `harvestCore`'s zero-admission return (nothing is collected, so nothing is captured), and a
|
|
272
|
+
* mark landing after the pre-commit pollution read leaves already-committed rows to the CHALLENGE sweep
|
|
273
|
+
* rather than to containment. What a given harvest actually contained is the other code's job
|
|
274
|
+
* (`memory.harvest_quarantined`), and the message says so instead of guessing here. The baseline clause
|
|
275
|
+
* is likewise scoped to a collecting harvest: containment judges a changed file against the
|
|
276
|
+
* MATERIALIZE-time baseline, not against the mark's timestamp, so pre-mark writes are covered too.
|
|
277
|
+
*
|
|
278
|
+
* The challenge clause is likewise a POLICY statement with its failure channel named (adversarial-review
|
|
279
|
+
* round 3): the mark-side sweep is best-effort — an unreadable or unwritable lineage ledger makes
|
|
280
|
+
* `markSessionPolluted` announce `memory.challenge_sweep_failed` and return normally — so this notice
|
|
281
|
+
* must not be read as a receipt that the sweep landed.
|
|
282
|
+
*
|
|
283
|
+
* Minted by the runner's mark seat (the one call site of the mark face), so an engine-direct host keeps
|
|
284
|
+
* the incident seat and gains no new obligation.
|
|
285
|
+
*
|
|
286
|
+
* `reason` names a TOOL, and tool names come from the host/protocol roster: neutralized and bounded
|
|
287
|
+
* here — the same treatment the report warnings give it — before it rides either the message or the
|
|
288
|
+
* detail (a host renders both).
|
|
289
|
+
*/
|
|
290
|
+
export declare function memorySessionPollutedNotice(input: {
|
|
291
|
+
reason: string;
|
|
292
|
+
sessionId?: string;
|
|
293
|
+
}): EngineNotice;
|
|
294
|
+
/**
|
|
295
|
+
* The polluted harvest's loud half ({@link EngineNotice}): what the containment actually did, minted
|
|
296
|
+
* once per harvest that withheld at least one ENTRY file. `count` is the withheld set (the
|
|
297
|
+
* `polluted`-coded rejections — entry files that diverged from the materialize baseline), `moved` the
|
|
298
|
+
* subset that also LEFT the model-visible plane, and `escalated` the subset carrying a
|
|
299
|
+
* `quarantineFailures` row.
|
|
300
|
+
*
|
|
301
|
+
* `escalated` is its own input rather than `count - moved` (adversarial-review round 3): the two are
|
|
302
|
+
* NOT the same set. A file whose delete fails but whose tombstone write succeeds counts as removed —
|
|
303
|
+
* it rides `movedToQuarantine` AND an escalation row saying it was tombstoned in place — so the
|
|
304
|
+
* arithmetic would report a clean containment while a known failure sat in the report. Every
|
|
305
|
+
* not-moved file does have an escalation row, so `escalated` is the superset and the honest clause.
|
|
306
|
+
*
|
|
307
|
+
* KNOWN GAP (adversarial-review round 1, P2 — registered, not closed here): the derived index is
|
|
308
|
+
* contained by a different path (`restorePollutedIndex` captures and restores `MEMORY.md`) that mints
|
|
309
|
+
* no rejection row, so a polluted session whose ONLY memory change was an index line produces no
|
|
310
|
+
* `count` and no notice — its containment is disclosed by the harvest report's warnings alone. Closing
|
|
311
|
+
* it needs a structured containment signal on `HarvestReport`, a public type this fix does not touch.
|
|
312
|
+
* The wording therefore says "entry file(s)" rather than implying the whole containment.
|
|
313
|
+
*
|
|
314
|
+
* `reason` is optional because the mark record is read separately from the report: a record that cannot
|
|
315
|
+
* be read must not turn a true "N files were withheld" into a lie about why.
|
|
316
|
+
*/
|
|
317
|
+
/**
|
|
318
|
+
* The three numbers {@link memoryHarvestQuarantinedNotice} reports, read off ONE harvest report. Pure
|
|
319
|
+
* and exported so the derivation is provable on its own (the tombstone case — moved AND escalated — is
|
|
320
|
+
* not constructible through a real filesystem without root-dependent permission tricks).
|
|
321
|
+
*
|
|
322
|
+
* Every number is scoped to the WITHHELD paths: a dual-root harvest merges the read plane's report in,
|
|
323
|
+
* and that plane's ordinary write gate quarantines on its own grounds — counting its rows would credit
|
|
324
|
+
* this session's pollution with containment it never performed.
|
|
325
|
+
*
|
|
326
|
+
* FILES, NOT ROWS (adversarial-review round 4). One path can appear twice in the same report: when the
|
|
327
|
+
* pre-commit pollution recheck flips a clean harvest to containment, a file the ordinary secret/filename
|
|
328
|
+
* gate already quarantined is contained again, and both passes append. Counting rows would then publish
|
|
329
|
+
* `moved`/`escalated` greater than `count` — an impossible statement about a set of files — so every
|
|
330
|
+
* number here is a count of DISTINCT paths.
|
|
331
|
+
*/
|
|
332
|
+
export declare function pollutionContainmentCounts(report: HarvestReport): {
|
|
333
|
+
count: number;
|
|
334
|
+
moved: number;
|
|
335
|
+
escalated: number;
|
|
336
|
+
};
|
|
337
|
+
export declare function memoryHarvestQuarantinedNotice(input: {
|
|
338
|
+
count: number;
|
|
339
|
+
moved: number;
|
|
340
|
+
escalated: number;
|
|
341
|
+
reason?: string;
|
|
342
|
+
sessionId?: string;
|
|
343
|
+
}): EngineNotice;
|
|
258
344
|
export declare class MemoryEngine {
|
|
259
345
|
private readonly backend;
|
|
260
346
|
private readonly memoryDir;
|
|
@@ -301,8 +387,14 @@ export declare class MemoryEngine {
|
|
|
301
387
|
* record, and the durable marker survives restarts (control plane — never model-writable
|
|
302
388
|
* frontmatter). A failed durable write is disclosed via `onIncident` (`memory.pollution_mark_failed`)
|
|
303
389
|
* while the in-memory mark keeps THIS process fail-closed.
|
|
390
|
+
*
|
|
391
|
+
* RETURNS the durable marker's {@link SessionPollutionMarkOutcome} — `"created"` ⇔ this call made
|
|
392
|
+
* the transition. That is the ONE atomic answer to "was this the clean → polluted crossing", and the
|
|
393
|
+
* runner's once-per-session disclosure is keyed on it: a pre-read cannot decide it (two concurrent
|
|
394
|
+
* markers of one session would both read clean and both announce). Callers that do not disclose
|
|
395
|
+
* ignore it exactly as before.
|
|
304
396
|
*/
|
|
305
|
-
markSessionPolluted(sessionId: string, reason: string):
|
|
397
|
+
markSessionPolluted(sessionId: string, reason: string): SessionPollutionMarkOutcome;
|
|
306
398
|
/** The session's pollution record (in-process mark first, then the durable marker) — undefined =
|
|
307
399
|
* clean. Reads are side-effect-free. */
|
|
308
400
|
sessionPollution(sessionId: string): SessionPollutionRecord | undefined;
|
|
@@ -48,6 +48,47 @@ export const DEFAULT_HARVEST_DEADLINE_MS = 5_000;
|
|
|
48
48
|
export const DEFAULT_HARVEST_FILE_BUDGET = 2_000;
|
|
49
49
|
export const MASS_DELETION_FUSE_RATIO = 0.5;
|
|
50
50
|
let indexCaptureSeq = 0;
|
|
51
|
+
export function memorySessionPollutedNotice(input) {
|
|
52
|
+
const reason = inlineUntrusted(input.reason, 200);
|
|
53
|
+
return {
|
|
54
|
+
code: "memory.session_polluted",
|
|
55
|
+
message: `Memory for this session is marked polluted: ${reason}. Its memory writes are no longer eligible for the ` +
|
|
56
|
+
`long-term library — a harvest withholds what it collects (quarantined for host review, judged against the ` +
|
|
57
|
+
`session's opening baseline, so changes made before this point are withheld too), and contributions this ` +
|
|
58
|
+
`session already committed are challenged (a sweep that cannot run announces memory.challenge_sweep_failed). ` +
|
|
59
|
+
`What a given harvest actually contained is announced as memory.harvest_quarantined.`,
|
|
60
|
+
detail: { reason, ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}) },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function pollutionContainmentCounts(report) {
|
|
64
|
+
const withheld = new Set(report.rejections.filter((r) => r.code === "polluted").map((r) => r.path));
|
|
65
|
+
const distinct = (paths) => new Set(paths.filter((p) => withheld.has(p))).size;
|
|
66
|
+
return {
|
|
67
|
+
count: withheld.size,
|
|
68
|
+
moved: distinct(report.movedToQuarantine),
|
|
69
|
+
escalated: distinct((report.quarantineFailures ?? []).map((q) => q.path)),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function memoryHarvestQuarantinedNotice(input) {
|
|
73
|
+
const reason = input.reason !== undefined ? inlineUntrusted(input.reason, 200) : undefined;
|
|
74
|
+
return {
|
|
75
|
+
code: "memory.harvest_quarantined",
|
|
76
|
+
message: `Memory harvest committed nothing for this polluted session${reason !== undefined ? ` (${reason})` : ""}: ` +
|
|
77
|
+
`${input.count} memory entry file(s) written or changed in it were withheld, ${input.moved} of them moved into ` +
|
|
78
|
+
`the control-plane quarantine directory for host review` +
|
|
79
|
+
(input.escalated > 0
|
|
80
|
+
? `; ${input.escalated} carr${input.escalated === 1 ? "ies" : "y"} a containment escalation (a file that did not leave the ` +
|
|
81
|
+
`model-visible plane, or one tombstoned in place rather than deleted) — the harvest report's quarantineFailures names them.`
|
|
82
|
+
: "."),
|
|
83
|
+
detail: {
|
|
84
|
+
count: input.count,
|
|
85
|
+
moved: input.moved,
|
|
86
|
+
escalated: input.escalated,
|
|
87
|
+
...(reason !== undefined ? { reason } : {}),
|
|
88
|
+
...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
51
92
|
export class MemoryEngine {
|
|
52
93
|
backend;
|
|
53
94
|
memoryDir;
|
|
@@ -115,14 +156,14 @@ export class MemoryEngine {
|
|
|
115
156
|
markSessionPolluted(sessionId, reason) {
|
|
116
157
|
if (!this.pollutedSessions.has(sessionId))
|
|
117
158
|
this.pollutedSessions.set(sessionId, { at: this.now(), reason });
|
|
118
|
-
let
|
|
159
|
+
let outcome;
|
|
119
160
|
try {
|
|
120
|
-
|
|
161
|
+
outcome = markSessionPolluted(this.controlDir, sessionId, reason, this.now);
|
|
121
162
|
}
|
|
122
163
|
catch {
|
|
123
|
-
|
|
164
|
+
outcome = "unpersisted";
|
|
124
165
|
}
|
|
125
|
-
if (
|
|
166
|
+
if (outcome === "unpersisted") {
|
|
126
167
|
const sink = this.onIncident;
|
|
127
168
|
if (sink !== undefined) {
|
|
128
169
|
try {
|
|
@@ -156,6 +197,7 @@ export class MemoryEngine {
|
|
|
156
197
|
}
|
|
157
198
|
}
|
|
158
199
|
}
|
|
200
|
+
return outcome;
|
|
159
201
|
}
|
|
160
202
|
sessionPollution(sessionId) {
|
|
161
203
|
const inProcess = this.pollutedSessions.get(sessionId);
|
|
@@ -288,11 +288,18 @@ export interface SessionPollutionRecord {
|
|
|
288
288
|
}
|
|
289
289
|
/**
|
|
290
290
|
* Mark `sessionId` polluted (idempotent; the FIRST record wins — `wx` create refuses overwrite).
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
291
|
+
*
|
|
292
|
+
* The outcome distinguishes the two ways a durable marker can exist after the call, because the
|
|
293
|
+
* `wx` create is the only ATOMIC answer to "who made this transition" available anywhere in the
|
|
294
|
+
* stack (#324a, adversarial-review round 5): a check-then-act pre-read lets two concurrent markers
|
|
295
|
+
* of one session both believe they were first, and a once-per-session disclosure keyed on that would
|
|
296
|
+
* announce twice. `"created"` ⇔ THIS call minted the marker; `"existed"` ⇔ it was already there (a
|
|
297
|
+
* repeat mark, a resumed session, or a racing peer that won the create); `"unpersisted"` ⇔ the marker
|
|
298
|
+
* could not be written at all — the caller must keep its own in-memory mark and disclose (the session
|
|
299
|
+
* stays polluted for this process either way).
|
|
294
300
|
*/
|
|
295
|
-
export
|
|
301
|
+
export type SessionPollutionMarkOutcome = "created" | "existed" | "unpersisted";
|
|
302
|
+
export declare function markSessionPolluted(controlDir: string, sessionId: string, reason: string, now: () => number): SessionPollutionMarkOutcome;
|
|
296
303
|
/** Read the durable pollution record for `sessionId` (undefined = no marker).
|
|
297
304
|
* A marker that EXISTS still reads as polluted whether or not its bytes can be read or parsed
|
|
298
305
|
* (fail-closed: neither corruption nor an unreadable node may launder the state) — a synthesized
|
|
@@ -740,12 +740,12 @@ export function markSessionPolluted(controlDir, sessionId, reason, now) {
|
|
|
740
740
|
ensureDirExists(dirname(path));
|
|
741
741
|
const record = { at: now(), reason };
|
|
742
742
|
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
743
|
-
return
|
|
743
|
+
return "created";
|
|
744
744
|
}
|
|
745
745
|
catch (err) {
|
|
746
746
|
if (err instanceof Error && "code" in err && err.code === "EEXIST")
|
|
747
|
-
return
|
|
748
|
-
return
|
|
747
|
+
return "existed";
|
|
748
|
+
return "unpersisted";
|
|
749
749
|
}
|
|
750
750
|
}
|
|
751
751
|
export function readSessionPollution(controlDir, sessionId) {
|