@sema-agent/core 5.42.0 → 5.44.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 +97 -0
- package/dist/core/governance-codes.js +2 -0
- 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 +3 -0
- package/dist/core/runner/assemble-result.js +4 -1
- package/dist/core/runner/prepare-memory.js +24 -3
- package/dist/core/runner/prepare-task.d.ts +2 -0
- package/dist/core/runner/prepare-task.js +6 -4
- package/dist/core/runner/runtask.js +2 -0
- package/dist/core/types.d.ts +32 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +8 -0
- package/dist/engine/execution-env/node-execution-env.js +41 -4
- package/dist/tools/fs/fs-search-tools.d.ts +5 -4
- package/dist/tools/fs/fs-search-tools.js +8 -5
- package/dist/tools/fs/search.d.ts +48 -1
- package/dist/tools/fs/search.js +96 -41
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,102 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.44.0 — 2026-08-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Memory pollution is LOUD (#324a): two EngineNotice codes — `memory.session_polluted` (once per
|
|
7
|
+
session, minted on the wx-atomic first transition; states the VERDICT: content written after the
|
|
8
|
+
mark is not admitted to the long-term library at harvest) and `memory.harvest_quarantined`
|
|
9
|
+
(at most once per harvest when a polluted session's harvest withheld ≥1 entry file;
|
|
10
|
+
`detail: {count, moved, escalated, reason?, sessionId?}` — deduplicated-path counts, `moved` =
|
|
11
|
+
files that actually left the model-visible plane, `escalated` = quarantine failures). Both ride
|
|
12
|
+
`deliverEngineNotice` (seat / absent-warn / malformed-seat fallback, unchanged). The notices
|
|
13
|
+
state facts, not promises: writeScope-null and persistence-incapable arms take the zero-adoption
|
|
14
|
+
return and are NOT described as quarantined.
|
|
15
|
+
- `MemoryEngine.markSessionPolluted` now returns `SessionPollutionMarkOutcome` (was `void`) —
|
|
16
|
+
additive; the wx-atomic marker distinguishes `"created" | "existed" | "unpersisted"`.
|
|
17
|
+
- Release-criteria gate hardening (5.44 rescan disposal, 8 findings): the EngineNotice MINT
|
|
18
|
+
judgment is checker-based (the `code` value inside an object literal whose non-nullable
|
|
19
|
+
contextual type carries `code`+`message` on EVERY union arm, in files the shipped build
|
|
20
|
+
compiles) — a literal type, the `err.code=` refusal dialect, a comment, an examples-only mint
|
|
21
|
+
and a bare `{code,message}` const no longer count; a public CLASS is a legal citation root
|
|
22
|
+
(instance + declared statics, minus private/write-only/language-supplied); a disclosed release
|
|
23
|
+
(CHANGELOG section present) with no criteria document reds the gate.
|
|
24
|
+
- design/278 recipe pins: the coordinator fresh-spawn recipe core arms (spawn-fresh imperative,
|
|
25
|
+
verbatim user quote, literal command, execute-only, relayed-consent line) are pinned six-ways —
|
|
26
|
+
the recipe the G6 classifier exception depends on can no longer drift silently. (The #278
|
|
27
|
+
product fix itself shipped in 5.38.0; the ledger row was stale and is now closed.)
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- Grep target-spelling flip completion (#318): `jsGrepDetailed` answers — including the rescan
|
|
31
|
+
legs an rg-present host falls back to — carry the single explicit FILE target's own spelling
|
|
32
|
+
(family-aware dot-segment collapse, one family judgment for the whole anchor; `..` cannot climb
|
|
33
|
+
past a namespace root). The two engines no longer disagree on path shape for the same call.
|
|
34
|
+
- Grep structured rows completion (#313 remainder): the JS legs hand served rows too; the served-row
|
|
35
|
+
contract states its one measured exception.
|
|
36
|
+
- `memory_get` not-found no longer blindly suggests `memory_search`; when the missing slug was
|
|
37
|
+
written THIS session the text states the mount-snapshot fact (new writes become retrievable
|
|
38
|
+
next session) and, in a polluted session, states that the write will be withheld at harvest.
|
|
39
|
+
`memory_search` zero-hit output appends the pollution disclosure line in polluted sessions only
|
|
40
|
+
(clean sessions byte-identical). Tool contract revisions move: search 1→2, get 2→3.
|
|
41
|
+
- The 5.43.0 `-H` entry erratum (recorded in that section): the flip covers count mode too, and
|
|
42
|
+
the "without a deny judge" arm is not a configuration any public route produces.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- `docs/release-criteria/5.43.0.json` four unexecutable/false arms repaired (deny-less arm no
|
|
46
|
+
public route can produce; the deny-note flip F5 declared and claimed by G3; the per-process
|
|
47
|
+
console latch construction made blackbox-executable; the rg-leg note wording quoted as minted).
|
|
48
|
+
|
|
49
|
+
## 5.43.0 — 2026-08-18
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- `TaskResult.checkpointId` — the non-secret checkpoint identity now rides the suspended /
|
|
53
|
+
needs_review terminals beside `checkpointToken` (full chain: mint → publishCommittedSuspend →
|
|
54
|
+
suspend/review ref → runTask ref copy → assemble-result; the ref copy is an explicit-key
|
|
55
|
+
whitelist and the new key crosses it). **Erratum for the 5.42.0 entry**: that release minted
|
|
56
|
+
`checkpointId` on `Checkpoint`/`CheckpointSummary` only — it did NOT yet reach `TaskResult`;
|
|
57
|
+
the entry omitted that boundary. This release completes the face.
|
|
58
|
+
- `NodeExecutionEnv` constructor seat `onSecretEnvScrub` (#316): the Bash exec site's secret-env
|
|
59
|
+
scrub decision is observable. Findings name the denylist-matched keys withheld from the child
|
|
60
|
+
env; a caller-overridden key (constructor `shellEnv` or per-exec `env`) is NOT reported withheld —
|
|
61
|
+
the child has it by the caller's explicit choice; an async seat's rejection is consumed (an announcement must
|
|
62
|
+
never become an exec failure); unwired builds print a once-per-process console summary.
|
|
63
|
+
Companion doc fix (#315, doc half): the scrub matches key NAMES against the denylist — a secret
|
|
64
|
+
VALUE under an innocent name is not detected; the doc now says so where the denylist lives.
|
|
65
|
+
- Grep structured rows (#313, first stage): the ripgrep legs hand their served rows
|
|
66
|
+
(`{path?, text}`, post cap/offset) to the tool layer, and `grepDetailFields` prefers row
|
|
67
|
+
identity when every served row carries it — `details.filenames` on rg legs no longer re-parses
|
|
68
|
+
paths out of formatted text (a path containing `:digits:` can no longer split wrong). Text
|
|
69
|
+
parsing remains the honest fallback (JS fallback leg unchanged; its rows stage is a later
|
|
70
|
+
window).
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
- Grep's ripgrep legs always pass `-H` (#311): a single explicit file target keeps its path
|
|
74
|
+
field, so a deny-guarded single-file grep keeps the rg fast path instead of abandoning rg for
|
|
75
|
+
the JS scanner's explicit-file lane (an engine swap over that one file, losing rg's
|
|
76
|
+
streaming/large-file handling). Observable flip: content-mode AND count-mode output for a
|
|
77
|
+
single explicit file target now carries a path prefix in the TARGET's spelling as passed (the
|
|
78
|
+
tool layer hands an absolute canonical path, so the prefix is ABSOLUTE; count mode reads
|
|
79
|
+
`<path>:<n>`); `files_with_matches` did not move. **Erratum (5.44 rescan)**: this entry
|
|
80
|
+
originally scoped the flip to content mode and described a "without a deny judge" arm — no
|
|
81
|
+
public route produces that arm (every route to the tool compiles a read-deny judge), and the
|
|
82
|
+
previous release answered the call from the JS lane with a ROOT-RELATIVE prefix in every deny
|
|
83
|
+
configuration a caller can produce; count mode flipped the same way. Multi-file and directory
|
|
84
|
+
targets: byte-identical (measured). The two engines still disagree on path shape for the same call (jsGrep emits
|
|
85
|
+
root-relative) — that alignment is tracked separately (#318), not silently changed here.
|
|
86
|
+
- The deny-withholding note no longer fabricates on single-file scopes (5.43 rescan): `rg --files`
|
|
87
|
+
lists an explicitly named file argument regardless of glob filters, so the existence probe
|
|
88
|
+
behind the note would have claimed "entries were excluded" on every deny-wired single-file grep
|
|
89
|
+
the `-H` change made reachable. A target the stat probe proves is a FILE is judged clean without
|
|
90
|
+
the probe — the tool layer already judged that one target against the same deny table before
|
|
91
|
+
the engine ran, and a scope of exactly one served file has nothing else to withhold. Directory
|
|
92
|
+
scopes keep the probe unchanged.
|
|
93
|
+
- A clean no-match ripgrep run now carries `rows: []` (5.43 rescan): the one rg terminal that
|
|
94
|
+
omitted the served-rows key, breaking the "rows present ⟺ ripgrep leg" reading.
|
|
95
|
+
- `onSecretEnvScrub` judges its seat by FUNCTION-ness (5.43 rescan): a present non-function seat
|
|
96
|
+
(null, an untyped host's JSON wiring) no longer silences both channels at once — the findings
|
|
97
|
+
keep the seatless once-per-process console summary and the seat defect itself is announced once
|
|
98
|
+
per process (the #170 shape; loud-bad-value law).
|
|
99
|
+
|
|
3
100
|
## 5.42.0 — 2026-08-18
|
|
4
101
|
|
|
5
102
|
### Added
|
|
@@ -14,6 +14,8 @@ 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",
|
|
17
19
|
"memory.challenge_sweep_failed",
|
|
18
20
|
"memory.lineage_settle_failed",
|
|
19
21
|
"memory.challenge_ledger_corrupt",
|
|
@@ -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) {
|
|
@@ -66,6 +66,18 @@ export interface MemoryEngineToolsOptions {
|
|
|
66
66
|
planes: ReadonlyArray<MemoryEnginePlane>;
|
|
67
67
|
/** Clock (tests); default Date.now. Drives the natural-language age rendering only. */
|
|
68
68
|
now?: () => number;
|
|
69
|
+
/**
|
|
70
|
+
* design/178 §3 (#324b) — the session's pollution READ face: the record when this session is marked
|
|
71
|
+
* polluted, `undefined` when it is clean. Read-only and side-effect-free (the mark is one-way and
|
|
72
|
+
* lives at the runner's mark seat); consulted ONLY on the empty outcomes, where a session whose
|
|
73
|
+
* writes are being withheld deserves to hear it instead of being told to search again.
|
|
74
|
+
*
|
|
75
|
+
* Absent ⇒ no pollution face is wired (engine-direct hosts, tests): the empty-outcome wording is
|
|
76
|
+
* then exactly the clean-session wording — nothing is inferred from the seat's absence.
|
|
77
|
+
*/
|
|
78
|
+
sessionPollution?: () => {
|
|
79
|
+
reason: string;
|
|
80
|
+
} | undefined;
|
|
69
81
|
}
|
|
70
82
|
export interface MemorySearchHit {
|
|
71
83
|
id: string;
|
|
@@ -91,6 +91,26 @@ export function skipBytes(text, startBytes) {
|
|
|
91
91
|
export function createMemoryEngineTools(opts) {
|
|
92
92
|
const { planes } = opts;
|
|
93
93
|
const now = opts.now ?? Date.now;
|
|
94
|
+
const pollutionSentence = () => {
|
|
95
|
+
let reason;
|
|
96
|
+
try {
|
|
97
|
+
reason = opts.sessionPollution?.()?.reason;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
if (reason === undefined)
|
|
103
|
+
return "";
|
|
104
|
+
return ` This session is marked polluted (${inlineUntrusted(reason, 200)}): the memory it writes is not admitted to the library at harvest, so writing this down now would not make it retrievable later either.`;
|
|
105
|
+
};
|
|
106
|
+
const noMatch = (query) => {
|
|
107
|
+
const details = { outcome: "ok", hits: [] };
|
|
108
|
+
return {
|
|
109
|
+
content: `No memory entries matched ${JSON.stringify(inlineUntrusted(query, 120))}. If the user expected you to know this, say that you checked memory and found nothing.` +
|
|
110
|
+
pollutionSentence(),
|
|
111
|
+
details,
|
|
112
|
+
};
|
|
113
|
+
};
|
|
94
114
|
const getWithinScopes = async (plane, ids) => {
|
|
95
115
|
const scopeSet = new Set(plane.scopes);
|
|
96
116
|
return (await plane.backend.getByIds(ids)).filter((e) => scopeSet.has(e.scope) && e.frontmatter.deleted !== true);
|
|
@@ -102,7 +122,7 @@ export function createMemoryEngineTools(opts) {
|
|
|
102
122
|
defer: true,
|
|
103
123
|
offload: false,
|
|
104
124
|
contentOrigin: "local",
|
|
105
|
-
contract: { contractId: "core.memory_search@1", implementationRevision: "
|
|
125
|
+
contract: { contractId: "core.memory_search@1", implementationRevision: "2" },
|
|
106
126
|
parameters: Type.Object({
|
|
107
127
|
query: Type.String({ description: "Keywords to look for (lexical match against entry names, descriptions and bodies)." }),
|
|
108
128
|
limit: Type.Optional(Type.Number({ description: `Maximum hits to return (default ${MEMORY_SEARCH_DEFAULT_LIMIT}, max ${MEMORY_SEARCH_MAX_LIMIT}).` })),
|
|
@@ -144,10 +164,8 @@ export function createMemoryEngineTools(opts) {
|
|
|
144
164
|
}
|
|
145
165
|
merged.sort(contractOrder);
|
|
146
166
|
const top = merged.slice(0, limit);
|
|
147
|
-
if (top.length === 0)
|
|
148
|
-
|
|
149
|
-
return { content: `No memory entries matched ${JSON.stringify(inlineUntrusted(query, 120))}. If the user expected you to know this, say that you checked memory and found nothing.`, details };
|
|
150
|
-
}
|
|
167
|
+
if (top.length === 0)
|
|
168
|
+
return noMatch(query);
|
|
151
169
|
const bodyById = new Map();
|
|
152
170
|
try {
|
|
153
171
|
for (let i = 0; i < planes.length; i++) {
|
|
@@ -171,10 +189,8 @@ export function createMemoryEngineTools(opts) {
|
|
|
171
189
|
return refusedSearch("challenge_ledger_unavailable", "Memory search is unavailable: the challenge ledger for a mounted memory plane cannot be read (fail-closed). Report this to the operator.", "failed");
|
|
172
190
|
}
|
|
173
191
|
const live = top.filter((h) => bodyById.has(h.id) && terminalExclusions[h.planeIndex]?.has(h.id) !== true);
|
|
174
|
-
if (live.length === 0)
|
|
175
|
-
|
|
176
|
-
return { content: `No memory entries matched ${JSON.stringify(inlineUntrusted(query, 120))}. If the user expected you to know this, say that you checked memory and found nothing.`, details };
|
|
177
|
-
}
|
|
192
|
+
if (live.length === 0)
|
|
193
|
+
return noMatch(query);
|
|
178
194
|
for (let i = 0; i < planes.length; i++) {
|
|
179
195
|
const ids = live.filter((h) => h.planeIndex === i).map((h) => h.id);
|
|
180
196
|
if (ids.length === 0)
|
|
@@ -219,7 +235,7 @@ export function createMemoryEngineTools(opts) {
|
|
|
219
235
|
defer: true,
|
|
220
236
|
offload: false,
|
|
221
237
|
contentOrigin: "local",
|
|
222
|
-
contract: { contractId: "core.memory_get@1", implementationRevision: "
|
|
238
|
+
contract: { contractId: "core.memory_get@1", implementationRevision: "3" },
|
|
223
239
|
parameters: Type.Object({
|
|
224
240
|
id: Type.Optional(Type.String({ description: "Entry id (exact lookup). Pass either id or slug, not both." })),
|
|
225
241
|
slug: Type.Optional(Type.String({ description: "Entry slug (its file path without .md). Ambiguous across scopes unless scope is also passed." })),
|
|
@@ -275,7 +291,10 @@ export function createMemoryEngineTools(opts) {
|
|
|
275
291
|
}
|
|
276
292
|
if (matches.length === 0) {
|
|
277
293
|
const where = scope !== undefined ? ` in scope ${JSON.stringify(inlineUntrusted(scope, 80))}` : "";
|
|
278
|
-
return refusedGet("not_found", `No memory entry with slug ${JSON.stringify(inlineUntrusted(slug, 160))}${where} is mounted in this session.
|
|
294
|
+
return refusedGet("not_found", `No memory entry with slug ${JSON.stringify(inlineUntrusted(slug, 160))}${where} is mounted in this session. ` +
|
|
295
|
+
`Only entries carrying a committed entry id are served here, and a memory file written into the memory root during ` +
|
|
296
|
+
`this session gets its id when the session-boundary harvest admits it — a file written in this session is not served here before then.` +
|
|
297
|
+
pollutionSentence(), "not_found", { slug: slug, ...(scope !== undefined ? { scope } : {}) });
|
|
279
298
|
}
|
|
280
299
|
if (matches.length > 1) {
|
|
281
300
|
const candidates = matches.map((m) => ({ scope: m.header.scope, slug: m.header.slug, id: m.header.id }));
|
|
@@ -170,6 +170,8 @@ export interface ResultFlags {
|
|
|
170
170
|
* but does not set `threw`, so it never reaches the `flags.threw` branch. */
|
|
171
171
|
suspendRef?: {
|
|
172
172
|
token: import("../checkpoint-store.js").CheckpointToken;
|
|
173
|
+
/** The non-secret identity twin ({@link import("../checkpoint-store.js").Checkpoint.checkpointId}). */
|
|
174
|
+
checkpointId?: string;
|
|
173
175
|
gate: import("../checkpoint-store.js").CheckpointGate;
|
|
174
176
|
/** RB-439-b: how the paused workspace comes back (`"park_only"` = a non-suspendable target that was
|
|
175
177
|
* never actually paused). Echoed on `TaskResult.workspaceRestoreMode`; absent for a process-local
|
|
@@ -190,6 +192,7 @@ export interface ResultFlags {
|
|
|
190
192
|
* so it never hits the failure branches above. */
|
|
191
193
|
reviewRef?: {
|
|
192
194
|
token: import("../checkpoint-store.js").CheckpointToken;
|
|
195
|
+
checkpointId?: string;
|
|
193
196
|
gate: import("../checkpoint-store.js").CheckpointGate;
|
|
194
197
|
/** RB-439-b: the review pause pauses the same workspace the approval pause does — same discriminant. */
|
|
195
198
|
restoreMode?: "snapshot" | "park_only";
|
|
@@ -60,6 +60,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
60
60
|
let blockedReason;
|
|
61
61
|
let salvagedOutput;
|
|
62
62
|
let checkpointToken;
|
|
63
|
+
let checkpointId;
|
|
63
64
|
let checkpointGate;
|
|
64
65
|
let workspaceRestoreMode;
|
|
65
66
|
const isDegenerate = final?.stopReason === "error" && isDegenerateCutMessage(final);
|
|
@@ -114,6 +115,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
114
115
|
else if (flags.suspendRef) {
|
|
115
116
|
status = "suspended";
|
|
116
117
|
checkpointToken = flags.suspendRef.token;
|
|
118
|
+
checkpointId = flags.suspendRef.checkpointId;
|
|
117
119
|
checkpointGate = flags.suspendRef.gate;
|
|
118
120
|
workspaceRestoreMode = flags.suspendRef.restoreMode;
|
|
119
121
|
}
|
|
@@ -121,6 +123,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
121
123
|
status = "needs_review";
|
|
122
124
|
errorCode = "review.pending";
|
|
123
125
|
checkpointToken = flags.reviewRef.token;
|
|
126
|
+
checkpointId = flags.reviewRef.checkpointId;
|
|
124
127
|
checkpointGate = flags.reviewRef.gate;
|
|
125
128
|
workspaceRestoreMode = flags.reviewRef.restoreMode;
|
|
126
129
|
}
|
|
@@ -162,5 +165,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
162
165
|
void _internalCompaction;
|
|
163
166
|
if (flags.unpricedSpend)
|
|
164
167
|
delete publicStats.costMicroUsd;
|
|
165
|
-
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), stats: publicStats };
|
|
168
|
+
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), stats: publicStats };
|
|
166
169
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { sep } from "node:path";
|
|
2
|
+
import { deliverEngineNotice } from "../types.js";
|
|
2
3
|
import { admitMemoryScopes } from "../memory-admission.js";
|
|
3
4
|
import { adoptLegacyRepoDirs, canonicalize, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, isContainedIn, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
|
|
4
5
|
import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
|
|
5
6
|
import { normalizeMemorySpec } from "../memory.js";
|
|
6
|
-
import { MEMORY_ANNOUNCEMENT_READONLY_CODA, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
|
|
7
|
+
import { MEMORY_ANNOUNCEMENT_READONLY_CODA, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_RECALL_DISCIPLINE, MemoryEngine, memoryHarvestQuarantinedNotice, memorySessionPollutedNotice, pollutionContainmentCounts, } from "../memory-engine/engine.js";
|
|
7
8
|
import { createMemoryEngineTools } from "../memory-engine/tools.js";
|
|
8
9
|
import { assertScopeContractPlacement, parseScopeKey, resolveProjectId } from "../memory-engine/scope-contract.js";
|
|
9
10
|
import { FileMemoryEngineBackend } from "../memory-engine/file-backend.js";
|
|
@@ -298,12 +299,26 @@ export async function prepareMemory(input) {
|
|
|
298
299
|
}
|
|
299
300
|
return writeEngine.gateWrite(writeHandle, w.key, w.content);
|
|
300
301
|
};
|
|
302
|
+
let pollutionAnnounced = false;
|
|
303
|
+
const announceHarvestContainment = (report) => {
|
|
304
|
+
const { count, moved, escalated } = pollutionContainmentCounts(report);
|
|
305
|
+
if (count === 0)
|
|
306
|
+
return;
|
|
307
|
+
let reason;
|
|
308
|
+
try {
|
|
309
|
+
reason = writeEngine.sessionPollution(sessionId)?.reason;
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
}
|
|
313
|
+
deliverEngineNotice(deps.onNotice, memoryHarvestQuarantinedNotice({ count, moved, escalated, ...(reason !== undefined ? { reason } : {}), sessionId }));
|
|
314
|
+
};
|
|
301
315
|
const harvestSafe = async (phase = "terminal") => {
|
|
302
316
|
try {
|
|
303
317
|
const report = await harvestBoth();
|
|
304
318
|
if (!report.ok && report.incident) {
|
|
305
319
|
deps.onError?.(new Error(`memory harvest refused (${report.incident.kind}): ${report.incident.detail}`), { phase: "memory", sessionId });
|
|
306
320
|
}
|
|
321
|
+
announceHarvestContainment(report);
|
|
307
322
|
try {
|
|
308
323
|
deps.onMemoryHarvestReport?.(report, { sessionId, phase });
|
|
309
324
|
}
|
|
@@ -327,7 +342,13 @@ export async function prepareMemory(input) {
|
|
|
327
342
|
harvest: harvestSafe,
|
|
328
343
|
pollution: {
|
|
329
344
|
polluted: () => writeEngine.sessionPollution(sessionId),
|
|
330
|
-
markPolluted: (reason) =>
|
|
345
|
+
markPolluted: (reason) => {
|
|
346
|
+
const outcome = writeEngine.markSessionPolluted(sessionId, reason);
|
|
347
|
+
if (outcome === "existed" || (outcome === "unpersisted" && pollutionAnnounced))
|
|
348
|
+
return;
|
|
349
|
+
pollutionAnnounced = true;
|
|
350
|
+
deliverEngineNotice(deps.onNotice, memorySessionPollutedNotice({ reason, sessionId }));
|
|
351
|
+
},
|
|
331
352
|
},
|
|
332
353
|
contentSafety: {
|
|
333
354
|
trustedTools: new Set(memorySpec.trustedTools ?? []),
|
|
@@ -341,7 +362,7 @@ export async function prepareMemory(input) {
|
|
|
341
362
|
writeScope: memorySpec.writeScope,
|
|
342
363
|
};
|
|
343
364
|
if (input.memorySearchToolsPlanned)
|
|
344
|
-
memoryTools = createMemoryEngineTools({ planes: toolPlanes });
|
|
365
|
+
memoryTools = createMemoryEngineTools({ planes: toolPlanes, sessionPollution: () => writeEngine.sessionPollution(sessionId) });
|
|
345
366
|
}
|
|
346
367
|
catch (err) {
|
|
347
368
|
if (typeof err.code === "string" &&
|
|
@@ -433,6 +433,7 @@ export interface Prepared {
|
|
|
433
433
|
* fired this run. */
|
|
434
434
|
suspendRef: {
|
|
435
435
|
token?: CheckpointToken;
|
|
436
|
+
checkpointId?: string;
|
|
436
437
|
gate?: CheckpointGate;
|
|
437
438
|
scope?: string;
|
|
438
439
|
restoreMode?: "snapshot" | "park_only";
|
|
@@ -453,6 +454,7 @@ export interface Prepared {
|
|
|
453
454
|
* `status:"needs_review"`. Empty unless a review pause fired this run. */
|
|
454
455
|
reviewRef: {
|
|
455
456
|
token?: CheckpointToken;
|
|
457
|
+
checkpointId?: string;
|
|
456
458
|
gate?: CheckpointGate;
|
|
457
459
|
scope?: string;
|
|
458
460
|
restoreMode?: "snapshot" | "park_only";
|
|
@@ -3537,9 +3537,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3537
3537
|
return { ok: false, reason };
|
|
3538
3538
|
}
|
|
3539
3539
|
};
|
|
3540
|
-
const publishCommittedSuspend = (token, gate, scope, remoteHandle) => {
|
|
3540
|
+
const publishCommittedSuspend = (token, gate, scope, remoteHandle, checkpointId) => {
|
|
3541
3541
|
const ref = gate.kind === "needs_review" || gate.kind === "plan_review" ? reviewRef : suspendRef;
|
|
3542
3542
|
ref.token = token;
|
|
3543
|
+
if (checkpointId !== undefined)
|
|
3544
|
+
ref.checkpointId = checkpointId;
|
|
3543
3545
|
ref.gate = gate;
|
|
3544
3546
|
if (remoteHandle !== undefined)
|
|
3545
3547
|
ref.restoreMode = remoteHandle.restoreMode === "park_only" ? "park_only" : "snapshot";
|
|
@@ -3625,7 +3627,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3625
3627
|
};
|
|
3626
3628
|
if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
|
|
3627
3629
|
return false;
|
|
3628
|
-
publishCommittedSuspend(token, gate, scope, remoteHandle);
|
|
3630
|
+
publishCommittedSuspend(token, gate, scope, remoteHandle, cp.checkpointId);
|
|
3629
3631
|
try {
|
|
3630
3632
|
await sessions.pin?.(sessionId);
|
|
3631
3633
|
}
|
|
@@ -3711,7 +3713,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3711
3713
|
};
|
|
3712
3714
|
if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
|
|
3713
3715
|
return false;
|
|
3714
|
-
publishCommittedSuspend(token, gate, scope, remoteHandle);
|
|
3716
|
+
publishCommittedSuspend(token, gate, scope, remoteHandle, cp.checkpointId);
|
|
3715
3717
|
try {
|
|
3716
3718
|
await sessions.pin?.(sessionId);
|
|
3717
3719
|
}
|
|
@@ -4006,7 +4008,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4006
4008
|
if (!committed.ok) {
|
|
4007
4009
|
return committed.reason !== undefined ? { parkFailed: committed.reason } : undefined;
|
|
4008
4010
|
}
|
|
4009
|
-
publishCommittedSuspend(token, gate, cp.scope, remoteHandle);
|
|
4011
|
+
publishCommittedSuspend(token, gate, cp.scope, remoteHandle, cp.checkpointId);
|
|
4010
4012
|
try {
|
|
4011
4013
|
await sessions.pin?.(sessionId);
|
|
4012
4014
|
}
|
|
@@ -3472,6 +3472,7 @@ export class Runner {
|
|
|
3472
3472
|
suspendRef: prepared.suspendRef.token !== undefined && prepared.suspendRef.gate !== undefined
|
|
3473
3473
|
? {
|
|
3474
3474
|
token: prepared.suspendRef.token,
|
|
3475
|
+
...(prepared.suspendRef.checkpointId !== undefined ? { checkpointId: prepared.suspendRef.checkpointId } : {}),
|
|
3475
3476
|
gate: prepared.suspendRef.gate,
|
|
3476
3477
|
...(prepared.suspendRef.restoreMode !== undefined ? { restoreMode: prepared.suspendRef.restoreMode } : {}),
|
|
3477
3478
|
}
|
|
@@ -3479,6 +3480,7 @@ export class Runner {
|
|
|
3479
3480
|
reviewRef: prepared.reviewRef.token !== undefined && prepared.reviewRef.gate !== undefined
|
|
3480
3481
|
? {
|
|
3481
3482
|
token: prepared.reviewRef.token,
|
|
3483
|
+
...(prepared.reviewRef.checkpointId !== undefined ? { checkpointId: prepared.reviewRef.checkpointId } : {}),
|
|
3482
3484
|
gate: prepared.reviewRef.gate,
|
|
3483
3485
|
...(prepared.reviewRef.restoreMode !== undefined ? { restoreMode: prepared.reviewRef.restoreMode } : {}),
|
|
3484
3486
|
}
|