@sema-agent/core 7.21.1 → 7.23.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 +290 -1
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +2 -1
- package/dist/core/ask-origin.d.ts +1 -0
- package/dist/core/ask-origin.js +3 -2
- package/dist/core/auto-compaction.d.ts +2 -2
- package/dist/core/auto-mode-rebuild.js +6 -2
- package/dist/core/auto-mode.d.ts +40 -18
- package/dist/core/auto-mode.js +63 -45
- package/dist/core/checkpoint-store.d.ts +9 -13
- package/dist/core/context-edit.js +1 -1
- package/dist/core/engine-notice.d.ts +62 -1
- package/dist/core/engine-notice.js +14 -1
- package/dist/core/gate-bounded-await.d.ts +13 -3
- package/dist/core/gate-fold.js +3 -3
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +6 -0
- package/dist/core/mcp.d.ts +65 -0
- package/dist/core/mcp.js +190 -41
- package/dist/core/memory-engine/consolidation.d.ts +5 -0
- package/dist/core/memory-engine/consolidation.js +7 -3
- package/dist/core/memory-engine/engine.d.ts +8 -0
- package/dist/core/memory-engine/engine.js +25 -19
- package/dist/core/memory-engine/layout.d.ts +47 -9
- package/dist/core/memory-engine/layout.js +157 -57
- package/dist/core/memory-engine/session-incarnation.d.ts +27 -0
- package/dist/core/memory-engine/session-incarnation.js +63 -0
- package/dist/core/parent-spec-seat.d.ts +15 -3
- package/dist/core/parent-spec-seat.js +5 -0
- package/dist/core/park-selfcheck.js +1 -1
- package/dist/core/path-identity.d.ts +11 -4
- package/dist/core/path-identity.js +5 -2
- package/dist/core/permission-rule-model.d.ts +1 -2
- package/dist/core/read-root-candidate.js +11 -1
- package/dist/core/retention.d.ts +8 -1
- package/dist/core/retention.js +2 -0
- package/dist/core/runner/assemble-result.d.ts +2 -7
- package/dist/core/runner/assemble-result.js +25 -8
- package/dist/core/runner/classifier-ladder.js +1 -0
- package/dist/core/runner/contracts.d.ts +26 -15
- package/dist/core/runner/inherited-ask-grants.d.ts +2 -7
- package/dist/core/runner/inherited-ask-grants.js +1 -0
- package/dist/core/runner/mcp-redial.d.ts +109 -0
- package/dist/core/runner/mcp-redial.js +60 -0
- package/dist/core/runner/memory-consolidation-driver.d.ts +2 -0
- package/dist/core/runner/prepare-ask-lane.js +1 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +9 -12
- package/dist/core/runner/prepare-context-lane.d.ts +43 -1
- package/dist/core/runner/prepare-context-lane.js +25 -4
- package/dist/core/runner/prepare-hands-readface.d.ts +3 -0
- package/dist/core/runner/prepare-hands-readface.js +13 -3
- package/dist/core/runner/prepare-memory.js +3 -3
- package/dist/core/runner/prepare-protocol-tools.d.ts +6 -1
- package/dist/core/runner/prepare-protocol-tools.js +20 -38
- package/dist/core/runner/prepare-task.js +4 -4
- package/dist/core/runner/prepare-turn-wiring.js +2 -2
- package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
- package/dist/core/runner/prepare-wiring-manifest.js +2 -1
- package/dist/core/runner/run-harness-handlers.js +4 -2
- package/dist/core/runner/run-identity-wiring.d.ts +3 -1
- package/dist/core/runner/run-identity-wiring.js +4 -1
- package/dist/core/runner/run-recovery-lanes.js +6 -3
- package/dist/core/runner/run-settle-and-teardown.d.ts +3 -1
- package/dist/core/runner/run-settle-and-teardown.js +6 -2
- package/dist/core/runner/run-telemetry-and-budget-seats.js +11 -1
- package/dist/core/runner/run-terminal-adoption.d.ts +4 -1
- package/dist/core/runner/run-terminal-adoption.js +2 -6
- package/dist/core/runner/runtask.js +8 -5
- package/dist/core/runner/stream-lifecycle-verbs.d.ts +5 -1
- package/dist/core/runner/stream-lifecycle-verbs.js +23 -2
- package/dist/core/runner/write-protection-station.d.ts +7 -25
- package/dist/core/runner/write-protection-station.js +18 -0
- package/dist/core/runner-deps.d.ts +16 -7
- package/dist/core/skill-listing-budget.d.ts +5 -4
- package/dist/core/task-registry.d.ts +0 -6
- package/dist/core/task-result.d.ts +31 -12
- package/dist/core/task-spec.d.ts +2 -1
- package/dist/core/task-stream.d.ts +26 -0
- package/dist/core/tool-spec.d.ts +5 -1
- package/dist/core/trace.d.ts +5 -0
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/core/wiring-manifest.d.ts +23 -0
- package/dist/core/wiring-manifest.js +3 -0
- package/dist/core/write-protect.d.ts +64 -12
- package/dist/core/write-protect.js +88 -34
- package/dist/engine/execution-env/node-execution-env.d.ts +19 -2
- package/dist/engine/execution-env/node-execution-env.js +5 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -2
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +2 -2
- package/dist/tools/fs/bash-program-position.d.ts +26 -5
- package/dist/tools/fs/bash-program-position.js +63 -16
- package/dist/tools/fs/bash-readonly-classifier.d.ts +27 -17
- package/dist/tools/fs/bash-readonly-classifier.js +134 -55
- package/dist/tools/fs/fs-bash.js +43 -18
- package/dist/tools/fs/fs-read.js +2 -1
- package/dist/tools/fs/fs-shared.d.ts +33 -7
- package/dist/tools/fs/fs-shared.js +12 -1
- package/dist/tools/fs/index.d.ts +4 -2
- package/dist/tools/fs/read-deny.d.ts +25 -11
- package/dist/tools/fs/read-deny.js +5 -1
- package/dist/tools/fs/safety.d.ts +38 -4
- package/dist/tools/fs/safety.js +31 -3
- package/package.json +2 -2
- package/test/export-surface.snapshot.json +11 -1
- package/wire-changes/core@7.22.0.json +637 -0
- package/wire-changes/core@7.23.0.json +316 -0
|
@@ -430,6 +430,23 @@ export declare function bumpScanFuse(controlDir: string, key: string): number;
|
|
|
430
430
|
export declare function readIndexRevs(controlDir: string): Record<string, string>;
|
|
431
431
|
export declare function writeIndexRevs(controlDir: string, revs: Record<string, string>): void;
|
|
432
432
|
export declare function scanFuseCount(controlDir: string, key: string): number;
|
|
433
|
+
/**
|
|
434
|
+
* @contract memory.session_incarnation.proven_mismatch_only — only a PROVEN mismatch (both sides
|
|
435
|
+
* known and different) separates a record from the identity reading it; an absent incarnation on
|
|
436
|
+
* either side — a record written before the axis existed, a deployment that wires no resolver, a
|
|
437
|
+
* resolver that threw, a synthesized fail-closed record, a record imported from another plane —
|
|
438
|
+
* always BINDS, which is the pre-incarnation behavior. Every leg's conservative direction is
|
|
439
|
+
* therefore its old one, and the only way to lose a binding is to prove it belongs to someone else.
|
|
440
|
+
*/
|
|
441
|
+
export declare function sessionRecordBindsIncarnation(recorded: string | undefined, asking: string | undefined): boolean;
|
|
442
|
+
/** The incarnation this layer ACTS on: an unusable token is the unnamed occupant, never a throw. */
|
|
443
|
+
export declare function usableSessionIncarnation(raw: string | undefined): string | undefined;
|
|
444
|
+
/** How a per-session record read names the identity reading; absent ⇒ every record binds. */
|
|
445
|
+
export interface SessionIdentityRead {
|
|
446
|
+
incarnation?: string;
|
|
447
|
+
/** Called when a standing record was SET ASIDE as another occupant's, with the incarnation it carries — never silent. */
|
|
448
|
+
onSeparated?: (recorded: string) => void;
|
|
449
|
+
}
|
|
433
450
|
/** Directory (under the control plane) holding one marker file per polluted session. */
|
|
434
451
|
export declare const SESSION_POLLUTION_DIR = "session-pollution";
|
|
435
452
|
export interface SessionPollutionRecord {
|
|
@@ -440,6 +457,9 @@ export interface SessionPollutionRecord {
|
|
|
440
457
|
* honestly cause-less (the mint then falls back to `"observed"`); old readers narrow-take the
|
|
441
458
|
* two fields they know. */
|
|
442
459
|
cause?: import("./types.js").MemoryOriginCause;
|
|
460
|
+
/** the occupant that minted this record ({@link sessionRecordBindsIncarnation}). ADDITIVE:
|
|
461
|
+
* absent (no resolver wired, or written before the axis) ⇒ it binds every occupant. */
|
|
462
|
+
incarnation?: string;
|
|
443
463
|
}
|
|
444
464
|
/**
|
|
445
465
|
* Mark `sessionId` polluted (idempotent; the FIRST record wins — `wx` create refuses overwrite).
|
|
@@ -454,13 +474,13 @@ export interface SessionPollutionRecord {
|
|
|
454
474
|
* stays polluted for this process either way).
|
|
455
475
|
*/
|
|
456
476
|
export type SessionPollutionMarkOutcome = "created" | "existed" | "unpersisted";
|
|
457
|
-
export declare function markSessionPolluted(controlDir: string, sessionId: string, reason: string, now: () => number, cause?: import("./types.js").MemoryOriginCause): SessionPollutionMarkOutcome;
|
|
477
|
+
export declare function markSessionPolluted(controlDir: string, sessionId: string, reason: string, now: () => number, cause?: import("./types.js").MemoryOriginCause, incarnation?: string): SessionPollutionMarkOutcome;
|
|
458
478
|
/** Read the durable pollution record for `sessionId` (undefined = no marker).
|
|
459
479
|
* A marker that EXISTS still reads as polluted whether or not its bytes can be read or parsed
|
|
460
480
|
* (fail-closed: neither corruption nor an unreadable node may launder the state) — a synthesized
|
|
461
481
|
* record stands in. Only ABSENCE is clean, so a control plane that cannot be stat'ed at all reads
|
|
462
482
|
* clean for every session rather than disabling memory deployment-wide. */
|
|
463
|
-
export declare function readSessionPollution(controlDir: string, sessionId: string): SessionPollutionRecord | undefined;
|
|
483
|
+
export declare function readSessionPollution(controlDir: string, sessionId: string, identity?: SessionIdentityRead): SessionPollutionRecord | undefined;
|
|
464
484
|
/**
|
|
465
485
|
* design/178 v2-c §3-3 — enumerate EVERY durable pollution marker (the export snapshot's pollution
|
|
466
486
|
* face; `readSessionPollution` stays the per-session read). Same fail-closed record posture as the
|
|
@@ -487,6 +507,9 @@ export declare const SESSION_CAPTURE_OPTOUT_DIR = "session-capture-optout";
|
|
|
487
507
|
export interface SessionCaptureOptOutRecord {
|
|
488
508
|
at: number;
|
|
489
509
|
reason: string;
|
|
510
|
+
/** the occupant that declared this opt-out ({@link sessionRecordBindsIncarnation}).
|
|
511
|
+
* ADDITIVE: absent ⇒ binds every occupant, which is also where a carrier that drops it lands. */
|
|
512
|
+
incarnation?: string;
|
|
490
513
|
}
|
|
491
514
|
/** Outcome vocabulary shared with the pollution marker (same `wx` one-way form): `"created"` ⇔ THIS
|
|
492
515
|
* call minted the record; `"existed"` ⇔ already recorded (a resumed session / an idempotent
|
|
@@ -495,8 +518,10 @@ export interface SessionCaptureOptOutRecord {
|
|
|
495
518
|
* "could not persist" is a named refusal, never a silent in-process-only degrade — the deliberate
|
|
496
519
|
* divergence from the pollution marker's best-effort arm). */
|
|
497
520
|
export type SessionCaptureOptOutMarkOutcome = "created" | "existed" | "unpersisted";
|
|
498
|
-
/** Mark `sessionId` capture-opted-out (idempotent; the FIRST record wins — `wx` refuses overwrite).
|
|
499
|
-
|
|
521
|
+
/** Mark `sessionId` capture-opted-out (idempotent; the FIRST record wins — `wx` refuses overwrite).
|
|
522
|
+
* Incarnation law: first-record-wins is per IDENTITY — a new occupant whose declaration meets a stranger's
|
|
523
|
+
* standing record mints its own slot instead of answering `"existed"` ({@link sessionRecordPath}). */
|
|
524
|
+
export declare function markSessionCaptureOptOut(controlDir: string, sessionId: string, reason: string, now: () => number, incarnation?: string): SessionCaptureOptOutMarkOutcome;
|
|
500
525
|
/** Read the durable capture opt-out record for `sessionId` (undefined = capture is on). Fail-closed
|
|
501
526
|
* exactly like the pollution read: a record that EXISTS but cannot be read or parsed still answers
|
|
502
527
|
* opted-out (a synthesized record stands in) — corruption must not launder a privacy request back
|
|
@@ -504,7 +529,7 @@ export declare function markSessionCaptureOptOut(controlDir: string, sessionId:
|
|
|
504
529
|
* established (dir-level IO fault) THROWS — the engine's `sessionCaptureOptOutOrFault` catch turns
|
|
505
530
|
* it into the INDETERMINATE fault axis (same fail-closed suppression, honest attribution — see the
|
|
506
531
|
* catch below). */
|
|
507
|
-
export declare function readSessionCaptureOptOut(controlDir: string, sessionId: string): SessionCaptureOptOutRecord | undefined;
|
|
532
|
+
export declare function readSessionCaptureOptOut(controlDir: string, sessionId: string, identity?: SessionIdentityRead): SessionCaptureOptOutRecord | undefined;
|
|
508
533
|
/**
|
|
509
534
|
* Enumerate EVERY durable capture opt-out record (the consolidation eligibility arm's roster read —
|
|
510
535
|
* design/383 §2.4). Same fail-closed posture as {@link listSessionPollution}: a record that exists
|
|
@@ -545,7 +570,10 @@ export declare function listSessionCaptureOptOut(controlDir: string): Record<str
|
|
|
545
570
|
*/
|
|
546
571
|
export interface SessionCaptureRecordStore {
|
|
547
572
|
mark(sessionId: string, record: SessionCaptureOptOutRecord): SessionCaptureOptOutMarkOutcome | Promise<SessionCaptureOptOutMarkOutcome>;
|
|
548
|
-
|
|
573
|
+
/** `identity` names the occupant asking (absent ⇒ every record under the id binds, which is
|
|
574
|
+
* also what a carrier that ignores the parameter keeps — no carrier has to change). One that DOES
|
|
575
|
+
* persist the field keys its rows by (sessionId, incarnation): first record per IDENTITY wins. */
|
|
576
|
+
read(sessionId: string, identity?: SessionIdentityRead): SessionCaptureOptOutRecord | undefined | Promise<SessionCaptureOptOutRecord | undefined>;
|
|
549
577
|
list(): Record<string, SessionCaptureOptOutRecord> | Promise<Record<string, SessionCaptureOptOutRecord>>;
|
|
550
578
|
}
|
|
551
579
|
/** The core-default {@link SessionCaptureRecordStore}: the file trio over `controlDir` (single-
|
|
@@ -600,6 +628,9 @@ export declare function readStrictSidecar(controlDir: string, fileName: string,
|
|
|
600
628
|
export interface LineageContribution {
|
|
601
629
|
lastRev: string;
|
|
602
630
|
lastAt: number;
|
|
631
|
+
/** the occupant that made this contribution, carried from the staged txn at promotion as
|
|
632
|
+
* `marked` is. Absent ⇒ unknown ⇒ the row answers every occupant of the id (the old superset). */
|
|
633
|
+
incarnation?: string;
|
|
603
634
|
/** design/336 — carried from the staged row at promotion: the contribution's content (at
|
|
604
635
|
* `lastRev`) carries its external-origin marker, so the pollution retroaction sweeps owe it no
|
|
605
636
|
* challenge (the account travels with the entry). Absence = unmarked content — the sweeps'
|
|
@@ -630,6 +661,9 @@ export interface LineagePendingTxn {
|
|
|
630
661
|
sessionId: string;
|
|
631
662
|
at: number;
|
|
632
663
|
rows: LineagePendingRow[];
|
|
664
|
+
/** the occupant that staged this transaction, carried onto every contribution it
|
|
665
|
+
* promotes. Absent ⇒ unknown occupant, which binds every reader of the id. */
|
|
666
|
+
incarnation?: string;
|
|
633
667
|
/** Present ⇔ `applyPatches` returned and its applied set was durably recorded. */
|
|
634
668
|
credential?: {
|
|
635
669
|
appliedIds: string[];
|
|
@@ -644,7 +678,7 @@ export interface LineagePendingTxn {
|
|
|
644
678
|
}
|
|
645
679
|
/** Stage this transaction's would-commit rows BEFORE `applyPatches` (write-ahead). Throws on any
|
|
646
680
|
* write/lock/corruption failure — the caller refuses the whole harvest (fail-closed). */
|
|
647
|
-
export declare function stageLineagePending(controlDir: string, txnId: string, sessionId: string, rows: readonly LineagePendingRow[], now: () => number): void;
|
|
681
|
+
export declare function stageLineagePending(controlDir: string, txnId: string, sessionId: string, rows: readonly LineagePendingRow[], now: () => number, incarnation?: string): void;
|
|
648
682
|
/** Discard a staged transaction that never reached `applyPatches` (the pre-commit abort path —
|
|
649
683
|
* step ③ of the handshake, or a refused commit). Safe by construction: nothing was committed. */
|
|
650
684
|
export declare function discardLineagePending(controlDir: string, txnId: string): void;
|
|
@@ -724,8 +758,12 @@ export declare function lineageLatchedIds(controlDir: string): Set<string>;
|
|
|
724
758
|
* such as "constructor" indexes truthy on EVERY plain object, so a bare lookup would attribute
|
|
725
759
|
* the WHOLE committed set to it — behind the erasure selector that phantom is a whole-store
|
|
726
760
|
* delete, the exact shape the selector type forbids. Belt over the null-proto parse rebuild:
|
|
727
|
-
* this guard stays honest even against a map minted elsewhere.
|
|
728
|
-
|
|
761
|
+
* this guard stays honest even against a map minted elsewhere.
|
|
762
|
+
* `asking` names the occupant whose contributions are wanted: a row is kept unless it
|
|
763
|
+
* PROVABLY belongs to another ({@link sessionRecordBindsIncarnation}). An absent `asking`, and
|
|
764
|
+
* every unstamped row, keeps the whole by-id set — the superset this face has always answered, and
|
|
765
|
+
* the conservative side here (a sweep that challenges too little publishes). */
|
|
766
|
+
export declare function lineageContributionsOfSession(controlDir: string, sessionId: string, asking?: string): Array<{
|
|
729
767
|
entryId: string;
|
|
730
768
|
lastRev: string;
|
|
731
769
|
lastAt: number;
|
|
@@ -1114,16 +1114,76 @@ export function writeIndexRevs(controlDir, revs) {
|
|
|
1114
1114
|
export function scanFuseCount(controlDir, key) {
|
|
1115
1115
|
return coerceFuse(readSidecarJson(controlDir, SCAN_FUSE_FILE))[key] ?? 0;
|
|
1116
1116
|
}
|
|
1117
|
-
export
|
|
1118
|
-
|
|
1119
|
-
return join(controlDir, SESSION_POLLUTION_DIR, `${encodeURIComponent(sessionId)}.json`);
|
|
1117
|
+
export function sessionRecordBindsIncarnation(recorded, asking) {
|
|
1118
|
+
return recorded === undefined || asking === undefined || recorded === asking;
|
|
1120
1119
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1120
|
+
function readIncarnationSeat(parsed) {
|
|
1121
|
+
return isIncarnationSeat(parsed.incarnation) && parsed.incarnation !== undefined ? parsed.incarnation : undefined;
|
|
1122
|
+
}
|
|
1123
|
+
function isIncarnationSeat(raw) {
|
|
1124
|
+
if (raw === undefined)
|
|
1125
|
+
return true;
|
|
1126
|
+
if (typeof raw !== "string" || raw === "")
|
|
1127
|
+
return false;
|
|
1128
|
+
try {
|
|
1129
|
+
encodeURIComponent(raw);
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
export function usableSessionIncarnation(raw) {
|
|
1137
|
+
return isIncarnationSeat(raw) ? raw : undefined;
|
|
1138
|
+
}
|
|
1139
|
+
function sessionRecordPath(controlDir, dir, sessionId, incarnation) {
|
|
1140
|
+
const name = incarnation === undefined ? `${encodeURIComponent(sessionId)}.json` : `${encodeURIComponent(sessionId)}@${encodeURIComponent(incarnation)}.json`;
|
|
1141
|
+
return join(controlDir, dir, name);
|
|
1142
|
+
}
|
|
1143
|
+
function standingOwnerAt(path) {
|
|
1144
|
+
let raw;
|
|
1145
|
+
try {
|
|
1146
|
+
raw = readFileSync(path, "utf8");
|
|
1147
|
+
}
|
|
1148
|
+
catch (err) {
|
|
1149
|
+
if (err.code !== "ENOENT")
|
|
1150
|
+
return "unattributable";
|
|
1151
|
+
try {
|
|
1152
|
+
lstatSync(path);
|
|
1153
|
+
}
|
|
1154
|
+
catch {
|
|
1155
|
+
return "unattributable";
|
|
1156
|
+
}
|
|
1157
|
+
return { owner: undefined };
|
|
1158
|
+
}
|
|
1123
1159
|
try {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1160
|
+
const parsed = JSON.parse(raw);
|
|
1161
|
+
if (parsed !== null && typeof parsed === "object")
|
|
1162
|
+
return { owner: readIncarnationSeat(parsed) };
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
}
|
|
1166
|
+
return "unattributable";
|
|
1167
|
+
}
|
|
1168
|
+
function mintSessionRecordSlots(controlDir, dir, sessionId, incarnation, text) {
|
|
1169
|
+
const idSlot = sessionRecordPath(controlDir, dir, sessionId);
|
|
1170
|
+
try {
|
|
1171
|
+
ensureDirExists(dirname(idSlot));
|
|
1172
|
+
writeFileSync(idSlot, text, { encoding: "utf8", flag: "wx" });
|
|
1173
|
+
return "created";
|
|
1174
|
+
}
|
|
1175
|
+
catch (err) {
|
|
1176
|
+
if (!(err instanceof Error && "code" in err && err.code === "EEXIST"))
|
|
1177
|
+
return "unpersisted";
|
|
1178
|
+
}
|
|
1179
|
+
const own = usableSessionIncarnation(incarnation);
|
|
1180
|
+
const standing = standingOwnerAt(idSlot);
|
|
1181
|
+
const vouches = standing !== "unattributable" && (standing.owner === undefined || standing.owner === own);
|
|
1182
|
+
const unnamed = vouches || own === undefined ? undefined : standingOwnerAt(sessionRecordPath(controlDir, dir, sessionId, ""));
|
|
1183
|
+
if (vouches || (unnamed !== undefined && unnamed !== "unattributable" && unnamed.owner === undefined))
|
|
1184
|
+
return "existed";
|
|
1185
|
+
try {
|
|
1186
|
+
writeFileSync(sessionRecordPath(controlDir, dir, sessionId, own ?? ""), text, { encoding: "utf8", flag: "wx" });
|
|
1127
1187
|
return "created";
|
|
1128
1188
|
}
|
|
1129
1189
|
catch (err) {
|
|
@@ -1132,15 +1192,59 @@ export function markSessionPolluted(controlDir, sessionId, reason, now, cause) {
|
|
|
1132
1192
|
return "unpersisted";
|
|
1133
1193
|
}
|
|
1134
1194
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1195
|
+
function bindOrSeparate(record, identity, asking, unknownSlot) {
|
|
1196
|
+
if (sessionRecordBindsIncarnation(record.incarnation, asking))
|
|
1197
|
+
return record;
|
|
1198
|
+
const unnamed = unknownSlot();
|
|
1199
|
+
if (unnamed !== undefined && sessionRecordBindsIncarnation(unnamed.incarnation, asking))
|
|
1200
|
+
return unnamed;
|
|
1201
|
+
if (record.incarnation !== undefined)
|
|
1202
|
+
identity?.onSeparated?.(record.incarnation);
|
|
1203
|
+
return undefined;
|
|
1204
|
+
}
|
|
1205
|
+
function sessionIdOfRecordName(name) {
|
|
1206
|
+
if (!name.endsWith(".json"))
|
|
1207
|
+
return undefined;
|
|
1208
|
+
const stem = name.slice(0, -".json".length);
|
|
1209
|
+
const at = stem.indexOf("@");
|
|
1210
|
+
try {
|
|
1211
|
+
return decodeURIComponent(at < 0 ? stem : stem.slice(0, at));
|
|
1212
|
+
}
|
|
1213
|
+
catch {
|
|
1214
|
+
return undefined;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
export const SESSION_POLLUTION_DIR = "session-pollution";
|
|
1218
|
+
function pollutionPath(controlDir, sessionId, incarnation) {
|
|
1219
|
+
return sessionRecordPath(controlDir, SESSION_POLLUTION_DIR, sessionId, incarnation);
|
|
1220
|
+
}
|
|
1221
|
+
export function markSessionPolluted(controlDir, sessionId, reason, now, cause, incarnation) {
|
|
1222
|
+
const record = { at: now(), reason, ...(cause !== undefined ? { cause } : {}), ...(incarnation !== undefined ? { incarnation } : {}) };
|
|
1223
|
+
return mintSessionRecordSlots(controlDir, SESSION_POLLUTION_DIR, sessionId, incarnation, `${JSON.stringify(record, null, 2)}\n`);
|
|
1224
|
+
}
|
|
1225
|
+
export function readSessionPollution(controlDir, sessionId, identity) {
|
|
1226
|
+
const asking = usableSessionIncarnation(identity?.incarnation);
|
|
1227
|
+
if (asking !== undefined) {
|
|
1228
|
+
const own = readPollutionAt(pollutionPath(controlDir, sessionId, asking));
|
|
1229
|
+
if (own !== undefined)
|
|
1230
|
+
return own;
|
|
1231
|
+
}
|
|
1232
|
+
const unnamedSlot = () => readPollutionAt(pollutionPath(controlDir, sessionId, ""));
|
|
1233
|
+
const standing = readPollutionAt(pollutionPath(controlDir, sessionId));
|
|
1234
|
+
return standing === undefined ? unnamedSlot() : bindOrSeparate(standing, identity, asking, unnamedSlot);
|
|
1235
|
+
}
|
|
1236
|
+
function readPollutionAt(path) {
|
|
1137
1237
|
let raw;
|
|
1138
1238
|
try {
|
|
1139
1239
|
raw = readFileSync(path, "utf8");
|
|
1140
1240
|
}
|
|
1141
1241
|
catch {
|
|
1142
|
-
|
|
1242
|
+
try {
|
|
1243
|
+
lstatSync(path);
|
|
1244
|
+
}
|
|
1245
|
+
catch {
|
|
1143
1246
|
return undefined;
|
|
1247
|
+
}
|
|
1144
1248
|
return { at: 0, reason: "pollution marker present but unreadable (kept fail-closed)" };
|
|
1145
1249
|
}
|
|
1146
1250
|
try {
|
|
@@ -1150,8 +1254,9 @@ export function readSessionPollution(controlDir, sessionId) {
|
|
|
1150
1254
|
const reason = parsed.reason;
|
|
1151
1255
|
const cause = parsed.cause;
|
|
1152
1256
|
const knownCause = typeof cause === "string" && MEMORY_ORIGIN_CAUSES.includes(cause) ? cause : undefined;
|
|
1257
|
+
const incarnation = readIncarnationSeat(parsed);
|
|
1153
1258
|
if (typeof at === "number" && typeof reason === "string")
|
|
1154
|
-
return { at, reason, ...(knownCause !== undefined ? { cause: knownCause } : {}) };
|
|
1259
|
+
return { at, reason, ...(knownCause !== undefined ? { cause: knownCause } : {}), ...(incarnation !== undefined ? { incarnation } : {}) };
|
|
1155
1260
|
}
|
|
1156
1261
|
}
|
|
1157
1262
|
catch {
|
|
@@ -1171,16 +1276,10 @@ export function listSessionPollution(controlDir) {
|
|
|
1171
1276
|
}
|
|
1172
1277
|
const out = Object.create(null);
|
|
1173
1278
|
for (const name of names.sort()) {
|
|
1174
|
-
|
|
1279
|
+
const sessionId = sessionIdOfRecordName(name);
|
|
1280
|
+
if (sessionId === undefined || Object.prototype.hasOwnProperty.call(out, sessionId))
|
|
1175
1281
|
continue;
|
|
1176
|
-
|
|
1177
|
-
try {
|
|
1178
|
-
sessionId = decodeURIComponent(name.slice(0, -".json".length));
|
|
1179
|
-
}
|
|
1180
|
-
catch {
|
|
1181
|
-
continue;
|
|
1182
|
-
}
|
|
1183
|
-
const rec = readSessionPollution(controlDir, sessionId);
|
|
1282
|
+
const rec = readPollutionAt(join(dir, name));
|
|
1184
1283
|
if (rec !== undefined)
|
|
1185
1284
|
out[sessionId] = rec;
|
|
1186
1285
|
}
|
|
@@ -1201,25 +1300,25 @@ export function importSessionPollution(controlDir, sessionId, record) {
|
|
|
1201
1300
|
return standing !== undefined && standing.at === record.at && standing.reason === record.reason ? "already" : "divergent";
|
|
1202
1301
|
}
|
|
1203
1302
|
export const SESSION_CAPTURE_OPTOUT_DIR = "session-capture-optout";
|
|
1204
|
-
function captureOptOutPath(controlDir, sessionId) {
|
|
1205
|
-
return
|
|
1303
|
+
function captureOptOutPath(controlDir, sessionId, incarnation) {
|
|
1304
|
+
return sessionRecordPath(controlDir, SESSION_CAPTURE_OPTOUT_DIR, sessionId, incarnation);
|
|
1206
1305
|
}
|
|
1207
|
-
export function markSessionCaptureOptOut(controlDir, sessionId, reason, now) {
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
return "existed";
|
|
1218
|
-
return "unpersisted";
|
|
1306
|
+
export function markSessionCaptureOptOut(controlDir, sessionId, reason, now, incarnation) {
|
|
1307
|
+
const record = { at: now(), reason, ...(incarnation !== undefined ? { incarnation } : {}) };
|
|
1308
|
+
return mintSessionRecordSlots(controlDir, SESSION_CAPTURE_OPTOUT_DIR, sessionId, incarnation, `${JSON.stringify(record, null, 2)}\n`);
|
|
1309
|
+
}
|
|
1310
|
+
export function readSessionCaptureOptOut(controlDir, sessionId, identity) {
|
|
1311
|
+
const asking = usableSessionIncarnation(identity?.incarnation);
|
|
1312
|
+
if (asking !== undefined) {
|
|
1313
|
+
const own = readCaptureOptOutAt(captureOptOutPath(controlDir, sessionId, asking));
|
|
1314
|
+
if (own !== undefined)
|
|
1315
|
+
return own;
|
|
1219
1316
|
}
|
|
1317
|
+
const unnamedSlot = () => readCaptureOptOutAt(captureOptOutPath(controlDir, sessionId, ""));
|
|
1318
|
+
const standing = readCaptureOptOutAt(captureOptOutPath(controlDir, sessionId));
|
|
1319
|
+
return standing === undefined ? unnamedSlot() : bindOrSeparate(standing, identity, asking, unnamedSlot);
|
|
1220
1320
|
}
|
|
1221
|
-
|
|
1222
|
-
const path = captureOptOutPath(controlDir, sessionId);
|
|
1321
|
+
function readCaptureOptOutAt(path) {
|
|
1223
1322
|
let raw;
|
|
1224
1323
|
try {
|
|
1225
1324
|
raw = readFileSync(path, "utf8");
|
|
@@ -1249,8 +1348,9 @@ export function readSessionCaptureOptOut(controlDir, sessionId) {
|
|
|
1249
1348
|
if (parsed !== null && typeof parsed === "object") {
|
|
1250
1349
|
const at = parsed.at;
|
|
1251
1350
|
const reason = parsed.reason;
|
|
1351
|
+
const incarnation = readIncarnationSeat(parsed);
|
|
1252
1352
|
if (typeof at === "number" && typeof reason === "string")
|
|
1253
|
-
return { at, reason };
|
|
1353
|
+
return { at, reason, ...(incarnation !== undefined ? { incarnation } : {}) };
|
|
1254
1354
|
}
|
|
1255
1355
|
}
|
|
1256
1356
|
catch {
|
|
@@ -1270,16 +1370,10 @@ export function listSessionCaptureOptOut(controlDir) {
|
|
|
1270
1370
|
}
|
|
1271
1371
|
const out = Object.create(null);
|
|
1272
1372
|
for (const name of names.sort()) {
|
|
1273
|
-
|
|
1373
|
+
const sessionId = sessionIdOfRecordName(name);
|
|
1374
|
+
if (sessionId === undefined || Object.prototype.hasOwnProperty.call(out, sessionId))
|
|
1274
1375
|
continue;
|
|
1275
|
-
|
|
1276
|
-
try {
|
|
1277
|
-
sessionId = decodeURIComponent(name.slice(0, -".json".length));
|
|
1278
|
-
}
|
|
1279
|
-
catch {
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
const rec = readSessionCaptureOptOut(controlDir, sessionId);
|
|
1376
|
+
const rec = readCaptureOptOutAt(join(dir, name));
|
|
1283
1377
|
if (rec !== undefined)
|
|
1284
1378
|
out[sessionId] = rec;
|
|
1285
1379
|
}
|
|
@@ -1287,8 +1381,8 @@ export function listSessionCaptureOptOut(controlDir) {
|
|
|
1287
1381
|
}
|
|
1288
1382
|
export function fileSessionCaptureRecordStore(controlDir) {
|
|
1289
1383
|
return {
|
|
1290
|
-
mark: (sessionId, record) => markSessionCaptureOptOut(controlDir, sessionId, record.reason, () => record.at),
|
|
1291
|
-
read: (sessionId) => readSessionCaptureOptOut(controlDir, sessionId),
|
|
1384
|
+
mark: (sessionId, record) => markSessionCaptureOptOut(controlDir, sessionId, record.reason, () => record.at, record.incarnation),
|
|
1385
|
+
read: (sessionId, identity) => readSessionCaptureOptOut(controlDir, sessionId, identity),
|
|
1292
1386
|
list: () => listSessionCaptureOptOut(controlDir),
|
|
1293
1387
|
};
|
|
1294
1388
|
}
|
|
@@ -1461,6 +1555,9 @@ function coerceLineage(raw) {
|
|
|
1461
1555
|
}
|
|
1462
1556
|
if (row.marked !== undefined && row.marked !== true)
|
|
1463
1557
|
throw badShape(`committed[${JSON.stringify(entryId)}][${JSON.stringify(sid)}] marked`);
|
|
1558
|
+
if (!isIncarnationSeat(row.incarnation)) {
|
|
1559
|
+
throw badShape(`committed[${JSON.stringify(entryId)}][${JSON.stringify(sid)}] incarnation`);
|
|
1560
|
+
}
|
|
1464
1561
|
}
|
|
1465
1562
|
}
|
|
1466
1563
|
for (const [txnId, t] of Object.entries(r.pending)) {
|
|
@@ -1470,6 +1567,8 @@ function coerceLineage(raw) {
|
|
|
1470
1567
|
}
|
|
1471
1568
|
if (txn.kind !== undefined && txn.kind !== "latch-only")
|
|
1472
1569
|
throw badShape(`pending[${JSON.stringify(txnId)}] kind`);
|
|
1570
|
+
if (!isIncarnationSeat(txn.incarnation))
|
|
1571
|
+
throw badShape(`pending[${JSON.stringify(txnId)}] incarnation`);
|
|
1473
1572
|
for (const row of txn.rows) {
|
|
1474
1573
|
const p = row;
|
|
1475
1574
|
if (!p || typeof p !== "object" || typeof p.entryId !== "string" || typeof p.rev !== "string")
|
|
@@ -1498,9 +1597,9 @@ function coerceLineage(raw) {
|
|
|
1498
1597
|
pending[txnId] = t;
|
|
1499
1598
|
return { version: 1, committed, pending };
|
|
1500
1599
|
}
|
|
1501
|
-
export function stageLineagePending(controlDir, txnId, sessionId, rows, now) {
|
|
1600
|
+
export function stageLineagePending(controlDir, txnId, sessionId, rows, now, incarnation) {
|
|
1502
1601
|
lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
|
|
1503
|
-
rec.pending[txnId] = { sessionId, at: now(), rows: [...rows] };
|
|
1602
|
+
rec.pending[txnId] = { sessionId, at: now(), rows: [...rows], ...(incarnation !== undefined ? { incarnation } : {}) };
|
|
1504
1603
|
return { next: rec, result: undefined };
|
|
1505
1604
|
});
|
|
1506
1605
|
}
|
|
@@ -1536,7 +1635,7 @@ export function promoteLineagePending(controlDir, txnId, now) {
|
|
|
1536
1635
|
if (!applied.has(row.entryId))
|
|
1537
1636
|
continue;
|
|
1538
1637
|
const sessions = (rec.committed[row.entryId] ??= Object.create(null));
|
|
1539
|
-
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}) };
|
|
1638
|
+
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}), ...(txn.incarnation !== undefined ? { incarnation: txn.incarnation } : {}) };
|
|
1540
1639
|
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
|
|
1541
1640
|
}
|
|
1542
1641
|
delete rec.pending[txnId];
|
|
@@ -1562,7 +1661,7 @@ export function adjudicateLineagePending(controlDir, txnId, action, now) {
|
|
|
1562
1661
|
if (applied !== undefined && !applied.has(row.entryId))
|
|
1563
1662
|
continue;
|
|
1564
1663
|
const sessions = (rec.committed[row.entryId] ??= Object.create(null));
|
|
1565
|
-
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}) };
|
|
1664
|
+
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}), ...(txn.incarnation !== undefined ? { incarnation: txn.incarnation } : {}) };
|
|
1566
1665
|
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
|
|
1567
1666
|
}
|
|
1568
1667
|
delete rec.pending[txnId];
|
|
@@ -1586,7 +1685,7 @@ export function reconcileLineage(controlDir, now) {
|
|
|
1586
1685
|
continue;
|
|
1587
1686
|
at ??= now();
|
|
1588
1687
|
const sessions = (rec.committed[row.entryId] ??= Object.create(null));
|
|
1589
|
-
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}) };
|
|
1688
|
+
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}), ...(txn.incarnation !== undefined ? { incarnation: txn.incarnation } : {}) };
|
|
1590
1689
|
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
|
|
1591
1690
|
}
|
|
1592
1691
|
delete rec.pending[txnId];
|
|
@@ -1663,13 +1762,14 @@ export function lineageLatchedIds(controlDir) {
|
|
|
1663
1762
|
out.add(row.entryId);
|
|
1664
1763
|
return out;
|
|
1665
1764
|
}
|
|
1666
|
-
export function lineageContributionsOfSession(controlDir, sessionId) {
|
|
1765
|
+
export function lineageContributionsOfSession(controlDir, sessionId, asking) {
|
|
1667
1766
|
const rec = coerceLineage(readStrictSidecar(controlDir, LINEAGE_FILE, "memory lineage ledger"));
|
|
1668
1767
|
const out = [];
|
|
1669
1768
|
for (const [entryId, sessions] of Object.entries(rec.committed)) {
|
|
1670
1769
|
const c = Object.prototype.hasOwnProperty.call(sessions, sessionId) ? sessions[sessionId] : undefined;
|
|
1671
|
-
if (c
|
|
1672
|
-
|
|
1770
|
+
if (c === undefined || !sessionRecordBindsIncarnation(c.incarnation, asking))
|
|
1771
|
+
continue;
|
|
1772
|
+
out.push({ entryId, lastRev: c.lastRev, lastAt: c.lastAt, ...(c.marked === true ? { marked: true } : {}) });
|
|
1673
1773
|
}
|
|
1674
1774
|
return out;
|
|
1675
1775
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type MemoryPartitionIncidentSink, type SessionIdentityRead } from "./layout.js";
|
|
2
|
+
/** The current occupant's token, and the identity seat a per-session record read is made under. */
|
|
3
|
+
export interface SessionIncarnationSeat {
|
|
4
|
+
incarnationOf(sessionId: string): string | undefined;
|
|
5
|
+
identityRead(sessionId: string): SessionIdentityRead;
|
|
6
|
+
}
|
|
7
|
+
/** Build the seat over a host resolver (absent ⇒ nothing stamped, nothing asked, every record binds
|
|
8
|
+
* by id — the pre-incarnation answer). Both disclosures are DEDUPED on the axis their fact belongs to: the resolver
|
|
9
|
+
* fault once per engine (a wiring fact), a separation once per session id (a fact about that id). */
|
|
10
|
+
export declare function createSessionIncarnationSeat(input: {
|
|
11
|
+
resolve: ((session: {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
}) => string | undefined) | undefined;
|
|
14
|
+
onIncident: MemoryPartitionIncidentSink | undefined;
|
|
15
|
+
}): SessionIncarnationSeat;
|
|
16
|
+
/** Remember an in-process record under its session id, FIRST-WINS PER IDENTITY: this map is the
|
|
17
|
+
* durable half's fallback exactly when the durable write failed, so keeping only the first
|
|
18
|
+
* occupant's row would leave the next one unprotected on the one path it exists to cover. */
|
|
19
|
+
export declare function rememberForIdentity<T extends {
|
|
20
|
+
incarnation?: string;
|
|
21
|
+
}>(map: Map<string, T[]>, sessionId: string, record: T): void;
|
|
22
|
+
/** The remembered record that BINDS `asking` ({@link sessionRecordBindsIncarnation}), if any. */
|
|
23
|
+
export declare function recalledForIdentity<T extends {
|
|
24
|
+
incarnation?: string;
|
|
25
|
+
}>(map: Map<string, T[]>, sessionId: string, asking: string | undefined): T | undefined;
|
|
26
|
+
/** The first record remembered for `sessionId` by ANY occupant — for the id-blind faces. */
|
|
27
|
+
export declare function recalledForAnyIdentity<T>(map: Map<string, T[]>, sessionId: string): T | undefined;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { sessionRecordBindsIncarnation, usableSessionIncarnation } from "./layout.js";
|
|
2
|
+
export function createSessionIncarnationSeat(input) {
|
|
3
|
+
const { resolve, onIncident } = input;
|
|
4
|
+
let faultAnnounced = false;
|
|
5
|
+
const separationAnnounced = new Set();
|
|
6
|
+
const disclose = (code, message) => {
|
|
7
|
+
if (onIncident === undefined)
|
|
8
|
+
return;
|
|
9
|
+
try {
|
|
10
|
+
const e = new Error(message);
|
|
11
|
+
e.code = code;
|
|
12
|
+
onIncident(e);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const incarnationOf = (sessionId) => {
|
|
18
|
+
if (resolve === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
const answer = resolve({ sessionId });
|
|
22
|
+
return usableSessionIncarnation(answer);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (!faultAnnounced) {
|
|
26
|
+
faultAnnounced = true;
|
|
27
|
+
disclose("memory.session_incarnation_unreadable", `the session incarnation resolver threw (${err instanceof Error ? err.message : String(err)}) — this run cannot tell one occupant of a session id from the next, so every durable session record binds by id alone (the conservative reading, and the behavior of a deployment that wires no resolver)`);
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
incarnationOf,
|
|
34
|
+
identityRead: (sessionId) => {
|
|
35
|
+
const incarnation = incarnationOf(sessionId);
|
|
36
|
+
return {
|
|
37
|
+
...(incarnation !== undefined ? { incarnation } : {}),
|
|
38
|
+
onSeparated: (recorded) => {
|
|
39
|
+
if (separationAnnounced.has(sessionId))
|
|
40
|
+
return;
|
|
41
|
+
separationAnnounced.add(sessionId);
|
|
42
|
+
disclose("memory.session_incarnation_mismatch", `a durable record stands for session ${sessionId} but it was minted by a DIFFERENT occupant of that id (recorded incarnation ${JSON.stringify(recorded)}, this run ${JSON.stringify(incarnation)}) — it does not bind this run and the record is kept where it is. Either the id was recycled after its previous session was deleted, or the deployment's incarnation resolver is not stable for the life of one session`);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function rememberForIdentity(map, sessionId, record) {
|
|
49
|
+
const rows = map.get(sessionId);
|
|
50
|
+
if (rows === undefined) {
|
|
51
|
+
map.set(sessionId, [record]);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (rows.some((r) => r.incarnation === record.incarnation))
|
|
55
|
+
return;
|
|
56
|
+
rows.push(record);
|
|
57
|
+
}
|
|
58
|
+
export function recalledForIdentity(map, sessionId, asking) {
|
|
59
|
+
return map.get(sessionId)?.find((r) => sessionRecordBindsIncarnation(r.incarnation, asking));
|
|
60
|
+
}
|
|
61
|
+
export function recalledForAnyIdentity(map, sessionId) {
|
|
62
|
+
return map.get(sessionId)?.[0];
|
|
63
|
+
}
|
|
@@ -12,7 +12,7 @@ import type { OnQuestion } from "./ask-question.js";
|
|
|
12
12
|
import type { ToolExecuteContext } from "./tool-spec.js";
|
|
13
13
|
import type { TaskSpec } from "./types.js";
|
|
14
14
|
/** The fields of a spec the seat carries — one `Pick`, shared by the mint's input and the apply's target. */
|
|
15
|
-
export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "clientContext" | "onQuestion" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable">;
|
|
15
|
+
export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "clientContext" | "onQuestion" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "memory">;
|
|
16
16
|
/**
|
|
17
17
|
* The seat a run hands its children. Every member is the parent's own value, frozen at mint (arrays copied and
|
|
18
18
|
* frozen; the facts object copied). Members present iff the parent carries them — an absent member is "nothing to
|
|
@@ -26,6 +26,8 @@ export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "cli
|
|
|
26
26
|
* · `envFacts` — the sandbox facts (profile / egress / scratchpad).
|
|
27
27
|
* · `memoryPersistenceCapable` — the deployment's DECLARED persistence statement: `false` is a REMOVAL (floor),
|
|
28
28
|
* `true` a default a child's own declaration may narrow.
|
|
29
|
+
* · `memoryOff` — the request-level "memory is off" FACT (`spec.memory.enabled === false`): a REMOVAL
|
|
30
|
+
* (floor) that REWRITES the child's own memory declaration instead of travelling as a value.
|
|
29
31
|
*/
|
|
30
32
|
export interface ParentSpecSeat {
|
|
31
33
|
readonly principal?: string;
|
|
@@ -36,14 +38,15 @@ export interface ParentSpecSeat {
|
|
|
36
38
|
readonly additionalReadDirectories?: readonly string[];
|
|
37
39
|
readonly envFacts?: TaskSpec["envFacts"];
|
|
38
40
|
readonly memoryPersistenceCapable?: boolean;
|
|
41
|
+
readonly memoryOff?: true;
|
|
39
42
|
}
|
|
40
43
|
/** Mint the seat off the parent's REBOUND spec and its frozen question face (spec over deps — the caller resolves it once,
|
|
41
44
|
* the same value the parent's own question routing reads). Absent fields stay absent; `oneShot` only when `true`. */
|
|
42
45
|
export declare function parentSpecSeatOf(spec: Omit<ParentSpecSeatFields, "onQuestion">, frozenOnQuestion: OnQuestion | undefined): ParentSpecSeat;
|
|
43
46
|
/**
|
|
44
47
|
* Apply the seat to a child spec — ONE law for every member, both lanes:
|
|
45
|
-
* a value the child spec already carries WINS; the seat FILLS an unset one; the
|
|
46
|
-
* (`oneShot: true`, `memoryPersistenceCapable: false`) land UNCONDITIONALLY.
|
|
48
|
+
* a value the child spec already carries WINS; the seat FILLS an unset one; the three REMOVAL values
|
|
49
|
+
* (`oneShot: true`, `memoryPersistenceCapable: false`, `memoryOff: true`) land UNCONDITIONALLY.
|
|
47
50
|
* "Already carries" is the child's own authorship — a deployment baseline, a trusted script, an agent definition's
|
|
48
51
|
* fold (the Task lane folds the definition's `memoryPersistenceCapable` before applying; the workflow lane applies at
|
|
49
52
|
* the launch site AFTER the agentType fold) — so a definition may narrow a parent `true` to `false`, a baseline-pinned
|
|
@@ -52,6 +55,15 @@ export declare function parentSpecSeatOf(spec: Omit<ParentSpecSeatFields, "onQue
|
|
|
52
55
|
* No seat (a tool running outside a Runner task) ⇒ the child spec is returned as is.
|
|
53
56
|
*/
|
|
54
57
|
export declare function applyParentSpecSeat<S extends ParentSpecSeatFields>(child: S, seat: ParentSpecSeat | undefined): S;
|
|
58
|
+
/** The ONE way the memory-off floor is written onto a spec — the seat's apply above and the resume lane's
|
|
59
|
+
* clamp fold both call it: keep every field the spec authored (scopes, write scope, content-safety knobs)
|
|
60
|
+
* and state `enabled: false` over them. Returning a value rather than deleting the key IS the mechanism:
|
|
61
|
+
* the next seat mint reads `spec.memory.enabled === false`, so a spec whose `memory` was removed would end
|
|
62
|
+
* the floor one level down. A spec that declared nothing gets the bare `{ enabled: false }`, which the
|
|
63
|
+
* normalizer reads as "no usable scopes" — identical to the absent declaration it replaces. */
|
|
64
|
+
export declare function memoryTurnedOff(memory: TaskSpec["memory"]): NonNullable<TaskSpec["memory"]> & {
|
|
65
|
+
enabled: false;
|
|
66
|
+
};
|
|
55
67
|
/** The ctx keys that restate a seat member as THIS run's own fact (tools read them for scope / receipts / clamps). */
|
|
56
68
|
declare const SEAT_FACTS_ON_CTX: readonly ["principal", "oneShot", "onQuestion", "memoryPersistenceCapable"];
|
|
57
69
|
/**
|
|
@@ -8,6 +8,7 @@ export function parentSpecSeatOf(spec, frozenOnQuestion) {
|
|
|
8
8
|
...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
|
|
9
9
|
...(spec.envFacts !== undefined ? { envFacts: Object.freeze({ ...spec.envFacts }) } : {}),
|
|
10
10
|
...(spec.memoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: spec.memoryPersistenceCapable } : {}),
|
|
11
|
+
...(spec.memory?.enabled === false ? { memoryOff: true } : {}),
|
|
11
12
|
});
|
|
12
13
|
}
|
|
13
14
|
export function applyParentSpecSeat(child, seat) {
|
|
@@ -24,8 +25,12 @@ export function applyParentSpecSeat(child, seat) {
|
|
|
24
25
|
...fill("envFacts", (seat.envFacts !== undefined ? { ...seat.envFacts } : undefined)),
|
|
25
26
|
...(seat.oneShot === true ? { oneShot: true } : {}),
|
|
26
27
|
...(seat.memoryPersistenceCapable === false ? { memoryPersistenceCapable: false } : fill("memoryPersistenceCapable", seat.memoryPersistenceCapable)),
|
|
28
|
+
...(seat.memoryOff === true ? { memory: memoryTurnedOff(child.memory) } : {}),
|
|
27
29
|
};
|
|
28
30
|
}
|
|
31
|
+
export function memoryTurnedOff(memory) {
|
|
32
|
+
return { ...(memory ?? {}), enabled: false };
|
|
33
|
+
}
|
|
29
34
|
const SEAT_FACTS_ON_CTX = ["principal", "oneShot", "onQuestion", "memoryPersistenceCapable"];
|
|
30
35
|
export function parentSpecSeatOfCtx(ctx) {
|
|
31
36
|
const seat = ctx.parentSpecSeat;
|
|
@@ -86,7 +86,7 @@ function syntheticCheckpoint(scope) {
|
|
|
86
86
|
suspendCount: 1,
|
|
87
87
|
suspendedAt: Date.now(),
|
|
88
88
|
resourceLedger: { spentMicroUsd: 0, spentTokens: 0, spentTurns: 0, sliceCount: 1 },
|
|
89
|
-
humanReview: { count: 1, totalWaitMs: 0, gates: [{ kind: "human", waitMs: 0, decision: "approve" }] },
|
|
89
|
+
humanReview: { count: 1, totalWaitMs: 0, gates: [{ kind: "human", waitMs: 0, decision: "approve", toolName: "SelfCheck", toolCallId: `${scope}:selfcheck-call`, toolArg: "park self-check probe" }] },
|
|
90
90
|
sourceTaskId: `${scope}:task`,
|
|
91
91
|
principal: `${scope}:principal`,
|
|
92
92
|
durableApproval: { scope, ttlMs: PARK_SELFCHECK_ROW_TTL_MS },
|