@sema-agent/core 5.39.0 → 5.41.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 +150 -11
- package/dist/core/auto-mode-prompt.js +1 -1
- package/dist/core/checkpoint-store.d.ts +12 -0
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.js +9 -1
- package/dist/core/memory-engine/engine.d.ts +27 -0
- package/dist/core/memory-engine/engine.js +103 -1
- package/dist/core/memory-engine/export-bundle.d.ts +192 -0
- package/dist/core/memory-engine/export-bundle.js +306 -0
- package/dist/core/memory-engine/file-backend.d.ts +178 -1
- package/dist/core/memory-engine/file-backend.js +648 -6
- package/dist/core/memory-engine/index.d.ts +2 -1
- package/dist/core/memory-engine/index.js +1 -0
- package/dist/core/memory-engine/layout.d.ts +99 -1
- package/dist/core/memory-engine/layout.js +143 -7
- package/dist/core/memory-engine/memory-backend-contract.d.ts +1 -1
- package/dist/core/memory-engine/memory-backend-contract.js +52 -0
- package/dist/core/memory-engine/tools.js +8 -1
- package/dist/core/permission-rule-consent.js +14 -2
- package/dist/core/runner/prepare-config-doors.js +20 -0
- package/dist/core/runner/prepare-task.js +13 -0
- package/dist/core/runner/runtask.js +14 -1
- package/dist/core/runner/synthetic-tools.js +3 -1
- package/dist/core/runner/tool-disclosure.js +2 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/core/write-protect.d.ts +0 -20
- package/dist/core/write-protect.js +4 -3
- package/dist/engine/harness/agent-harness.d.ts +17 -0
- package/dist/engine/harness/agent-harness.js +19 -1
- package/dist/engine/harness/types.d.ts +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/safety.js +1 -1
- package/dist/tools/fs/search.d.ts +33 -0
- package/dist/tools/fs/search.js +72 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +9 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, } from "./engine.js";
|
|
2
2
|
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type MemoryGetDetails, } from "./tools.js";
|
|
3
3
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
4
|
-
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, type ScannedEntryFile, type TransferEvidence, type CommittedBinding, type CommittedEntrySnapshot, type CommittedScopeSnapshots, type EntryCustodyReport, erasureSelectHash, type EraseMemoryEntriesInput, type ErasureSelect, type ErasedBinding, type MemoryErasureAttestation, } from "./file-backend.js";
|
|
4
|
+
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, type ScannedEntryFile, type TransferEvidence, type CommittedBinding, type CommittedEntrySnapshot, type CommittedScopeSnapshots, type EntryCustodyReport, erasureSelectHash, type EraseMemoryEntriesInput, type ErasureSelect, type ErasedBinding, type MemoryErasureAttestation, type MemoryExportSnapshot, } from "./file-backend.js";
|
|
5
|
+
export { computeBundleSectionHashes, computeMemoryBundleHash, memoryBundleInvalid, type MemoryExportBundle, type MemoryImportReport, type MemoryBundleImportPlan, type BundleChallengeRow, type BundleLineageRow, type BundlePollutedSession, } from "./export-bundle.js";
|
|
5
6
|
export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengeAppendResult, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, } from "./layout.js";
|
|
6
7
|
export { readV2HeaderHints, type V2HeaderHints } from "./header-hints.js";
|
|
7
8
|
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, type ParsedEntryFile } from "./frontmatter.js";
|
|
@@ -2,6 +2,7 @@ export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION
|
|
|
2
2
|
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, } from "./tools.js";
|
|
3
3
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
4
4
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, erasureSelectHash, } from "./file-backend.js";
|
|
5
|
+
export { computeBundleSectionHashes, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
|
|
5
6
|
export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, } from "./layout.js";
|
|
6
7
|
export { readV2HeaderHints } from "./header-hints.js";
|
|
7
8
|
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile } from "./frontmatter.js";
|
|
@@ -248,6 +248,16 @@ export interface SidecarLockOptions {
|
|
|
248
248
|
* volume, revoked permission) makes mkdir fail forever while the lock dir it would stat never
|
|
249
249
|
* exists — spun this synchronous loop at full speed and never reached its own cap. */
|
|
250
250
|
export declare function acquireSidecarLock(lockDir: string, opts?: SidecarLockOptions): string;
|
|
251
|
+
/** 证伪式复审 L2-analog: re-verify the owner token at the COMMIT POINT — a stolen-from holder
|
|
252
|
+
* aborts instead of writing over the stealer. A FOREIGN token always aborts (positive proof the
|
|
253
|
+
* lock is someone else's now). What an ABSENT/UNREADABLE owner file means splits by caller family
|
|
254
|
+
* (backlog #255): the fail-closed LEDGER family (`"strict"` — challenges/lineage via
|
|
255
|
+
* lockedStrictUpdate) treats unprovable ownership as lost — those sidecars promise their commit
|
|
256
|
+
* point is fenced, and "cannot disprove" is not a fence; the fail-open calibration family
|
|
257
|
+
* (`"lenient"`, the pre-#255 shape — e.g. the scope registry, whose sidecars self-heal at the next
|
|
258
|
+
* locked update) keeps the leniency, because its token writes are best-effort and an absent owner
|
|
259
|
+
* file there is the documented pre-token degraded shape, not evidence of a steal. */
|
|
260
|
+
export declare function assertSidecarLockOwnership(lockDir: string, token: string, what: string, mode?: "strict" | "lenient"): void;
|
|
251
261
|
interface AnnouncementsRecord {
|
|
252
262
|
/** How many announcements were dropped by the bounded-queue fold (disclosed at render). */
|
|
253
263
|
folded: number;
|
|
@@ -284,6 +294,25 @@ export declare function markSessionPolluted(controlDir: string, sessionId: strin
|
|
|
284
294
|
* record stands in. Only ABSENCE is clean, so a control plane that cannot be stat'ed at all reads
|
|
285
295
|
* clean for every session rather than disabling memory deployment-wide. */
|
|
286
296
|
export declare function readSessionPollution(controlDir: string, sessionId: string): SessionPollutionRecord | undefined;
|
|
297
|
+
/**
|
|
298
|
+
* design/178 v2-c §3-3 — enumerate EVERY durable pollution marker (the export snapshot's pollution
|
|
299
|
+
* face; `readSessionPollution` stays the per-session read). Same fail-closed record posture as the
|
|
300
|
+
* per-session read: a marker file that exists but cannot be read or parsed answers a synthesized
|
|
301
|
+
* record, never a silent omission (corruption must not launder the state out of an export). An
|
|
302
|
+
* absent marker directory is the genuine empty state (ENOENT-only); any OTHER enumeration failure
|
|
303
|
+
* THROWS — a listing that silently dropped markers would export a package that launders pollution.
|
|
304
|
+
*/
|
|
305
|
+
export declare function listSessionPollution(controlDir: string): Record<string, SessionPollutionRecord>;
|
|
306
|
+
/**
|
|
307
|
+
* design/178 v2-c §5.3 — the STRICT durable pollution import. Deliberately NOT
|
|
308
|
+
* `markSessionPolluted`: that leg is best-effort-with-disclosure and mints its own `at`, so a
|
|
309
|
+
* bundle's source record (the governance fact being carried) could neither land verbatim nor prove
|
|
310
|
+
* durability. This leg writes the SOURCE record verbatim (`wx`); on EEXIST it reads the standing
|
|
311
|
+
* record back and COMPARES — equal = converged no-op, different = the destination's first-mark-wins
|
|
312
|
+
* record stands and the divergence is REPORTED (`"divergent"`), never silently absorbed. Any other
|
|
313
|
+
* write failure THROWS (the import aborts with its latch standing — §1-2⑤).
|
|
314
|
+
*/
|
|
315
|
+
export declare function importSessionPollution(controlDir: string, sessionId: string, record: SessionPollutionRecord): "written" | "already" | "divergent";
|
|
287
316
|
/** The retrieved-account sidecar (control plane): entry id → { count, lastAt }. */
|
|
288
317
|
export declare const USAGE_RETRIEVED_FILE = "usage-retrieved.json";
|
|
289
318
|
/** Bound on tracked ids — over the cap, the entries with the OLDEST `lastAt` are evicted first
|
|
@@ -320,10 +349,15 @@ export interface LineageContribution {
|
|
|
320
349
|
lastRev: string;
|
|
321
350
|
lastAt: number;
|
|
322
351
|
}
|
|
323
|
-
/** One staged (pre-commit) row: an entry this transaction WOULD commit.
|
|
352
|
+
/** One staged (pre-commit) row: an entry this transaction WOULD commit. `kind: "latch-only"` marks
|
|
353
|
+
* a bundle-import SYNTHETIC latch row (design/178 v2-c §1): it withholds the id on the model-visible
|
|
354
|
+
* read faces exactly like a real staged row, but it is NOT a contribution — the erasure lane's
|
|
355
|
+
* capture-and-clear strikes it without crediting its txn's sessionId, and the host adjudication
|
|
356
|
+
* refuses to touch its transaction (the import protocol's receipt-gated release is the only exit). */
|
|
324
357
|
export interface LineagePendingRow {
|
|
325
358
|
entryId: string;
|
|
326
359
|
rev: string;
|
|
360
|
+
kind?: "latch-only";
|
|
327
361
|
}
|
|
328
362
|
/** One staged transaction: rows land BEFORE `applyPatches`; the commit credential lands after it
|
|
329
363
|
* succeeds; promotion consumes both. A pending txn without a credential is the crash window —
|
|
@@ -338,6 +372,12 @@ export interface LineagePendingTxn {
|
|
|
338
372
|
appliedIds: string[];
|
|
339
373
|
at: number;
|
|
340
374
|
};
|
|
375
|
+
/** design/178 v2-c §1 — present ⇔ this txn is a bundle-import SYNTHETIC LATCH: it never promotes
|
|
376
|
+
* (no credential is ever written), the generic adjudication REFUSES both arms on it, and the only
|
|
377
|
+
* release channel is {@link releaseImportLatch} (durable completion-receipt precondition). An
|
|
378
|
+
* older binary reads it as an ordinary uncredentialed pending txn and reports it undecidable —
|
|
379
|
+
* the accepted mixed-version posture (the drain precondition of the ledger family applies). */
|
|
380
|
+
kind?: "latch-only";
|
|
341
381
|
}
|
|
342
382
|
/** Stage this transaction's would-commit rows BEFORE `applyPatches` (write-ahead). Throws on any
|
|
343
383
|
* write/lock/corruption failure — the caller refuses the whole harvest (fail-closed). */
|
|
@@ -373,8 +413,39 @@ export declare function reconcileLineage(controlDir: string, now: () => number):
|
|
|
373
413
|
txnId: string;
|
|
374
414
|
sessionId: string;
|
|
375
415
|
entryIds: string[];
|
|
416
|
+
kind?: "latch-only";
|
|
376
417
|
}>;
|
|
377
418
|
};
|
|
419
|
+
/** design/178 v2-c §1 — the import COMPLETION-RECEIPT directory (control plane): one `wx`-minted
|
|
420
|
+
* JSON per bundle hash, written only after the import's governance legs all landed. Its presence
|
|
421
|
+
* is {@link releaseImportLatch}'s hard precondition and the idempotent re-import's short-circuit
|
|
422
|
+
* credential. */
|
|
423
|
+
export declare const IMPORT_RECEIPTS_DIR = "import-receipts";
|
|
424
|
+
/** The lineage txn id of a bundle import's synthetic latch (ONE latch per bundle — a re-import of
|
|
425
|
+
* the same bundle re-stages the same txn id, which is what makes the latch idempotent). */
|
|
426
|
+
export declare function importLatchTxnId(bundleHash: string): string;
|
|
427
|
+
/**
|
|
428
|
+
* design/178 v2-c §1-1 — stage the bundle import's SYNTHETIC LATCH: one pending lineage txn
|
|
429
|
+
* (txnId = {@link importLatchTxnId}) whose rows name every bundle entry id, every row and the txn
|
|
430
|
+
* itself carrying the structural `kind: "latch-only"` seat. From this write until
|
|
431
|
+
* {@link releaseImportLatch}, those ids are withheld on every model-visible read face (the same
|
|
432
|
+
* dirty-latch consumption real staged rows get) — a crash anywhere inside the import leaves the
|
|
433
|
+
* withhold standing (durable pending row), and re-staging the same bundle is an idempotent
|
|
434
|
+
* overwrite with identical content. Never credentialed, never promoted.
|
|
435
|
+
*/
|
|
436
|
+
export declare function stageImportLatch(controlDir: string, bundleHash: string, rows: ReadonlyArray<{
|
|
437
|
+
entryId: string;
|
|
438
|
+
rev: string;
|
|
439
|
+
}>, now: () => number): string;
|
|
440
|
+
/**
|
|
441
|
+
* design/178 v2-c §1-2⑥ — the ONE latch release channel. Hard precondition: the durable completion
|
|
442
|
+
* receipt for the bundle is IN PLACE (`import-receipts/<bundleHash>.json`, minted after the
|
|
443
|
+
* governance legs all landed) — absent receipt ⇒ loud refusal, the latch stands, and convergence is
|
|
444
|
+
* re-running the same bundle import. Receipt-presence is ENOENT-only-absent (a probe failure never
|
|
445
|
+
* reads as "no receipt" — fail-closed). Idempotent: an already-released (absent) txn is a no-op;
|
|
446
|
+
* a NON-latch txn under the id is refused (this is never a generic discard).
|
|
447
|
+
*/
|
|
448
|
+
export declare function releaseImportLatch(controlDir: string, txnId: string): void;
|
|
378
449
|
/** The DIRTY-LATCH set: every entry id named by ANY pending row (credentialed or not). While an id
|
|
379
450
|
* is latched, its model-visible read faces (index mechanical rows / memory_search / memory_get)
|
|
380
451
|
* refuse — "the account is not settled, the content does not go on the table". Throws on a
|
|
@@ -429,6 +500,33 @@ export declare function clearLineageForEntries(controlDir: string, entryIds: rea
|
|
|
429
500
|
* carries the chain's pre-captured set (honest downgrade, never a fabricated rebuild).
|
|
430
501
|
*/
|
|
431
502
|
export declare function captureAndClearLineageForEntries(controlDir: string, entryIds: readonly string[]): Map<string, string[]>;
|
|
503
|
+
/** One (entryId, sessionId) pair whose imported value LOST to the destination's standing row —
|
|
504
|
+
* design/178 v2-c §5.1's disclosure seat (kept is always "destination": the in-account fact is
|
|
505
|
+
* never overwritten by an equal-time divergent import, only reported). */
|
|
506
|
+
export interface LineageImportDivergence {
|
|
507
|
+
entryId: string;
|
|
508
|
+
sessionId: string;
|
|
509
|
+
kept: "destination";
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* design/178 v2-c §5.1 — the lineage FIDELITY importer: write bundle lineage rows into the
|
|
513
|
+
* committed set VERBATIM (`lastRev`/`lastAt` carried byte-for-byte — the promote leg mints its own
|
|
514
|
+
* `now` and is therefore unusable for carriage). Deterministic same-key merge, idempotent under
|
|
515
|
+
* re-import of the same bundle:
|
|
516
|
+
* - destination has no (entryId, sessionId) row ⇒ the source row lands verbatim;
|
|
517
|
+
* - both present, source `lastAt` is GREATER ⇒ source wins ("last contribution" is a time-axis
|
|
518
|
+
* fact); destination greater ⇒ destination stands (no divergence — the merge is deterministic);
|
|
519
|
+
* - equal `lastAt`, equal `lastRev` ⇒ no-op; equal `lastAt`, DIFFERENT `lastRev` ⇒ the
|
|
520
|
+
* destination row stands and the pair is reported (first-mark-wins side: an in-account fact is
|
|
521
|
+
* never rewritten by an外来 package; the disagreement must not be silent).
|
|
522
|
+
* Throws on a corrupt ledger (fail-closed family).
|
|
523
|
+
*/
|
|
524
|
+
export declare function importLineageCommitted(controlDir: string, rows: ReadonlyArray<{
|
|
525
|
+
entryId: string;
|
|
526
|
+
sessionId: string;
|
|
527
|
+
lastRev: string;
|
|
528
|
+
lastAt: number;
|
|
529
|
+
}>): LineageImportDivergence[];
|
|
432
530
|
/** Full-ledger read (tests / host observability). Throws on corruption. */
|
|
433
531
|
export declare function readLineageRecord(controlDir: string): {
|
|
434
532
|
committed: Record<string, Record<string, LineageContribution>>;
|
|
@@ -598,12 +598,15 @@ function lockedJournaledUpdate(controlDir, fileName, mutate) {
|
|
|
598
598
|
releaseSidecarLock(lock, token);
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
|
-
function assertSidecarLockOwnership(lockDir, token, what) {
|
|
601
|
+
export function assertSidecarLockOwnership(lockDir, token, what, mode = "lenient") {
|
|
602
602
|
let held;
|
|
603
603
|
try {
|
|
604
604
|
held = readFileSync(join(lockDir, "owner"), "utf8");
|
|
605
605
|
}
|
|
606
|
-
catch {
|
|
606
|
+
catch (err) {
|
|
607
|
+
if (mode === "strict" && err.code !== undefined) {
|
|
608
|
+
throw new ControlPlaneCorruptError(`${what}: sidecar lock ownership UNPROVABLE at commit (owner file ${err.code === "ENOENT" ? "absent" : `unreadable: ${err.code}`}) — a fail-closed ledger must not commit on a fence it cannot prove; update aborted`, { cause: err });
|
|
609
|
+
}
|
|
607
610
|
return;
|
|
608
611
|
}
|
|
609
612
|
if (held !== token) {
|
|
@@ -766,6 +769,48 @@ export function readSessionPollution(controlDir, sessionId) {
|
|
|
766
769
|
}
|
|
767
770
|
return { at: 0, reason: "pollution marker present but unreadable (kept fail-closed)" };
|
|
768
771
|
}
|
|
772
|
+
export function listSessionPollution(controlDir) {
|
|
773
|
+
const dir = join(controlDir, SESSION_POLLUTION_DIR);
|
|
774
|
+
let names;
|
|
775
|
+
try {
|
|
776
|
+
names = readdirSync(dir);
|
|
777
|
+
}
|
|
778
|
+
catch (err) {
|
|
779
|
+
if (err.code === "ENOENT")
|
|
780
|
+
return {};
|
|
781
|
+
throw new ControlPlaneCorruptError(`session pollution markers could not be enumerated (${err.code ?? "io error"}) at ${dir} — an enumeration failure is not an empty set (fail-closed)`, { cause: err });
|
|
782
|
+
}
|
|
783
|
+
const out = Object.create(null);
|
|
784
|
+
for (const name of names.sort()) {
|
|
785
|
+
if (!name.endsWith(".json"))
|
|
786
|
+
continue;
|
|
787
|
+
let sessionId;
|
|
788
|
+
try {
|
|
789
|
+
sessionId = decodeURIComponent(name.slice(0, -".json".length));
|
|
790
|
+
}
|
|
791
|
+
catch {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
const rec = readSessionPollution(controlDir, sessionId);
|
|
795
|
+
if (rec !== undefined)
|
|
796
|
+
out[sessionId] = rec;
|
|
797
|
+
}
|
|
798
|
+
return out;
|
|
799
|
+
}
|
|
800
|
+
export function importSessionPollution(controlDir, sessionId, record) {
|
|
801
|
+
const path = pollutionPath(controlDir, sessionId);
|
|
802
|
+
ensureDirExists(dirname(path));
|
|
803
|
+
try {
|
|
804
|
+
writeFileSync(path, `${JSON.stringify({ at: record.at, reason: record.reason }, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
805
|
+
return "written";
|
|
806
|
+
}
|
|
807
|
+
catch (err) {
|
|
808
|
+
if (!(err instanceof Error && "code" in err && err.code === "EEXIST"))
|
|
809
|
+
throw err;
|
|
810
|
+
}
|
|
811
|
+
const standing = readSessionPollution(controlDir, sessionId);
|
|
812
|
+
return standing !== undefined && standing.at === record.at && standing.reason === record.reason ? "already" : "divergent";
|
|
813
|
+
}
|
|
769
814
|
export const USAGE_RETRIEVED_FILE = "usage-retrieved.json";
|
|
770
815
|
export const USAGE_RETRIEVED_MAX_IDS = 4096;
|
|
771
816
|
function coerceRetrievedAccount(raw) {
|
|
@@ -855,7 +900,7 @@ function lockedStrictUpdate(controlDir, fileName, what, coerce, fn) {
|
|
|
855
900
|
const { next, result } = fn(current);
|
|
856
901
|
if (next !== undefined) {
|
|
857
902
|
const data = `${JSON.stringify(next, null, 2)}\n`;
|
|
858
|
-
assertSidecarLockOwnership(lock, token, what);
|
|
903
|
+
assertSidecarLockOwnership(lock, token, what, "strict");
|
|
859
904
|
atomicWriteFileSync(journal, data);
|
|
860
905
|
atomicWriteFileSync(file, data);
|
|
861
906
|
rmSync(journal, { force: true });
|
|
@@ -890,7 +935,10 @@ function readStrictSidecar(controlDir, fileName, what) {
|
|
|
890
935
|
try {
|
|
891
936
|
journalRaw = readFileSync(journal, "utf8");
|
|
892
937
|
}
|
|
893
|
-
catch {
|
|
938
|
+
catch (err) {
|
|
939
|
+
if (err.code !== "ENOENT") {
|
|
940
|
+
throw new ControlPlaneCorruptError(`control-plane journal unreadable ((${err.code ?? "io error"})) — a committed next state may exist that cannot be proven; repair the read fault rather than serving the pre-transaction state: ${journal}`, { cause: err });
|
|
941
|
+
}
|
|
894
942
|
journalRaw = undefined;
|
|
895
943
|
}
|
|
896
944
|
if (journalRaw !== undefined) {
|
|
@@ -932,10 +980,14 @@ function coerceLineage(raw) {
|
|
|
932
980
|
if (!txn || typeof txn !== "object" || typeof txn.sessionId !== "string" || typeof txn.at !== "number" || !Array.isArray(txn.rows)) {
|
|
933
981
|
throw badShape(`pending[${JSON.stringify(txnId)}]`);
|
|
934
982
|
}
|
|
983
|
+
if (txn.kind !== undefined && txn.kind !== "latch-only")
|
|
984
|
+
throw badShape(`pending[${JSON.stringify(txnId)}] kind`);
|
|
935
985
|
for (const row of txn.rows) {
|
|
936
986
|
const p = row;
|
|
937
987
|
if (!p || typeof p !== "object" || typeof p.entryId !== "string" || typeof p.rev !== "string")
|
|
938
988
|
throw badShape(`pending[${JSON.stringify(txnId)}] row`);
|
|
989
|
+
if (p.kind !== undefined && p.kind !== "latch-only")
|
|
990
|
+
throw badShape(`pending[${JSON.stringify(txnId)}] row kind`);
|
|
939
991
|
}
|
|
940
992
|
if (txn.credential !== undefined) {
|
|
941
993
|
const c = txn.credential;
|
|
@@ -1006,6 +1058,9 @@ export function adjudicateLineagePending(controlDir, txnId, action, now) {
|
|
|
1006
1058
|
const txn = rec.pending[txnId];
|
|
1007
1059
|
if (txn === undefined)
|
|
1008
1060
|
return { result: [] };
|
|
1061
|
+
if (isLatchOnlyTxn(txn)) {
|
|
1062
|
+
throw new ControlPlaneCorruptError(`memory lineage ledger: pending transaction ${txnId} is a bundle-import latch — it cannot be ${action === "promote" ? "promoted (its rows are not contributions)" : "discarded (its withhold guards not-yet-governed imported entries)"}; re-run the same bundle import to converge it (releaseImportLatch is receipt-gated)`);
|
|
1063
|
+
}
|
|
1009
1064
|
if (action === "discard") {
|
|
1010
1065
|
delete rec.pending[txnId];
|
|
1011
1066
|
return { next: rec, result: [] };
|
|
@@ -1032,7 +1087,7 @@ export function reconcileLineage(controlDir, now) {
|
|
|
1032
1087
|
let changed = false;
|
|
1033
1088
|
for (const [txnId, txn] of Object.entries(rec.pending)) {
|
|
1034
1089
|
if (txn.credential === undefined) {
|
|
1035
|
-
undecidable.push({ txnId, sessionId: txn.sessionId, entryIds: txn.rows.map((r) => r.entryId) });
|
|
1090
|
+
undecidable.push({ txnId, sessionId: txn.sessionId, entryIds: txn.rows.map((r) => r.entryId), ...(isLatchOnlyTxn(txn) ? { kind: "latch-only" } : {}) });
|
|
1036
1091
|
continue;
|
|
1037
1092
|
}
|
|
1038
1093
|
const applied = new Set(txn.credential.appliedIds);
|
|
@@ -1050,6 +1105,66 @@ export function reconcileLineage(controlDir, now) {
|
|
|
1050
1105
|
return { ...(changed ? { next: rec } : {}), result: { promoted, undecidable } };
|
|
1051
1106
|
});
|
|
1052
1107
|
}
|
|
1108
|
+
function isLatchOnlyTxn(txn) {
|
|
1109
|
+
if (txn.kind === "latch-only")
|
|
1110
|
+
return true;
|
|
1111
|
+
return txn.rows.some((row) => row.kind === "latch-only");
|
|
1112
|
+
}
|
|
1113
|
+
export const IMPORT_RECEIPTS_DIR = "import-receipts";
|
|
1114
|
+
export function importLatchTxnId(bundleHash) {
|
|
1115
|
+
return `import:${bundleHash}`;
|
|
1116
|
+
}
|
|
1117
|
+
function importReceiptPath(controlDir, bundleHash) {
|
|
1118
|
+
return join(controlDir, IMPORT_RECEIPTS_DIR, `${bundleHash}.json`);
|
|
1119
|
+
}
|
|
1120
|
+
export function stageImportLatch(controlDir, bundleHash, rows, now) {
|
|
1121
|
+
const txnId = importLatchTxnId(bundleHash);
|
|
1122
|
+
lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
|
|
1123
|
+
const existing = rec.pending[txnId];
|
|
1124
|
+
if (existing !== undefined && !isLatchOnlyTxn(existing)) {
|
|
1125
|
+
throw new ControlPlaneCorruptError(`memory lineage ledger: pending transaction ${txnId} exists and is NOT an import latch — refusing to overwrite a real staged transaction`);
|
|
1126
|
+
}
|
|
1127
|
+
rec.pending[txnId] = {
|
|
1128
|
+
sessionId: txnId,
|
|
1129
|
+
at: now(),
|
|
1130
|
+
kind: "latch-only",
|
|
1131
|
+
rows: rows.map((r) => ({ entryId: r.entryId, rev: r.rev, kind: "latch-only" })),
|
|
1132
|
+
};
|
|
1133
|
+
return { next: rec, result: undefined };
|
|
1134
|
+
});
|
|
1135
|
+
return txnId;
|
|
1136
|
+
}
|
|
1137
|
+
export function releaseImportLatch(controlDir, txnId) {
|
|
1138
|
+
const prefix = "import:";
|
|
1139
|
+
if (!txnId.startsWith(prefix) || !/^[0-9a-f]{64}$/.test(txnId.slice(prefix.length))) {
|
|
1140
|
+
throw new ControlPlaneCorruptError(`releaseImportLatch: ${JSON.stringify(txnId)} is not an import-latch transaction id (import:<64-hex bundleHash>)`);
|
|
1141
|
+
}
|
|
1142
|
+
const receipt = importReceiptPath(controlDir, txnId.slice(prefix.length));
|
|
1143
|
+
let receiptPresent;
|
|
1144
|
+
try {
|
|
1145
|
+
readFileSync(receipt);
|
|
1146
|
+
receiptPresent = true;
|
|
1147
|
+
}
|
|
1148
|
+
catch (err) {
|
|
1149
|
+
if (err.code !== "ENOENT") {
|
|
1150
|
+
throw new ControlPlaneCorruptError(`releaseImportLatch: the completion receipt at ${receipt} could not be read (${err.code ?? "io error"}) — a probe failure is not absence (fail-closed), and absence would refuse the release`, { cause: err });
|
|
1151
|
+
}
|
|
1152
|
+
receiptPresent = false;
|
|
1153
|
+
}
|
|
1154
|
+
if (!receiptPresent) {
|
|
1155
|
+
throw new ControlPlaneCorruptError(`releaseImportLatch: no completion receipt at ${receipt} — the import's governance legs are not proven landed, the latch stays; re-run the same bundle import to converge`);
|
|
1156
|
+
}
|
|
1157
|
+
lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
|
|
1158
|
+
const txn = rec.pending[txnId];
|
|
1159
|
+
if (txn === undefined)
|
|
1160
|
+
return { result: undefined };
|
|
1161
|
+
if (!isLatchOnlyTxn(txn)) {
|
|
1162
|
+
throw new ControlPlaneCorruptError(`releaseImportLatch: pending transaction ${txnId} is NOT an import latch — refusing (this channel never discards a real staged transaction)`);
|
|
1163
|
+
}
|
|
1164
|
+
delete rec.pending[txnId];
|
|
1165
|
+
return { next: rec, result: undefined };
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1053
1168
|
export function lineageLatchedIds(controlDir) {
|
|
1054
1169
|
const rec = coerceLineage(readStrictSidecar(controlDir, LINEAGE_FILE, "memory lineage ledger"));
|
|
1055
1170
|
const out = new Set();
|
|
@@ -1134,6 +1249,27 @@ export function captureAndClearLineageForEntries(controlDir, entryIds) {
|
|
|
1134
1249
|
return { ...(changed ? { next: rec } : {}), result };
|
|
1135
1250
|
});
|
|
1136
1251
|
}
|
|
1252
|
+
export function importLineageCommitted(controlDir, rows) {
|
|
1253
|
+
if (rows.length === 0)
|
|
1254
|
+
return [];
|
|
1255
|
+
return lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
|
|
1256
|
+
const divergence = [];
|
|
1257
|
+
let changed = false;
|
|
1258
|
+
for (const row of rows) {
|
|
1259
|
+
const sessions = (rec.committed[row.entryId] ??= Object.create(null));
|
|
1260
|
+
const standing = Object.prototype.hasOwnProperty.call(sessions, row.sessionId) ? sessions[row.sessionId] : undefined;
|
|
1261
|
+
if (standing === undefined || row.lastAt > standing.lastAt) {
|
|
1262
|
+
sessions[row.sessionId] = { lastRev: row.lastRev, lastAt: row.lastAt };
|
|
1263
|
+
changed = true;
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (row.lastAt === standing.lastAt && row.lastRev !== standing.lastRev) {
|
|
1267
|
+
divergence.push({ entryId: row.entryId, sessionId: row.sessionId, kept: "destination" });
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return { ...(changed ? { next: rec } : {}), result: divergence };
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1137
1273
|
export function readLineageRecord(controlDir) {
|
|
1138
1274
|
const rec = coerceLineage(readStrictSidecar(controlDir, LINEAGE_FILE, "memory lineage ledger"));
|
|
1139
1275
|
return { committed: rec.committed, pending: rec.pending };
|
|
@@ -1291,7 +1427,7 @@ export function rebuildStrictControlPlaneLedger(controlDir, ledger, now) {
|
|
|
1291
1427
|
}
|
|
1292
1428
|
const at = now();
|
|
1293
1429
|
const quarantinedTo = [];
|
|
1294
|
-
assertSidecarLockOwnership(lock, token, what);
|
|
1430
|
+
assertSidecarLockOwnership(lock, token, what, "strict");
|
|
1295
1431
|
for (const path of [file, journal]) {
|
|
1296
1432
|
if (!existsSync(path))
|
|
1297
1433
|
continue;
|
|
@@ -1309,7 +1445,7 @@ export function rebuildStrictControlPlaneLedger(controlDir, ledger, now) {
|
|
|
1309
1445
|
}
|
|
1310
1446
|
quarantinedTo.push(dest);
|
|
1311
1447
|
}
|
|
1312
|
-
assertSidecarLockOwnership(lock, token, what);
|
|
1448
|
+
assertSidecarLockOwnership(lock, token, what, "strict");
|
|
1313
1449
|
atomicWriteFileSync(file, `${JSON.stringify(empty, null, 2)}\n`);
|
|
1314
1450
|
rmSync(journal, { force: true });
|
|
1315
1451
|
return { ledger, quarantinedTo, at };
|
|
@@ -22,7 +22,7 @@ export interface MemoryBackendContractHooks {
|
|
|
22
22
|
* assembly answers it as capability-absent.
|
|
23
23
|
*/
|
|
24
24
|
onOptionalMember?: (report: {
|
|
25
|
-
member: "committedSnapshotOf" | "committedSnapshotsOfScopes" | "custodyOf" | "eraseWithEvidence";
|
|
25
|
+
member: "committedSnapshotOf" | "committedSnapshotsOfScopes" | "custodyOf" | "eraseWithEvidence" | "exportSnapshotOf" | "governanceExport" | "custodyImport" | "importBundleCommit";
|
|
26
26
|
status: "verified" | "absent";
|
|
27
27
|
}) => void;
|
|
28
28
|
}
|
|
@@ -384,6 +384,58 @@ export async function memoryBackendContract(hooks) {
|
|
|
384
384
|
await assert.rejects(async () => erase.call(b, { requestId: "req-contract-1", select: { ids: ["id-other-7777"] } }), (err) => err.code === "memory.erasure_selector_mismatch", "a reused requestId with a different selector refuses loudly");
|
|
385
385
|
hooks.onOptionalMember?.({ member: "eraseWithEvidence", status: "verified" });
|
|
386
386
|
});
|
|
387
|
+
defer("design/178 v2-c optional export/import faces: export composite / governance slice / custody carriage (absent ⇒ reported)", async () => {
|
|
388
|
+
const b = await hooks.make();
|
|
389
|
+
const seats = [
|
|
390
|
+
["exportSnapshotOf", b.exportSnapshotOf],
|
|
391
|
+
["governanceExport", b.governanceExport],
|
|
392
|
+
["custodyImport", b.custodyImport],
|
|
393
|
+
["importBundleCommit", b.importBundleCommit],
|
|
394
|
+
];
|
|
395
|
+
for (const [member, seat] of seats) {
|
|
396
|
+
assert.ok(seat === undefined || typeof seat === "function", `${member} must be undefined (absent) or a function — a non-function occupant is a defect, not absence`);
|
|
397
|
+
}
|
|
398
|
+
const e = entry("id-export-0001", "s1", "exported", "exported body", { name: "Exported" });
|
|
399
|
+
await b.applyPatches([{ op: "add", id: e.id, entry: e }]);
|
|
400
|
+
const exportSnapshotOf = typeof seats[0]?.[1] === "function" ? seats[0][1] : undefined;
|
|
401
|
+
if (exportSnapshotOf === undefined) {
|
|
402
|
+
hooks.onOptionalMember?.({ member: "exportSnapshotOf", status: "absent" });
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
const snap = await exportSnapshotOf.call(b, ["s1"]);
|
|
406
|
+
assert.ok(typeof snap.storeId === "string" && snap.storeId.length > 0, "the export snapshot names the store identity");
|
|
407
|
+
assert.strictEqual(typeof snap.fullStore, "boolean");
|
|
408
|
+
assert.deepStrictEqual(snap.entries.map((r) => r.id), [e.id], "the composite answers the committed entries of the requested scopes");
|
|
409
|
+
assert.strictEqual(snap.entries[0]?.rev, e.rev, "entries carry the committed rev");
|
|
410
|
+
assert.ok(Array.isArray(snap.custody) && Array.isArray(snap.unbound) && Array.isArray(snap.pendingLatch));
|
|
411
|
+
hooks.onOptionalMember?.({ member: "exportSnapshotOf", status: "verified" });
|
|
412
|
+
}
|
|
413
|
+
const governanceExport = typeof seats[1]?.[1] === "function" ? seats[1][1] : undefined;
|
|
414
|
+
if (governanceExport === undefined) {
|
|
415
|
+
hooks.onOptionalMember?.({ member: "governanceExport", status: "absent" });
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
const slice = await governanceExport.call(b, ["s1"]);
|
|
419
|
+
assert.strictEqual(slice.complete, true, "a quiet store's governance slice answers complete");
|
|
420
|
+
assert.ok(Array.isArray(slice.custody) && Array.isArray(slice.unbound));
|
|
421
|
+
hooks.onOptionalMember?.({ member: "governanceExport", status: "verified" });
|
|
422
|
+
}
|
|
423
|
+
const custodyImport = typeof seats[2]?.[1] === "function" ? seats[2][1] : undefined;
|
|
424
|
+
if (custodyImport === undefined) {
|
|
425
|
+
hooks.onOptionalMember?.({ member: "custodyImport", status: "absent" });
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
const opts = { bundleHash: "c".repeat(64), sourceStoreId: "contract-src-store" };
|
|
429
|
+
const outcome = await custodyImport.call(b, [{ ev: "kit-del-1", channel: "delete", id: "id-ghost-kit-1", rev: "deadbeefdeadbeef", at: 1 }], opts);
|
|
430
|
+
assert.strictEqual(outcome.appended, 1, "a well-formed delete row for an absent id is carried (anti-resurrection evidence needs no living entry)");
|
|
431
|
+
assert.deepStrictEqual(outcome.withheld, []);
|
|
432
|
+
const unknown = await custodyImport.call(b, [{ ev: "kit-unk-1", channel: "kit-unknown-channel", payload: 1, at: 1 }], opts);
|
|
433
|
+
assert.strictEqual(unknown.appended, 0, "an unknown channel is never appended verbatim");
|
|
434
|
+
assert.deepStrictEqual(unknown.withheld, [{ srcEv: "kit-unk-1", channel: "kit-unknown-channel" }], "an unknown channel is withheld AND reported — never a whole-package refusal");
|
|
435
|
+
hooks.onOptionalMember?.({ member: "custodyImport", status: "verified" });
|
|
436
|
+
}
|
|
437
|
+
hooks.onOptionalMember?.({ member: "importBundleCommit", status: typeof seats[3]?.[1] === "function" ? "verified" : "absent" });
|
|
438
|
+
});
|
|
387
439
|
if (!hooks.makeSibling) {
|
|
388
440
|
const reason = hooks.skipCrossInstanceCas?.trim();
|
|
389
441
|
if (reason) {
|
|
@@ -163,7 +163,14 @@ export function createMemoryEngineTools(opts) {
|
|
|
163
163
|
throw err;
|
|
164
164
|
return refusedSearch("error", GENERIC_FAILURE, "failed");
|
|
165
165
|
}
|
|
166
|
-
|
|
166
|
+
let terminalExclusions;
|
|
167
|
+
try {
|
|
168
|
+
terminalExclusions = planes.map((plane) => plane.challengeExclusions?.());
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return refusedSearch("challenge_ledger_unavailable", "Memory search is unavailable: the challenge ledger for a mounted memory plane cannot be read (fail-closed). Report this to the operator.", "failed");
|
|
172
|
+
}
|
|
173
|
+
const live = top.filter((h) => bodyById.has(h.id) && terminalExclusions[h.planeIndex]?.has(h.id) !== true);
|
|
167
174
|
if (live.length === 0) {
|
|
168
175
|
const details = { outcome: "ok", hits: [] };
|
|
169
176
|
return { content: `No memory entries matched ${JSON.stringify(inlineUntrusted(query, 120))}. If the user expected you to know this, say that you checked memory and found nothing.`, details };
|
|
@@ -357,14 +357,26 @@ export async function prepareCcImport(opts) {
|
|
|
357
357
|
continue;
|
|
358
358
|
}
|
|
359
359
|
layers.push({ path: layer.path, layer: layer.layer, found: true });
|
|
360
|
-
let
|
|
360
|
+
let settingsRoot;
|
|
361
361
|
try {
|
|
362
|
-
|
|
362
|
+
settingsRoot = JSON.parse(raw);
|
|
363
363
|
}
|
|
364
364
|
catch (err) {
|
|
365
365
|
skipped.push({ rule: layer.path, reason: `settings file is not valid JSON (${errText(err)})` });
|
|
366
366
|
continue;
|
|
367
367
|
}
|
|
368
|
+
let container;
|
|
369
|
+
if (settingsRoot === null)
|
|
370
|
+
container = undefined;
|
|
371
|
+
else if (typeof settingsRoot === "object" && !Array.isArray(settingsRoot))
|
|
372
|
+
container = settingsRoot.permissions;
|
|
373
|
+
else {
|
|
374
|
+
skipped.push({
|
|
375
|
+
rule: layer.path,
|
|
376
|
+
reason: `settings file is not a JSON object — no bucket in this layer could be read, so it is reported as unread rather than as empty (it stays in the settings file either way)`,
|
|
377
|
+
});
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
368
380
|
let permissions;
|
|
369
381
|
if (container === undefined || container === null)
|
|
370
382
|
permissions = undefined;
|
|
@@ -87,6 +87,26 @@ export function prepareConfigDoors(input) {
|
|
|
87
87
|
const { deps, sessions, resume, internals } = input;
|
|
88
88
|
let spec = input.spec;
|
|
89
89
|
assertRestoreGatedToolsValue(spec.restoreGatedTools);
|
|
90
|
+
const assertToolNameListValue = (value, seat) => {
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
return;
|
|
93
|
+
if (!Array.isArray(value)) {
|
|
94
|
+
const e = new Error(`TaskSpec.${seat} must be an array of tool names or absent (got ${value === null ? "null" : typeof value}) — a garbage list must refuse loudly, never be read as "no ${seat}".`);
|
|
95
|
+
e.code = "config.tool_face_control";
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
const entries = value;
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (typeof entry !== "string") {
|
|
101
|
+
const e = new Error(`TaskSpec.${seat} entries must be strings (got ${entry === null ? "null" : typeof entry}).`);
|
|
102
|
+
e.code = "config.tool_face_control";
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
assertToolNameListValue(spec.excludeTools, "excludeTools");
|
|
108
|
+
assertToolNameListValue(spec.deferTools, "deferTools");
|
|
109
|
+
assertToolNameListValue(spec.alwaysLoadTools, "alwaysLoadTools");
|
|
90
110
|
const toolFaceSnapshot = {
|
|
91
111
|
exclude: spec.excludeTools ? Object.freeze([...spec.excludeTools]) : undefined,
|
|
92
112
|
defer: spec.deferTools ? Object.freeze([...spec.deferTools]) : undefined,
|
|
@@ -327,6 +327,13 @@ function orgRevisionEvidenceOf(resolution, onDefect) {
|
|
|
327
327
|
function persistedRuleHitOf(admitting) {
|
|
328
328
|
return admitting === undefined ? undefined : { rule: admitting.rule, dots: admitting.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) };
|
|
329
329
|
}
|
|
330
|
+
function cwdConflictsRestoreError(requestedCwd) {
|
|
331
|
+
const e = new Error(`RunInternals.requestedCwd ("${requestedCwd}") cannot be combined with a checkpoint workspace restore — ` +
|
|
332
|
+
`the restored workspace's own mount path is authoritative for the task root, so a requested cwd on this leg ` +
|
|
333
|
+
`would be ignored (or worse, probed against an unrestored environment). Drop requestedCwd on resume legs.`);
|
|
334
|
+
e.code = "config.cwd_conflicts_restore";
|
|
335
|
+
return e;
|
|
336
|
+
}
|
|
330
337
|
export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
|
|
331
338
|
const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
|
|
332
339
|
spec = doors.spec;
|
|
@@ -421,6 +428,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
421
428
|
e.code = "config.cwd_unsupported";
|
|
422
429
|
throw e;
|
|
423
430
|
}
|
|
431
|
+
if (internals?.requestedCwd !== undefined && resume?.workspaceHandle !== undefined) {
|
|
432
|
+
await settleTeardownLeg(() => forgetOnThrow(), "forgetOnThrow (cwd-conflicts-restore leg)", (err) => deps.onError?.(err, { phase: "config", sessionId }));
|
|
433
|
+
throw cwdConflictsRestoreError(internals.requestedCwd);
|
|
434
|
+
}
|
|
424
435
|
try {
|
|
425
436
|
ownedEnv = deps.executionEnvFactory
|
|
426
437
|
? await deps.executionEnvFactory({
|
|
@@ -1248,6 +1259,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1248
1259
|
if (c.ok)
|
|
1249
1260
|
additionalRootsCanonical.push(c.value);
|
|
1250
1261
|
}
|
|
1262
|
+
for (const a of [additionalRootsCanonical, additionalReadRootsCanonical])
|
|
1263
|
+
a.splice(0, a.length, ...new Set(a));
|
|
1251
1264
|
const readFileState = new Map((resume?.seed.readFileState ?? []).map(([k, v]) => [rebaseRestoredPath(k), v]));
|
|
1252
1265
|
readFileStateForCheckpoint = readFileState;
|
|
1253
1266
|
seedContextFiles = async (files) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
|
|
2
2
|
import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
|
|
3
|
-
import { deliverDelegationLifecycle } from "../types.js";
|
|
3
|
+
import { deliverDelegationLifecycle, deliverEngineNotice } from "../types.js";
|
|
4
4
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
5
5
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
6
6
|
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
@@ -2068,6 +2068,14 @@ export class Runner {
|
|
|
2068
2068
|
for (const p of payloads)
|
|
2069
2069
|
this.pendingSessionNotifications.pend(notificationSessionId, p);
|
|
2070
2070
|
};
|
|
2071
|
+
prepared.harness.onUndrainedUserInputs = (counts) => {
|
|
2072
|
+
deliverEngineNotice(this.deps.onNotice, {
|
|
2073
|
+
code: "task.user_steer_undrained",
|
|
2074
|
+
message: `${counts.steer + counts.followUp} user input(s) accepted as "queued" were never consumed — the run ended first ` +
|
|
2075
|
+
`(${counts.steer} steer, ${counts.followUp} follow-up). They are NOT redelivered; re-send against a live run if still wanted.`,
|
|
2076
|
+
detail: { steer: counts.steer, followUp: counts.followUp, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}) },
|
|
2077
|
+
});
|
|
2078
|
+
};
|
|
2071
2079
|
prepared.harness.onEngineNoteConsumed = (p) => {
|
|
2072
2080
|
const peer = p?.peer;
|
|
2073
2081
|
if (peer !== undefined && Array.isArray(peer.hopChain))
|
|
@@ -4126,6 +4134,11 @@ export class Runner {
|
|
|
4126
4134
|
if (cp.state.workspaceHandle !== undefined && this.deps.executionEnvFactory === undefined) {
|
|
4127
4135
|
throw new CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no RunnerDeps.executionEnvFactory is wired to rebuild the env", { reason: "env_factory_missing" });
|
|
4128
4136
|
}
|
|
4137
|
+
if (cp.state.workspaceHandle !== undefined && internals?.requestedCwd !== undefined) {
|
|
4138
|
+
throw new CheckpointError("checkpoint.cwd_conflicts_restore", `internals.requestedCwd ("${internals.requestedCwd}") cannot be combined with a checkpoint workspace restore — ` +
|
|
4139
|
+
"the restored workspace's own mount path is authoritative for the task root. Drop requestedCwd on resume legs " +
|
|
4140
|
+
"(the checkpoint stays pending and is resumable without it).");
|
|
4141
|
+
}
|
|
4129
4142
|
if (cp.state.inheritedGate?.requiresParentConstraint === true) {
|
|
4130
4143
|
const supplied = internals?.inheritedGate?.parentConstraints?.length ?? 0;
|
|
4131
4144
|
if (supplied === 0) {
|
|
@@ -196,7 +196,9 @@ export function createSkillTool(skills, scope) {
|
|
|
196
196
|
const name = String(a.skill ?? "");
|
|
197
197
|
const skill = byName.get(name);
|
|
198
198
|
if (!skill) {
|
|
199
|
-
throw new Error(`Unknown skill "${name}". Available skills: ${names}
|
|
199
|
+
throw new Error(`Unknown skill "${name}". Available skills: ${names}. ` +
|
|
200
|
+
`(Only skills registered with this deployment are loadable — other products' skills directories are not read. ` +
|
|
201
|
+
`If the user wants one of those here, read its source file and recreate its content for this deployment instead.)`);
|
|
200
202
|
}
|
|
201
203
|
if (scope && skill.manifest) {
|
|
202
204
|
scope.push({ kind: "manifest", manifest: skill.manifest });
|
|
@@ -45,7 +45,8 @@ export function classifyDeferred(opts) {
|
|
|
45
45
|
if (!pinned.has(name))
|
|
46
46
|
deferred.add(name);
|
|
47
47
|
if (opts.deferMode === "auto") {
|
|
48
|
-
const
|
|
48
|
+
const callerNames = new Set(opts.specs.map((s) => s.name));
|
|
49
|
+
const candidates = opts.fullTools.filter((t) => !deferred.has(t.name) && !pinned.has(t.name) && callerNames.has(t.name));
|
|
49
50
|
const inlineFace = opts.fullTools.filter((t) => !deferred.has(t.name));
|
|
50
51
|
const total = inlineFace.reduce((n, t) => n + inlinedChars(t), 0);
|
|
51
52
|
const window = (opts.model.contextTokens ?? opts.model.contextWindow ?? 0) * CHARS_PER_TOKEN;
|