@sema-agent/core 7.19.0 → 7.20.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 +183 -2
- package/dist/agents/subagent.js +12 -20
- package/dist/agents/teacher.js +2 -5
- package/dist/agents/verify.js +21 -10
- package/dist/brain/anthropic.js +3 -1
- package/dist/brain/open-responses.js +4 -2
- package/dist/brain/openai.js +3 -1
- package/dist/brain/stream-engine.d.ts +4 -0
- package/dist/brain/stream-engine.js +4 -0
- package/dist/core/auto-mode.d.ts +37 -18
- package/dist/core/auto-mode.js +1 -1
- package/dist/core/child-clamp-seats.d.ts +79 -0
- package/dist/core/child-clamp-seats.js +29 -0
- package/dist/core/engine-notice.d.ts +67 -49
- package/dist/core/fs-write-gate-policy.d.ts +17 -9
- package/dist/core/fs-write-gate-policy.js +51 -4
- package/dist/core/gate-bounded-await.d.ts +38 -0
- package/dist/core/gate-bounded-await.js +53 -0
- package/dist/core/gate-fold.js +16 -48
- package/dist/core/gate-lanes.js +11 -3
- package/dist/core/gate-outcome.d.ts +9 -6
- package/dist/core/governance-codes.d.ts +16 -1
- package/dist/core/governance-codes.js +12 -0
- package/dist/core/hooks.d.ts +8 -8
- package/dist/core/memory-engine/engine.d.ts +14 -9
- package/dist/core/memory-engine/engine.js +21 -8
- package/dist/core/memory-engine/layout.d.ts +31 -0
- package/dist/core/memory-engine/layout.js +68 -3
- package/dist/core/memory-engine/types.d.ts +24 -0
- package/dist/core/memory.d.ts +19 -0
- package/dist/core/memory.js +12 -0
- package/dist/core/remote-env.d.ts +32 -0
- package/dist/core/remote-env.js +4 -1
- package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
- package/dist/core/runner/advertised-writable-dirs.js +51 -3
- package/dist/core/runner/announce-once-ledger.d.ts +15 -0
- package/dist/core/runner/announce-once-ledger.js +8 -0
- package/dist/core/runner/classifier-ladder.d.ts +5 -0
- package/dist/core/runner/classifier-ladder.js +13 -2
- package/dist/core/runner/contracts.d.ts +44 -1
- package/dist/core/runner/denial-limit-arms.d.ts +14 -1
- package/dist/core/runner/denial-limit-arms.js +5 -4
- package/dist/core/runner/env-capability-census.d.ts +30 -0
- package/dist/core/runner/env-capability-census.js +38 -0
- package/dist/core/runner/forward-sink.d.ts +32 -0
- package/dist/core/runner/forward-sink.js +41 -0
- package/dist/core/runner/gate-exit.d.ts +4 -0
- package/dist/core/runner/gate-exit.js +4 -1
- package/dist/core/runner/git-status-frame.d.ts +0 -9
- package/dist/core/runner/listing-fold.d.ts +57 -0
- package/dist/core/runner/listing-fold.js +75 -0
- package/dist/core/runner/prepare-execution-env.d.ts +1 -1
- package/dist/core/runner/prepare-execution-env.js +2 -0
- package/dist/core/runner/prepare-gate-stations.d.ts +8 -0
- package/dist/core/runner/prepare-gate-stations.js +6 -2
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
- package/dist/core/runner/prepare-hands-readface.js +3 -5
- package/dist/core/runner/prepare-listings.d.ts +3 -0
- package/dist/core/runner/prepare-listings.js +4 -2
- package/dist/core/runner/prepare-memory.js +7 -1
- package/dist/core/runner/prepare-policy-chain.js +5 -5
- package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
- package/dist/core/runner/prepare-run-refs.d.ts +4 -2
- package/dist/core/runner/prepare-run-refs.js +5 -4
- package/dist/core/runner/prepare-task.js +7 -5
- package/dist/core/runner/run-clock-and-content.js +3 -7
- package/dist/core/runner/run-harness-handlers.js +10 -10
- package/dist/core/runner/run-leg.js +28 -19
- package/dist/core/runner/run-settle-and-teardown.js +2 -5
- package/dist/core/runner/run-turn-boundary.js +44 -33
- package/dist/core/runner/runtask.js +2 -1
- package/dist/core/runner/synthetic-tools.d.ts +13 -39
- package/dist/core/runner/synthetic-tools.js +15 -59
- package/dist/core/runner/task-root-canonical.d.ts +45 -0
- package/dist/core/runner/task-root-canonical.js +18 -0
- package/dist/core/runner/turn-attachments.d.ts +125 -49
- package/dist/core/runner/turn-attachments.js +124 -66
- package/dist/core/safe-notify.d.ts +5 -0
- package/dist/core/safe-notify.js +24 -2
- package/dist/core/sensitive-path-policy.d.ts +53 -27
- package/dist/core/sensitive-path-policy.js +16 -26
- package/dist/core/skill-listing-budget.d.ts +34 -40
- package/dist/core/skill-listing-budget.js +25 -26
- package/dist/core/skills-directory.js +17 -10
- package/dist/core/task-registry.js +2 -1
- package/dist/core/task-spec.d.ts +8 -7
- package/dist/core/tool-policy.d.ts +12 -1
- package/dist/core/tool-policy.js +39 -25
- package/dist/core/write-protect.d.ts +98 -18
- package/dist/core/write-protect.js +88 -34
- package/dist/index.d.ts +6 -5
- package/dist/index.js +5 -4
- package/dist/orchestration/workflow-primitives.js +10 -20
- package/dist/stores/file/memory-store.d.ts +5 -2
- package/dist/stores/file/memory-store.js +8 -4
- package/dist/tools/fs/bash-readonly-classifier.d.ts +13 -2
- package/dist/tools/fs/bash-readonly-classifier.js +317 -35
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +12 -25
- package/dist/tools/fs/fs-shared.d.ts +22 -0
- package/dist/tools/fs/fs-shared.js +30 -0
- package/dist/tools/fs/index.d.ts +1 -1
- package/dist/tools/fs/index.js +1 -1
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +19 -3
- package/wire-changes/core@7.20.0.json +568 -0
|
@@ -272,6 +272,12 @@ export interface QuarantineOutcome {
|
|
|
272
272
|
dest?: string;
|
|
273
273
|
/** True ⇔ the model-visible file no longer holds the suspect content (deleted, or tombstoned). */
|
|
274
274
|
removed: boolean;
|
|
275
|
+
/** WHAT this containment left at the model-visible path, which is the only thing a following restore
|
|
276
|
+
* may overwrite (see {@link restoreContainedBaseline}). ABSENT ⇔ the path was freed. Present ⇔ either
|
|
277
|
+
* this engine's own tombstone (the clear-in-place rung) or the suspect bytes themselves (neither rung
|
|
278
|
+
* worked) — in both cases replacing it takes nobody else's work, and in neither case may anything the
|
|
279
|
+
* restore did not put there be destroyed. */
|
|
280
|
+
leftOnPlane?: string;
|
|
275
281
|
/** Set on ANY deviation from the clean capture+delete path — never a silent best-effort downgrade. */
|
|
276
282
|
detail?: string;
|
|
277
283
|
}
|
|
@@ -313,6 +319,31 @@ export interface QuarantineOutcome {
|
|
|
313
319
|
* `O_TRUNC` already destroyed the destination) is the failure this guard exists to prevent.
|
|
314
320
|
*/
|
|
315
321
|
export declare function writeFileNoFollow(path: string, content: string): void;
|
|
322
|
+
/** {@link writeFileNoFollow}'s NO-REPLACE twin: write `content` only if nothing is at `path`. `"written"`
|
|
323
|
+
* when the bytes landed, `"kept"` when it was occupied; every other failure throws like the twin (a
|
|
324
|
+
* write that could not be ATTEMPTED must not read as one that chose not to). `O_EXCL | O_CREAT` is the
|
|
325
|
+
* atomic form — an `existsSync`-then-write pair is a TOCTOU with exactly the window this closes — and
|
|
326
|
+
* it refuses a symlink at the final component on its own (POSIX `EEXIST` on a link, dangling or not),
|
|
327
|
+
* so the twin's no-follow property is kept without a second check. */
|
|
328
|
+
export declare function writeFileNoReplace(path: string, content: string): "written" | "kept";
|
|
329
|
+
/**
|
|
330
|
+
* The ONE way a committed baseline goes back after {@link quarantineAndTombstone} emptied its seat —
|
|
331
|
+
* the inverse half of that function, beside it so the removal word and its readers stay in one module.
|
|
332
|
+
* Both containment arms (the capture-residue sweep and the polluted-harvest record loop) call it, and
|
|
333
|
+
* the rule is ONE sentence: THE RESTORE MAY ONLY OVERWRITE WHAT THE CONTAINMENT ITSELF LEFT AT THE PATH.
|
|
334
|
+
* The removal's own account of what it left (`leftOnPlane` — nothing, our tombstone, or the suspect
|
|
335
|
+
* bytes) is re-read and compared immediately before the write, through a read that refuses a link, a
|
|
336
|
+
* pipe and anything but a regular file: a caller that awaited the committed store cannot assume what
|
|
337
|
+
* the removal saw still holds, on ANY of the three arms. Anything else — a freed path, a path somebody
|
|
338
|
+
* took back, a path that could not be verified — takes the no-replace write, which is the right answer
|
|
339
|
+
* for all of them at once: it lands iff nothing is there. Residual window: the read-to-open gap, with
|
|
340
|
+
* nothing suspended in it.
|
|
341
|
+
*
|
|
342
|
+
* Declining never weakens containment. Where the suspect bytes were removed they are already in
|
|
343
|
+
* quarantine; where they were NOT, the only way the comparison fails is that somebody replaced them —
|
|
344
|
+
* which took them off the plane as surely as the removal would have.
|
|
345
|
+
*/
|
|
346
|
+
export declare function restoreContainedBaseline(path: string, committed: string, leftOnPlane: string | undefined): "written" | "kept" | "failed";
|
|
316
347
|
export declare function quarantineAndTombstone(path: string, content: string, quarantineDir: string, now: () => number): QuarantineOutcome;
|
|
317
348
|
/** S2-B — the queued memory-change announcements (control plane; drained at session-first inject). */
|
|
318
349
|
export declare const ANNOUNCEMENTS_FILE = "announcements.json";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { chmodSync, closeSync, constants as fsConstants, copyFileSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync, writeSync } from "node:fs";
|
|
2
|
-
const { O_WRONLY, O_CREAT, O_TRUNC, O_NOFOLLOW, O_EXCL } = fsConstants;
|
|
1
|
+
import { chmodSync, closeSync, constants as fsConstants, copyFileSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync, writeSync } from "node:fs";
|
|
2
|
+
const { O_WRONLY, O_RDONLY, O_CREAT, O_TRUNC, O_NOFOLLOW, O_NONBLOCK, O_EXCL } = fsConstants;
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
@@ -740,6 +740,63 @@ export function writeFileNoFollow(path, content) {
|
|
|
740
740
|
closeSync(fd);
|
|
741
741
|
}
|
|
742
742
|
}
|
|
743
|
+
export function writeFileNoReplace(path, content) {
|
|
744
|
+
let fd;
|
|
745
|
+
try {
|
|
746
|
+
fd = openSync(path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0o644);
|
|
747
|
+
}
|
|
748
|
+
catch (err) {
|
|
749
|
+
if (err.code === "EEXIST")
|
|
750
|
+
return "kept";
|
|
751
|
+
throw err;
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
writeFileSync(fd, content, "utf8");
|
|
755
|
+
}
|
|
756
|
+
finally {
|
|
757
|
+
closeSync(fd);
|
|
758
|
+
}
|
|
759
|
+
return "written";
|
|
760
|
+
}
|
|
761
|
+
function readForVerification(path, maxBytes) {
|
|
762
|
+
let fd;
|
|
763
|
+
try {
|
|
764
|
+
fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
if (!fstatSync(fd).isFile())
|
|
771
|
+
return undefined;
|
|
772
|
+
const buf = Buffer.allocUnsafe(maxBytes);
|
|
773
|
+
let got = 0;
|
|
774
|
+
for (;;) {
|
|
775
|
+
const n = readSync(fd, buf, got, maxBytes - got, got);
|
|
776
|
+
if (n <= 0 || got + n >= maxBytes)
|
|
777
|
+
return buf.subarray(0, got + Math.max(0, n)).toString("utf8");
|
|
778
|
+
got += n;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
return undefined;
|
|
783
|
+
}
|
|
784
|
+
finally {
|
|
785
|
+
closeSync(fd);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
export function restoreContainedBaseline(path, committed, leftOnPlane) {
|
|
789
|
+
try {
|
|
790
|
+
if (leftOnPlane === undefined || readForVerification(path, Buffer.byteLength(leftOnPlane) + 1) !== leftOnPlane) {
|
|
791
|
+
return writeFileNoReplace(path, committed);
|
|
792
|
+
}
|
|
793
|
+
writeFileNoFollow(path, committed);
|
|
794
|
+
return "written";
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
return "failed";
|
|
798
|
+
}
|
|
799
|
+
}
|
|
743
800
|
let quarantineSeq = 0;
|
|
744
801
|
export function quarantineAndTombstone(path, content, quarantineDir, now) {
|
|
745
802
|
let captured;
|
|
@@ -767,14 +824,17 @@ export function quarantineAndTombstone(path, content, quarantineDir, now) {
|
|
|
767
824
|
catch (err) {
|
|
768
825
|
detail = `quarantine copy failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
769
826
|
}
|
|
827
|
+
let left = content;
|
|
770
828
|
let removed = false;
|
|
771
829
|
try {
|
|
772
830
|
rmSync(path, { force: true });
|
|
831
|
+
left = undefined;
|
|
773
832
|
removed = true;
|
|
774
833
|
}
|
|
775
834
|
catch (rmErr) {
|
|
776
835
|
try {
|
|
777
836
|
writeFileNoFollow(path, DELETED_TOMBSTONE);
|
|
837
|
+
left = DELETED_TOMBSTONE;
|
|
778
838
|
removed = true;
|
|
779
839
|
detail = `${detail !== undefined ? `${detail}; ` : ""}delete failed (${rmErr instanceof Error ? rmErr.message : String(rmErr)}) — tombstoned in place`;
|
|
780
840
|
}
|
|
@@ -782,7 +842,12 @@ export function quarantineAndTombstone(path, content, quarantineDir, now) {
|
|
|
782
842
|
detail = `${detail !== undefined ? `${detail}; ` : ""}suspect file could not be deleted or cleared: ${clearErr instanceof Error ? clearErr.message : String(clearErr)}`;
|
|
783
843
|
}
|
|
784
844
|
}
|
|
785
|
-
return {
|
|
845
|
+
return {
|
|
846
|
+
...(captured !== undefined ? { dest: captured } : {}),
|
|
847
|
+
removed,
|
|
848
|
+
...(left !== undefined ? { leftOnPlane: left } : {}),
|
|
849
|
+
...(detail !== undefined ? { detail } : {}),
|
|
850
|
+
};
|
|
786
851
|
}
|
|
787
852
|
export const ANNOUNCEMENTS_FILE = "announcements.json";
|
|
788
853
|
export const MEMORY_ANNOUNCEMENTS_MAX = 20;
|
|
@@ -549,6 +549,21 @@ export interface MemoryAnnouncement {
|
|
|
549
549
|
* the stored strings are already render-safe; the render step only adds the trusted lead/frame. */
|
|
550
550
|
items: string[];
|
|
551
551
|
}
|
|
552
|
+
/** What `MemoryEngine.sweepSessionCaptureResidue` reports: the paths it contained, the baselines it put
|
|
553
|
+
* back, the ones it DECLINED to (`restoreContainedBaseline`), and the paths it could not contain. */
|
|
554
|
+
export interface SessionCaptureResidueSweep {
|
|
555
|
+
swept: string[];
|
|
556
|
+
restored: string[];
|
|
557
|
+
failures: Array<{
|
|
558
|
+
path: string;
|
|
559
|
+
detail: string;
|
|
560
|
+
}>;
|
|
561
|
+
withheld: Array<{
|
|
562
|
+
path: string;
|
|
563
|
+
entryId?: string;
|
|
564
|
+
rev?: string;
|
|
565
|
+
}>;
|
|
566
|
+
}
|
|
552
567
|
/**
|
|
553
568
|
* design/138 §2.4 — the harvest outcome. Rejections are MODEL-VISIBLE gate events: S1 delivers the
|
|
554
569
|
* structure; the runner's boundary-announcement wiring (attachments member) lands in S2 (TODO(S2)).
|
|
@@ -626,6 +641,15 @@ export interface HarvestReport {
|
|
|
626
641
|
* storage), so those paths appear here beside {@link containment}.heldInstruction, never in
|
|
627
642
|
* `missing` (the file never vanished — its edit is in custody). */
|
|
628
643
|
restored: string[];
|
|
644
|
+
/** The other outcome of the same restore: a path whose committed baseline was NOT written back, because
|
|
645
|
+
* another writer held the file when the restore reached it. The newer bytes stand, the baseline is
|
|
646
|
+
* still in the store, and this row is how a host learns which paths are in that state. ABSENT/empty
|
|
647
|
+
* ⇔ every restore landed; `rev` is the declined generation, when it could be derived. */
|
|
648
|
+
restoreWithheld?: Array<{
|
|
649
|
+
path: string;
|
|
650
|
+
entryId?: string;
|
|
651
|
+
rev?: string;
|
|
652
|
+
}>;
|
|
629
653
|
/** Index self-heal notes (L8): suspected-duplicate index lines kept-with-warning, cleared orphans. */
|
|
630
654
|
warnings: string[];
|
|
631
655
|
/** M7 harvest budget: set when the deadline/file budget cut the harvest short. `pending` files were
|
package/dist/core/memory.d.ts
CHANGED
|
@@ -275,6 +275,25 @@ export declare class MemoryGateError extends Error {
|
|
|
275
275
|
readonly code: string;
|
|
276
276
|
constructor(code: string, detail?: string);
|
|
277
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* The same SHAPE as {@link MemoryGateError} — a stable `code` a caller switches on — for the opposite
|
|
280
|
+
* PHASE of a memory write, and deliberately NOT that class: a gate error means the write was REFUSED
|
|
281
|
+
* and never reached the backend, so a host branching on it would take exactly the wrong action here.
|
|
282
|
+
*
|
|
283
|
+
* @contract memory.projection_failure_is_post_durable — a store throws this ONLY after the note's own
|
|
284
|
+
* event is durably appended and fsync'd, so `committed` is always true and the note survives a restart:
|
|
285
|
+
* a caller that retries the write on this error stores the note a second time. Only the derived
|
|
286
|
+
* projection is missing, and the next mutation of the same scope rewrites it whole.
|
|
287
|
+
*/
|
|
288
|
+
export declare class MemoryProjectionError extends Error {
|
|
289
|
+
/** Stable, switchable — the phase this failure belongs to. */
|
|
290
|
+
readonly code = "memory.projection_failed";
|
|
291
|
+
/** Always true: this error only exists AFTER the durable append was fsync'd. Never retry the write. */
|
|
292
|
+
readonly committed = true;
|
|
293
|
+
/** The projection file that could not be rewritten. */
|
|
294
|
+
readonly path: string;
|
|
295
|
+
constructor(path: string, cause: unknown);
|
|
296
|
+
}
|
|
278
297
|
/**
|
|
279
298
|
* Scan `text` for a high-confidence credential (CC-parity P0-2). Returns the matched pattern's stable
|
|
280
299
|
* `label` (NEVER the matched secret value — labels are safe to log/surface), or `null` when clean.
|
package/dist/core/memory.js
CHANGED
|
@@ -22,6 +22,18 @@ export class MemoryGateError extends Error {
|
|
|
22
22
|
this.code = code;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
export class MemoryProjectionError extends Error {
|
|
26
|
+
code = "memory.projection_failed";
|
|
27
|
+
committed = true;
|
|
28
|
+
path;
|
|
29
|
+
constructor(path, cause) {
|
|
30
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
31
|
+
super(`memory.projection_failed: the note IS durably committed — only the derived projection ${path} could not be rewritten: ${detail}. ` +
|
|
32
|
+
`Do not retry the write (it would store the note twice); the projection is rebuilt by the next mutation of this scope.`, cause !== undefined ? { cause } : undefined);
|
|
33
|
+
this.name = "MemoryProjectionError";
|
|
34
|
+
this.path = path;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
25
37
|
function hasPromotableMarker(note) {
|
|
26
38
|
return note.outcomeStats !== undefined;
|
|
27
39
|
}
|
|
@@ -316,6 +316,16 @@ export interface RemoteExecutionEnv extends ExecutionEnv {
|
|
|
316
316
|
* without durable suspend instead (the human gate refuses it), or extend capabilities with an
|
|
317
317
|
* explicit workspace-durability flag before building one. */
|
|
318
318
|
suspendable: boolean;
|
|
319
|
+
/** True iff this adapter's {@link FileSystem.canonicalPath} is the AUTHORITY on where a path lands —
|
|
320
|
+
* it answers for the very filesystem the commands run on, and a failure to answer is a failure of
|
|
321
|
+
* the resolver rather than a namespace it cannot see into. Optional, and absent is not a
|
|
322
|
+
* declaration: an adapter that says nothing keeps the reading every adapter has had, in which a
|
|
323
|
+
* chain nothing could be established about leaves the lexical verdict standing. Declaring it says
|
|
324
|
+
* the opposite — that an unanswerable chain is a question, not an absence — and the read
|
|
325
|
+
* boundary's execute-time recheck then refuses rather than passing the written spelling through.
|
|
326
|
+
* Read STRICTLY through {@link canonicalPathIsAuthoritative}, like every other capability whose
|
|
327
|
+
* `true` changes what a containment leg does; a wrong-SHAPE value reads absent and is announced. */
|
|
328
|
+
canonicalPathAuthoritative?: boolean;
|
|
319
329
|
};
|
|
320
330
|
/** Identity of the connected workspace (design/48 §5 gap 1). Synchronous: it is data the env already holds. */
|
|
321
331
|
workspaceHandle(): WorkspaceHandle;
|
|
@@ -507,6 +517,9 @@ export declare function missingRestoreSurface(env: ExecutionEnv): readonly ("res
|
|
|
507
517
|
*
|
|
508
518
|
* A composite type guard (structural AND capability) so a `suspendVM` call site narrowed by THIS predicate
|
|
509
519
|
* gets the {@link RemoteExecutionEnv} type without a cast — `true` always implies the structural check too.
|
|
520
|
+
* The capability is read STRICTLY, like the containment ones: a non-boolean reads as NOT suspendable and
|
|
521
|
+
* the census announces it. This took anything truthy, so `suspendable: "false"` was snapshotted — the one
|
|
522
|
+
* key of the three whose malformed reading fell on the unsafe side.
|
|
510
523
|
*
|
|
511
524
|
* It also requires the RESTORE surface ({@link missingRestoreSurface}). A suspend is only durable
|
|
512
525
|
* if something can undo it; an adapter that offers `suspendVM` but no `resumeVM` used to pass this guard,
|
|
@@ -549,3 +562,22 @@ export declare function isIsolated(env: ExecutionEnv): boolean;
|
|
|
549
562
|
* answers exactly, and reading the shape as containment made that deployment's boundary lexical-only.
|
|
550
563
|
*/
|
|
551
564
|
export declare function boundaryIsTheSandbox(env: ExecutionEnv): boolean;
|
|
565
|
+
/**
|
|
566
|
+
* Does this env DECLARE that its `canonicalPath` is the authority on where a path lands?
|
|
567
|
+
*
|
|
568
|
+
* The question the execute-time read boundary asks about its own resolver's SILENCE. Resolving a
|
|
569
|
+
* spelling walks it from the deepest answerable ancestor; when no ancestor answers at all, two
|
|
570
|
+
* different worlds produce the same `undefined`: a resolver that is authoritative and is FAILING (an
|
|
571
|
+
* RPC into the machine the command will run on, a transport that dropped), and an env that simply does
|
|
572
|
+
* not answer path questions (an in-memory double, an adapter with no resolution surface). The first is
|
|
573
|
+
* a question and the second is an absence, and reading both as the absence is what leaves a containment
|
|
574
|
+
* leg silently open on an RPC-backed lane.
|
|
575
|
+
*
|
|
576
|
+
* So the adapter says which it is, and says it the way {@link isIsolated} is said — strictly, because a
|
|
577
|
+
* `true` here makes a containment leg REFUSE where it used to pass. ABSENT is the reading every adapter
|
|
578
|
+
* has had, so no existing env moves; a PRESENT non-boolean reads absent HERE too but is announced at the
|
|
579
|
+
* env's admission point, once per key — this reader does not validate. A non-remote env keeps the same
|
|
580
|
+
* reading, which is why the local lane is unchanged: its resolver answers `/`, so the arm this gates
|
|
581
|
+
* has no case to fire on.
|
|
582
|
+
*/
|
|
583
|
+
export declare function canonicalPathIsAuthoritative(env: ExecutionEnv): boolean;
|
package/dist/core/remote-env.js
CHANGED
|
@@ -37,7 +37,7 @@ export function missingRestoreSurface(env) {
|
|
|
37
37
|
return missing;
|
|
38
38
|
}
|
|
39
39
|
export function isSuspendable(env) {
|
|
40
|
-
return isRemoteExecutionEnv(env) && env.capabilities.suspendable && missingRestoreSurface(env).length === 0;
|
|
40
|
+
return isRemoteExecutionEnv(env) && env.capabilities.suspendable === true && missingRestoreSurface(env).length === 0;
|
|
41
41
|
}
|
|
42
42
|
export function isIsolated(env) {
|
|
43
43
|
return isRemoteExecutionEnv(env) ? env.capabilities.isolation === true : false;
|
|
@@ -45,3 +45,6 @@ export function isIsolated(env) {
|
|
|
45
45
|
export function boundaryIsTheSandbox(env) {
|
|
46
46
|
return isIsolated(env);
|
|
47
47
|
}
|
|
48
|
+
export function canonicalPathIsAuthoritative(env) {
|
|
49
|
+
return isRemoteExecutionEnv(env) ? env.capabilities.canonicalPathAuthoritative === true : false;
|
|
50
|
+
}
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
* planted path is never written through); an advertiser whose directory is created elsewhere passes
|
|
21
21
|
* no marker and an absent directory narrows.
|
|
22
22
|
*
|
|
23
|
+
* A directory the fence already holds is never admitted as a HOLE in it: a candidate that resolves onto
|
|
24
|
+
* a root — or above one — would turn that whole root into the advertised, write-gate-exempt domain, so
|
|
25
|
+
* an ordinary source write inside the workspace would stop asking. One predicate
|
|
26
|
+
* ({@link swallowedFenceRoot}) states that, and the write gate's configured domains apply the same one.
|
|
27
|
+
*
|
|
23
28
|
* Reverse check (the seat's own invariant): nothing enters `roots` except through `admit`, and every
|
|
24
29
|
* caller of `admit` is an advertiser deciding its advertisement off the returned verdict — so no
|
|
25
30
|
* directory reaches the ROOTS FENCE through this seat without the prompt advertising it, and none
|
|
@@ -55,12 +60,65 @@ export interface AdvertisedWritableDir {
|
|
|
55
60
|
}
|
|
56
61
|
/** The skip announcement — the one loud outlet for a directory that was advertised but could not be admitted. */
|
|
57
62
|
export type AdvertisedWritableDirSkipped = (dir: AdvertisedWritableDir, reason: string) => void;
|
|
63
|
+
/**
|
|
64
|
+
* ONE rule for both halves of the auto-approved write surface: a directory offered as a WRITABLE DOMAIN
|
|
65
|
+
* (an advertised writable directory here; a configured `exemptDirs`/`acceptDirs` entry at the write
|
|
66
|
+
* gate) must not SWALLOW a root the fence already holds. Returns the first root the candidate equals or
|
|
67
|
+
* is an ancestor of — the one to name in the refusal — else `undefined`.
|
|
68
|
+
*
|
|
69
|
+
* Why a domain that contains a root is not a domain: the point of the domain is to exempt a small area
|
|
70
|
+
* the prompt (or the deployment) named. A candidate resolving onto the task root — a symlinked
|
|
71
|
+
* scratchpad or memory home is all it takes, and the resolution is what makes it invisible to the
|
|
72
|
+
* spelling — exempts the ENTIRE workspace instead, turning every ordinary source write from `ask` into
|
|
73
|
+
* `allow`. Refusing the admission is the fix; a special case inside the gate ("ignore this domain when
|
|
74
|
+
* it contains the root") would be the same rule written twice, in the place where it is easiest to
|
|
75
|
+
* forget. Both arguments must already be CANONICAL — this compares spellings, it does not resolve them.
|
|
76
|
+
*/
|
|
77
|
+
export declare function swallowedFenceRoot(domainCanonical: string, fenceRootsCanonical: readonly string[]): string | undefined;
|
|
58
78
|
export declare class AdvertisedWritableDirs {
|
|
59
79
|
private readonly env;
|
|
60
80
|
private readonly onSkipped;
|
|
81
|
+
/** The extra fence roots the SPEC declares (`additionalDirectories` / `additionalReadDirectories`), as
|
|
82
|
+
* spelled. Resolved by this seat rather than awaited from the hands phase — the memory advertiser is
|
|
83
|
+
* admitted BEFORE that phase, so a list that only grew there made the rule depend on phase order. An
|
|
84
|
+
* entry that does not resolve is not a fence root at all (the hands phase reads the SAME memoized
|
|
85
|
+
* answer and skips it too, announcing it there), so there is nothing here for it to guard. */
|
|
86
|
+
private readonly declaredRootSpellings;
|
|
61
87
|
private readonly admitted;
|
|
62
88
|
private readonly verdicts;
|
|
63
|
-
|
|
89
|
+
/** The CANONICAL roots the fence holds: the task root at construction, plus every root the spec
|
|
90
|
+
* DECLARES, resolved by this seat before it judges anything (see {@link resolveDeclaredRoots}) —
|
|
91
|
+
* so which advertiser runs first cannot decide what the rule sees. */
|
|
92
|
+
private readonly fenceRoots;
|
|
93
|
+
/** Latched after the declared spellings have been resolved once (the resolution is per seat, not per call). */
|
|
94
|
+
private declaredRootsResolved;
|
|
95
|
+
/** ONE answer per declared spelling, for this seat AND for the hands phase that folds the same entries
|
|
96
|
+
* into the allowlist ({@link canonicalizeDeclaredRoot}): two independent lookups of one path can
|
|
97
|
+
* DISAGREE, and a root outside this rule's list while inside the fence is exactly the gap. */
|
|
98
|
+
private readonly declaredRootAnswers;
|
|
99
|
+
constructor(env: ExecutionEnv, onSkipped: AdvertisedWritableDirSkipped,
|
|
100
|
+
/** The run's task root, CANONICAL (the driver's one resolution — see `task-root-canonical.ts`). */
|
|
101
|
+
taskRootCanonical: string,
|
|
102
|
+
/** The extra fence roots the SPEC declares (`additionalDirectories` / `additionalReadDirectories`), as
|
|
103
|
+
* spelled. Resolved by this seat rather than awaited from the hands phase — the memory advertiser is
|
|
104
|
+
* admitted BEFORE that phase, so a list that only grew there made the rule depend on phase order. An
|
|
105
|
+
* entry that does not resolve is not a fence root at all (the hands phase reads the SAME memoized
|
|
106
|
+
* answer and skips it too, announcing it there), so there is nothing here for it to guard. */
|
|
107
|
+
declaredRootSpellings?: readonly string[]);
|
|
108
|
+
/** Declare one more CANONICAL root the fence holds (an `additionalDirectories` /
|
|
109
|
+
* `additionalReadDirectories` entry, once the hands phase has resolved it). Declaring is
|
|
110
|
+
* idempotent and order-free; a root declared after an advertiser was judged does not re-open it. */
|
|
111
|
+
declareFenceRoot(canonical: string): void;
|
|
112
|
+
/**
|
|
113
|
+
* The canonical form of a DECLARED fence-root spelling — resolved at most once per spelling and
|
|
114
|
+
* served from the memo thereafter. The hands phase calls this instead of canonicalizing the same
|
|
115
|
+
* entries again, so the list this rule judges against and the allowlist the fence ends up holding
|
|
116
|
+
* are built from ONE answer per path: a lookup that fails for this seat fails for that phase too,
|
|
117
|
+
* and a root cannot be outside the rule's view while being inside the fence.
|
|
118
|
+
*/
|
|
119
|
+
canonicalizeDeclaredRoot(spelled: string): Promise<Awaited<ReturnType<ExecutionEnv["canonicalPath"]>>>;
|
|
120
|
+
/** Resolve the spec-declared root spellings into {@link fenceRoots}, exactly once per seat. */
|
|
121
|
+
private resolveDeclaredRoots;
|
|
64
122
|
/** The admitted roots, CANONICAL, in admission order, deduplicated. The hands phase folds this list into the
|
|
65
123
|
* write-capable containment allowlist before the toolkit compiles; it is not a live view after that. */
|
|
66
124
|
get roots(): readonly string[];
|
|
@@ -1,11 +1,52 @@
|
|
|
1
|
+
import { isWithin } from "./session-rule-policy.js";
|
|
2
|
+
export function swallowedFenceRoot(domainCanonical, fenceRootsCanonical) {
|
|
3
|
+
for (const root of fenceRootsCanonical) {
|
|
4
|
+
if (root && isWithin(domainCanonical, root))
|
|
5
|
+
return root;
|
|
6
|
+
}
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
1
9
|
export class AdvertisedWritableDirs {
|
|
2
10
|
env;
|
|
3
11
|
onSkipped;
|
|
12
|
+
declaredRootSpellings;
|
|
4
13
|
admitted = [];
|
|
5
14
|
verdicts = new Map();
|
|
6
|
-
|
|
15
|
+
fenceRoots;
|
|
16
|
+
declaredRootsResolved;
|
|
17
|
+
declaredRootAnswers = new Map();
|
|
18
|
+
constructor(env, onSkipped, taskRootCanonical, declaredRootSpellings = []) {
|
|
7
19
|
this.env = env;
|
|
8
20
|
this.onSkipped = onSkipped;
|
|
21
|
+
this.declaredRootSpellings = declaredRootSpellings;
|
|
22
|
+
this.fenceRoots = [taskRootCanonical];
|
|
23
|
+
}
|
|
24
|
+
declareFenceRoot(canonical) {
|
|
25
|
+
if (canonical && !this.fenceRoots.includes(canonical))
|
|
26
|
+
this.fenceRoots.push(canonical);
|
|
27
|
+
}
|
|
28
|
+
async canonicalizeDeclaredRoot(spelled) {
|
|
29
|
+
const memo = this.declaredRootAnswers.get(spelled);
|
|
30
|
+
if (memo !== undefined)
|
|
31
|
+
return memo;
|
|
32
|
+
const c = await this.env.canonicalPath(spelled);
|
|
33
|
+
const first = this.declaredRootAnswers.get(spelled);
|
|
34
|
+
if (first !== undefined)
|
|
35
|
+
return first;
|
|
36
|
+
this.declaredRootAnswers.set(spelled, c);
|
|
37
|
+
if (c.ok)
|
|
38
|
+
this.declareFenceRoot(c.value);
|
|
39
|
+
return c;
|
|
40
|
+
}
|
|
41
|
+
resolveDeclaredRoots() {
|
|
42
|
+
this.declaredRootsResolved ??= (async () => {
|
|
43
|
+
for (const spelled of this.declaredRootSpellings) {
|
|
44
|
+
if (!spelled || !spelled.trim())
|
|
45
|
+
continue;
|
|
46
|
+
await this.canonicalizeDeclaredRoot(spelled);
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
return this.declaredRootsResolved;
|
|
9
50
|
}
|
|
10
51
|
get roots() {
|
|
11
52
|
return this.admitted;
|
|
@@ -41,8 +82,15 @@ export class AdvertisedWritableDirs {
|
|
|
41
82
|
this.onSkipped(dir, `the execution environment resolves it to ${c.value} while the advertiser's gates key on ${dir.expectCanonical} — a fence hole those gates cannot see is not opened`);
|
|
42
83
|
return false;
|
|
43
84
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
85
|
+
if (this.admitted.includes(c.value))
|
|
86
|
+
return true;
|
|
87
|
+
await this.resolveDeclaredRoots();
|
|
88
|
+
const swallowed = swallowedFenceRoot(c.value, [...this.fenceRoots, ...this.admitted]);
|
|
89
|
+
if (swallowed !== undefined) {
|
|
90
|
+
this.onSkipped(dir, `it resolves to ${c.value}, which covers ${swallowed === this.fenceRoots[0] ? "task root" : "root"} ${swallowed} — a directory that holds a root is not a hole in the fence but the whole of it, and admitting it would auto-approve every write under that root`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
this.admitted.push(c.value);
|
|
46
94
|
return true;
|
|
47
95
|
}
|
|
48
96
|
}
|
|
@@ -27,6 +27,21 @@ export interface AnnounceOnceSession {
|
|
|
27
27
|
getAnnouncedListing(): Promise<AnnouncedListingSnapshot | undefined>;
|
|
28
28
|
appendAnnouncedListing(snapshot: AnnouncedListingSnapshotInput): Promise<string>;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Flush the PARKED announced-listing snapshot write (`FrameReceiptLedger.listingSnapshotOwed`, set by a
|
|
32
|
+
* frame receipt's commit inside the `message_end` walk) at a serialization point — the boundary's end,
|
|
33
|
+
* the prompt's settle. Best-effort: a lost append is a duplicate announcement next run. Never while
|
|
34
|
+
* prepare's branch read failed: a whole-snapshot write would replace the branch's record with this
|
|
35
|
+
* leg's partial view (the flag is cleared either way — nothing is owed on such a leg).
|
|
36
|
+
*/
|
|
37
|
+
export declare function flushOwedAnnouncedSnapshot(prepared: {
|
|
38
|
+
frameReceipts: {
|
|
39
|
+
listingSnapshotOwed: boolean;
|
|
40
|
+
};
|
|
41
|
+
announcedSnapshotRecovered: boolean;
|
|
42
|
+
announcedListingsRef: AnnouncedListingSnapshotInput;
|
|
43
|
+
session: Pick<AnnounceOnceSession, "appendAnnouncedListing">;
|
|
44
|
+
}): Promise<void>;
|
|
30
45
|
/** The run-local ledger — see the module header for the two commit disciplines it serves. */
|
|
31
46
|
export interface AnnounceOnceLedger {
|
|
32
47
|
/** The nearest announced snapshot on the branch as read at prepare (undefined = pre-entry
|
|
@@ -29,6 +29,14 @@ export function withAnnounceDigests(existing, add) {
|
|
|
29
29
|
function sameFailure(a, b) {
|
|
30
30
|
return a.name === b.name && a.error === b.error;
|
|
31
31
|
}
|
|
32
|
+
export async function flushOwedAnnouncedSnapshot(prepared) {
|
|
33
|
+
if (!prepared.frameReceipts.listingSnapshotOwed)
|
|
34
|
+
return;
|
|
35
|
+
prepared.frameReceipts.listingSnapshotOwed = false;
|
|
36
|
+
if (!prepared.announcedSnapshotRecovered)
|
|
37
|
+
return;
|
|
38
|
+
await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
|
|
39
|
+
}
|
|
32
40
|
export async function loadAnnounceOnceLedger(session, checkpointMirror, onError) {
|
|
33
41
|
let recovered = true;
|
|
34
42
|
const entry = await session.getAnnouncedListing().catch(() => {
|
|
@@ -87,6 +87,11 @@ export type ClassifierSeat = {
|
|
|
87
87
|
* all ⇒ the main model, `main_only`.
|
|
88
88
|
* The relative pick follows the run's MAIN model on purpose: a side channel that resolved without it is the
|
|
89
89
|
* misfit the ticket names (a `flash` classifier above a `lite` main; two tiers below a `max` main).
|
|
90
|
+
*
|
|
91
|
+
* @contract classifier.seat.one_resolver — this function is the ONE answer to "which model judges this run's
|
|
92
|
+
* gated calls". The engine's arming calls it and a host that DISPLAYS the seat (a capabilities face, an
|
|
93
|
+
* operator pane) calls the same exported function rather than re-deriving the pick from catalog, roles and
|
|
94
|
+
* tiers: a second derivation is a second answer, and the two drift the moment either side's rules move.
|
|
90
95
|
*/
|
|
91
96
|
export declare function classifierSeatModel(args: {
|
|
92
97
|
deps: Pick<RunnerDeps, "models" | "roles" | "tiers">;
|
|
@@ -2,7 +2,7 @@ import { sameRouteIdentity } from "../../brain/route-adjudicator.js";
|
|
|
2
2
|
import { thinkingOffSpelled } from "../../brain/reasoning.js";
|
|
3
3
|
import { AUTO_MODE_CLASSIFIER_CONTRACT_BROKEN_AFTER, AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS, AUTO_MODE_CLASSIFIER_MAX_TOKENS, AUTO_MODE_CLASSIFIER_THINK_BUDGET_TOKENS } from "../auto-mode-defaults.js";
|
|
4
4
|
import { autoModeClassifierFallbackOf, parseAutoModeResponse } from "../auto-mode.js";
|
|
5
|
-
import { DEFAULT_TIER_ORDER, resolveTaskModel, roleSpecNamesModel } from "../roles.js";
|
|
5
|
+
import { DEFAULT_TIER_ORDER, expandTiers, resolveTaskModel, roleSpecNamesModel } from "../roles.js";
|
|
6
6
|
import { emitTrace } from "../trace.js";
|
|
7
7
|
import { deliverEngineNotice } from "../types.js";
|
|
8
8
|
export function classifierLadderRungs(knob, classifier, main) {
|
|
@@ -20,7 +20,8 @@ function sameRouteQuietly(a, b) {
|
|
|
20
20
|
}
|
|
21
21
|
export const CLASSIFIER_ROLE_DEFAULT_REASONS = ["tier_below_main", "tier_table", "role_fallback", "main_only", "role_unresolvable"];
|
|
22
22
|
export function classifierSeatModel(args) {
|
|
23
|
-
const {
|
|
23
|
+
const { main } = args;
|
|
24
|
+
const deps = normalizeSeatDeps(args.deps);
|
|
24
25
|
if (roleSpecNamesModel(deps.roles?.classifier)) {
|
|
25
26
|
try {
|
|
26
27
|
return { model: resolveTaskModel({ modelRole: "classifier" }, deps).model };
|
|
@@ -42,6 +43,16 @@ export function classifierSeatModel(args) {
|
|
|
42
43
|
return { model: main, defaulted: "main_only" };
|
|
43
44
|
}
|
|
44
45
|
}
|
|
46
|
+
function normalizeSeatDeps(deps) {
|
|
47
|
+
if (deps.tiers === undefined)
|
|
48
|
+
return deps;
|
|
49
|
+
try {
|
|
50
|
+
return { ...deps, models: expandTiers(deps.models, deps.tiers) };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return deps;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
function tierBelowMain(deps, main) {
|
|
46
57
|
if (deps.tiers === undefined || Object.keys(deps.tiers).length === 0 || deps.models === undefined)
|
|
47
58
|
return undefined;
|
|
@@ -173,6 +173,35 @@ export interface CommittedPause {
|
|
|
173
173
|
scope: string;
|
|
174
174
|
gatedCallId?: string;
|
|
175
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* One engine frame whose commit waits for the receipt of the message carrying it.
|
|
178
|
+
*
|
|
179
|
+
* @contract frames.commit_on_receipt — an engine-minted frame that rides a user message (the git-status
|
|
180
|
+
* frame, an `agent_listing` or `skills_listing` frame) is recorded as announced ONLY when the run loop's
|
|
181
|
+
* `message_end` walk sees a persisted user entry whose text equals the carrying text it was registered
|
|
182
|
+
* with; every receipt with that text settles on that one entry. Until then nothing is recorded — a
|
|
183
|
+
* message that never reaches the transcript (a pre-append failure, a suppressed or undrained steer, a
|
|
184
|
+
* leg that ends first) leaves the frame unannounced, and the next boundary or leg renders it again.
|
|
185
|
+
* The error asymmetry is one-directional by design: a lost receipt costs a duplicate frame, never a
|
|
186
|
+
* fact the model was recorded as told and was not.
|
|
187
|
+
*/
|
|
188
|
+
export interface FrameReceipt {
|
|
189
|
+
/** The carrying message's exact text (its first text block) — the receipt's match key. */
|
|
190
|
+
text: string;
|
|
191
|
+
/** Which frame this receipt settles. A boundary collector must not render a listing whose receipt is
|
|
192
|
+
* still pending (the frame is queued, not yet in the transcript). */
|
|
193
|
+
source: "git_status" | "agent_listing" | "skills_listing";
|
|
194
|
+
/** Runs inside the `message_end` walk (synchronous, never awaited): advances the run-local record
|
|
195
|
+
* and parks any session write for the next serialization point. */
|
|
196
|
+
commit: (entryId: string) => void;
|
|
197
|
+
}
|
|
198
|
+
/** The leg's frame-receipt ledger — see {@link FrameReceipt}. `listingSnapshotOwed` is the parked
|
|
199
|
+
* announced-listing snapshot write (a session append from inside the walk would race the loop's own
|
|
200
|
+
* next transcript append); the boundary's end and the prompt's settle flush it. */
|
|
201
|
+
export interface FrameReceiptLedger {
|
|
202
|
+
pending: FrameReceipt[];
|
|
203
|
+
listingSnapshotOwed: boolean;
|
|
204
|
+
}
|
|
176
205
|
export interface Prepared {
|
|
177
206
|
harness: AgentHarness;
|
|
178
207
|
/** The CONCRETE built-in session (engine-internal: prepare constructs/acquires `StoredSession` itself,
|
|
@@ -1017,6 +1046,7 @@ export interface Prepared {
|
|
|
1017
1046
|
toolName: string;
|
|
1018
1047
|
seedAnnounced: boolean;
|
|
1019
1048
|
models?: readonly string[];
|
|
1049
|
+
budget: ResolvedSkillListingBudget;
|
|
1020
1050
|
};
|
|
1021
1051
|
/** [c209-C] skills_listing counterpart of {@link agentListing} — the normalized skills METADATA
|
|
1022
1052
|
* projection (names/descriptions/attachment paths, never bodies; the exact list `createSkillTool`
|
|
@@ -1038,6 +1068,9 @@ export interface Prepared {
|
|
|
1038
1068
|
* announce-once-ledger.ts): seeded at prepare from the branch's record, advanced by the run loop
|
|
1039
1069
|
* on intact delivery of the tools_delta `failedServers` arm / the mcp_dropped_tools frame. */
|
|
1040
1070
|
announcedListingsRef: AnnouncedListingSnapshotInput;
|
|
1071
|
+
/** The frame-receipt ledger — the ONE commit point of every engine frame that rides a user message
|
|
1072
|
+
* (the git-status frame, the agent and skills listing frames). Always present; per leg. */
|
|
1073
|
+
frameReceipts: FrameReceiptLedger;
|
|
1041
1074
|
/** env-tail migration — the git-status frame lane's run-local state: this leg's
|
|
1042
1075
|
* resolved frame (probe outcome rendered + hashed at prepare), the announced `(kind, hash)`
|
|
1043
1076
|
* mirror the checkpoint serializer reads, the trim-protection slot the request-build context
|
|
@@ -1796,7 +1829,11 @@ export interface RunInternals {
|
|
|
1796
1829
|
* `forwardSubagentEvents: true` it ALSO forwards the child's content events (`text_delta` / `text_end` /
|
|
1797
1830
|
* `reasoning_delta` / `tool_start` / `tool_end` — the subagent viewing pane, carrying the same UNTRUSTED-RAW/consumer-must-redact
|
|
1798
1831
|
* contract as the main stream's tool events). Either way the child stream is NEVER merged into the parent's
|
|
1799
|
-
* MODEL context (this is purely a render channel). Absent unless the deployment opted in.
|
|
1832
|
+
* MODEL context (this is purely a render channel). Absent unless the deployment opted in. CONTAINED at
|
|
1833
|
+
* one door: every engine site feeding this seat goes through `forwardSafely` (`forward-sink.ts`), so a
|
|
1834
|
+
* sink that throws — or, since an `async` function satisfies this `=> void` type, one that returns a
|
|
1835
|
+
* REJECTING promise — drops its frame and nothing else, never awaited
|
|
1836
|
+
* (`@contract runtask.forward_sink_never_faults_the_run`).
|
|
1800
1837
|
*/
|
|
1801
1838
|
onForwardEvent?: (event: TaskEvent) => void;
|
|
1802
1839
|
/**
|
|
@@ -1815,6 +1852,12 @@ export interface RunInternals {
|
|
|
1815
1852
|
/** The run-local per-tool-call detach hub. runtask creates it and exposes
|
|
1816
1853
|
* `TaskStream.detach(toolCallId)`; the hands Bash tool threads `signalFor(toolCallId)` into env.exec. */
|
|
1817
1854
|
detachHub?: import("../tool-detach.js").ToolDetachHub;
|
|
1855
|
+
/** The run's forwarding-failure bookkeeping for {@link onForwardEvent} — ENGINE-owned and opaque,
|
|
1856
|
+
* minted once per run by `Runner.runTaskStream` and carried by every later COPY of this object (the
|
|
1857
|
+
* prepare re-spread, a placement-root rebind, a child's snapshot), which is what makes "announced once
|
|
1858
|
+
* per run" true across the lanes that forward. A caller value is overridden like the refs beside it;
|
|
1859
|
+
* absent ⇒ the door mints a per-object one, so containment never depends on the threading. */
|
|
1860
|
+
forwardScope?: import("./forward-sink.js").ForwardSinkScope;
|
|
1818
1861
|
/**
|
|
1819
1862
|
* The Runner's per-session read-file-state seats (CC parity: readFileState is
|
|
1820
1863
|
* session-scoped). Always set by the Runner's own prepare call (overriding any caller value, like
|