@sema-agent/core 7.30.1 → 7.31.1
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 +284 -0
- package/dist/agents/cross-session-judge.d.ts +4 -12
- package/dist/agents/cross-session-judge.js +4 -11
- package/dist/agents/peer-session-drain.js +0 -5
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +1 -0
- package/dist/core/ask-origin.d.ts +6 -0
- package/dist/core/ask-origin.js +10 -0
- package/dist/core/checkpoint-store.d.ts +14 -0
- package/dist/core/checkpoint-store.js +1 -0
- package/dist/core/engine-notice.d.ts +4 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/memory-engine/data-plane.d.ts +3 -1
- package/dist/core/memory-engine/data-plane.js +1 -1
- package/dist/core/memory-engine/delegation-settlement.d.ts +44 -3
- package/dist/core/memory-engine/delegation-settlement.js +38 -3
- package/dist/core/memory-engine/engine.d.ts +133 -27
- package/dist/core/memory-engine/engine.js +452 -151
- package/dist/core/memory-engine/entry-scan.d.ts +23 -0
- package/dist/core/memory-engine/entry-scan.js +55 -0
- package/dist/core/memory-engine/file-backend.d.ts +31 -23
- package/dist/core/memory-engine/file-backend.js +174 -100
- package/dist/core/memory-engine/layer-observations.d.ts +109 -0
- package/dist/core/memory-engine/layer-observations.js +148 -0
- package/dist/core/memory-engine/layout.d.ts +40 -4
- package/dist/core/memory-engine/layout.js +43 -7
- package/dist/core/memory-engine/types.d.ts +26 -2
- package/dist/core/parent-spec-seat.d.ts +4 -1
- package/dist/core/parent-spec-seat.js +2 -0
- package/dist/core/park-selfcheck.js +1 -0
- package/dist/core/runner/announce-once-ledger.d.ts +26 -0
- package/dist/core/runner/announce-once-ledger.js +37 -0
- package/dist/core/runner/contracts.d.ts +22 -5
- package/dist/core/runner/denial-limit-arms.d.ts +2 -2
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +0 -4
- package/dist/core/runner/prepare-acquire-reconcile.js +1 -3
- package/dist/core/runner/prepare-file-history.d.ts +13 -15
- package/dist/core/runner/prepare-file-history.js +57 -43
- package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
- package/dist/core/runner/prepare-listings.d.ts +3 -1
- package/dist/core/runner/prepare-listings.js +2 -2
- package/dist/core/runner/prepare-memory.js +16 -5
- package/dist/core/runner/prepare-park-ask.js +27 -16
- package/dist/core/runner/prepare-prompt-inputs.d.ts +8 -1
- package/dist/core/runner/prepare-prompt-inputs.js +5 -3
- package/dist/core/runner/prepare-task.js +7 -5
- package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -0
- package/dist/core/runner/prepare-wiring-manifest.js +2 -1
- package/dist/core/runner/run-attachment-seats.js +1 -1
- package/dist/core/runner/run-leg.js +61 -52
- package/dist/core/runner/run-settle-and-teardown.js +4 -0
- package/dist/core/runner/run-turn-boundary.js +6 -6
- package/dist/core/runner/turn-attachments.d.ts +6 -6
- package/dist/core/runner-deps.d.ts +3 -1
- package/dist/core/secret-env.d.ts +12 -1
- package/dist/core/secret-env.js +27 -2
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +22 -0
- package/dist/core/task-result.d.ts +1 -1
- package/dist/core/task-spec.d.ts +17 -8
- package/dist/core/tool-spec.d.ts +1 -1
- package/dist/core/wiring-manifest.d.ts +10 -0
- package/dist/core/wiring-manifest.js +7 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +6 -0
- package/dist/engine/execution-env/node-execution-env.js +15 -17
- package/dist/engine/harness/agent-harness.js +7 -5
- package/dist/engine/lsp/node-lsp-manager.d.ts +4 -0
- package/dist/engine/lsp/node-lsp-manager.js +10 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/fs/shell-read-plan.d.ts +2 -2
- package/dist/tools/fs/shell-read-plan.js +15 -10
- package/dist/tools/fs/shell-read-word.d.ts +5 -4
- package/dist/tools/fs/shell-read-word.js +3 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -1
- package/wire-changes/core@7.31.0.json +705 -0
- package/wire-changes/core@7.31.1.json +189 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { closeSync, constants as fsConstants, existsSync,
|
|
2
|
+
import { accessSync, closeSync, constants as fsConstants, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, relative, sep } from "node:path";
|
|
4
4
|
import { uuidv7 } from "../../internal/harness.js";
|
|
5
5
|
import { MAX_MEMORY_BYTES, composeMemoryBlock, firstSentence } from "../memory.js";
|
|
@@ -11,12 +11,13 @@ import { MEMORY_PROVENANCE_RECALL_SENTENCE, memoryExposureIndexRow, parseMemoryE
|
|
|
11
11
|
import { isInstructionEntry } from "./header-hints.js";
|
|
12
12
|
import { ConsolidationRefusedError, CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, CONSOLIDATION_SEAT_FREEZE_GRACE_MS, consolidationTypeEligible, deriveProductSlug, deriveSupersededSet, isValidConsolidationPlanId, listConsolidationPlanIds, productAddPatch, quarantineCorruptPlan, readConsolidationGateFile, readConsolidationPlan, readIntentCredentials, recordActiveSetBaseline, recordConsolidationSession, sessionsSinceExcludingCurrent, recordIntentCredentials, scanRefusalCeiling, screenConsolidationOptions, supersessionFuseCeiling, updateConsolidationGate, writeConsolidationPlan, } from "./consolidation.js";
|
|
13
13
|
import { deriveShadowedSet } from "./shadowing.js";
|
|
14
|
-
import { classifySessionSettlements, clearHoldResolution, closeSessionAccount, disposeHold, effectiveSettlements, expireOverdueSettlements, foreignDanglingSessionAccounts, markHoldReleased, openInstructionHold, openSessionAccount, readHoldCustody, readHolds, reconcileHolds, replayExternalSettlementEffects, resolveHoldRecord, resolveSessionAccountRecord, resolveSettlementRecord, sessionSettlements, sessionUnattributedSet, emptyUnattributedSet, convertV1SidecarKeys, } from "./delegation-settlement.js";
|
|
14
|
+
import { classifySessionSettlements, clearHoldResolution, closeSessionAccount, disposeHold, effectiveSettlements, expireOverdueSettlements, foreignDanglingSessionAccounts, openLayerWriters, terminateSessionAccount, sessionAccountLeg, markHoldReleased, openInstructionHold, openSessionAccount, readHoldCustody, readHolds, reconcileHolds, replayExternalSettlementEffects, resolveHoldRecord, resolveSessionAccountRecord, resolveSettlementRecord, sessionSettlements, sessionUnattributedSet, emptyUnattributedSet, convertV1SidecarKeys, } from "./delegation-settlement.js";
|
|
15
15
|
import { noReplaceRestore, retireStagingToCustody, stagingPathBeside } from "./delegation-settlement.js";
|
|
16
|
+
import { landedUndecided, layerDigest, layerObservationLookup, readOnlyLayerCandidates, readOnlyLayerCensus, recordLayerObservations, settleUndecidedLayerObservations, observationBacked, sameObservation, undecidedLayerObservations } from "./layer-observations.js";
|
|
16
17
|
import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
|
|
17
18
|
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
|
|
18
19
|
import { assembleMemoryExportBundle, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
|
|
19
|
-
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, restoreContainedBaseline, readIndexRevs, writeIndexRevs, bumpScanFuse, scanFuseKey, canonicalize, adoptRootScopeLayout, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, markSessionPolluted, readSessionPollution, fileSessionCaptureRecordStore, recordRetrievedAccount, writeFileNoFollow, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, readProjectionDebts, resolveChallengeEvent, settleProjectionDebts, stageLineagePending, stageProjectionDebts, } from "./layout.js";
|
|
20
|
+
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, restoreContainedBaseline, readIndexRevs, writeIndexRevs, bumpScanFuse, scanFuseKey, canonicalize, landingPathOf, storeTargetEscape, TARGET_OUTSIDE_STORE, adoptRootScopeLayout, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, markSessionPolluted, readSessionPollution, fileSessionCaptureRecordStore, recordRetrievedAccount, writeFileNoFollow, withPermissionRemedy, permissionRemedyOf, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, readProjectionDebts, resolveChallengeEvent, settleProjectionDebts, stageLineagePending, stageProjectionDebts, } from "./layout.js";
|
|
20
21
|
import { createSessionIncarnationSeat, recalledForAnyIdentity, recalledForIdentity, rememberForIdentity } from "./session-incarnation.js";
|
|
21
22
|
import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
|
|
22
23
|
import { writableRootOf } from "./types.js";
|
|
@@ -301,9 +302,15 @@ export function memoryConsolidationRefusedNotice(input) {
|
|
|
301
302
|
},
|
|
302
303
|
};
|
|
303
304
|
}
|
|
305
|
+
const INDEX_WRITE_REFUSED = "the derived memory index could NOT be written";
|
|
306
|
+
function unexpectedLayerFile(handle, c) {
|
|
307
|
+
return { path: relative(handle.memoryDir, c.path), code: "readonly_layer", reason: "a file appeared in a read-only inherited memory layer during this session — it is recorded as an unexpected write and never adopted into memory" };
|
|
308
|
+
}
|
|
309
|
+
const STALE_LEG_NOTE = "memory session account: this harvest belongs to an earlier leg of the session (a later leg re-opened the account while it ran) — its account close and leg end were not written over the later leg";
|
|
304
310
|
export class MemoryEngine {
|
|
305
311
|
backend;
|
|
306
312
|
memoryDir;
|
|
313
|
+
mountedRoot;
|
|
307
314
|
controlDir;
|
|
308
315
|
now;
|
|
309
316
|
perScopeBudgetBytes;
|
|
@@ -315,6 +322,7 @@ export class MemoryEngine {
|
|
|
315
322
|
holdSettleTimeoutMs;
|
|
316
323
|
provenance;
|
|
317
324
|
onIncident;
|
|
325
|
+
unsettleableDisclosed = false;
|
|
318
326
|
backendPinnedRoot;
|
|
319
327
|
constructor(opts) {
|
|
320
328
|
if (opts.provenance !== undefined && opts.provenance !== "off" && opts.provenance !== "carry") {
|
|
@@ -360,6 +368,18 @@ export class MemoryEngine {
|
|
|
360
368
|
return this.provenance;
|
|
361
369
|
}
|
|
362
370
|
consolidation;
|
|
371
|
+
endSessionLeg(handle, sessionId) {
|
|
372
|
+
return handle.accountLeg !== undefined ? this.endLeg(sessionId, handle.accountLeg) : undefined;
|
|
373
|
+
}
|
|
374
|
+
endLeg(sessionId, leg) {
|
|
375
|
+
try {
|
|
376
|
+
return terminateSessionAccount(this.controlDir, { sessionId, now: this.now, leg });
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
this.discloseSessionAccountIncident("the end of this session's leg could not be recorded — it keeps reading as a live writer of its layer (read-only restores there are withheld)", err);
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
363
383
|
discloseSessionAccountIncident(what, cause) {
|
|
364
384
|
const sink = this.onIncident;
|
|
365
385
|
if (sink === undefined)
|
|
@@ -372,6 +392,209 @@ export class MemoryEngine {
|
|
|
372
392
|
catch {
|
|
373
393
|
}
|
|
374
394
|
}
|
|
395
|
+
async detectReadOnlyLayerChanges(handle, report, admitsNothing) {
|
|
396
|
+
const candidates = [];
|
|
397
|
+
for (const m of handle.materialized.filter((x) => x.readonly)) {
|
|
398
|
+
const text = readSafe(m.path);
|
|
399
|
+
if (text === undefined) {
|
|
400
|
+
report.missing.push(m.relPath);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (revOfText(text, m.id) === m.rev)
|
|
404
|
+
continue;
|
|
405
|
+
if ((await this.committedContentUnadopted(m.id)) === text)
|
|
406
|
+
continue;
|
|
407
|
+
const stood = handle.layerBaseline?.get(m.path) === layerDigest(text);
|
|
408
|
+
const rejection = admitsNothing
|
|
409
|
+
? { path: m.relPath, code: "readonly_layer", reason: `a read-only inherited memory layer's file ${stood ? "differed from its committed content when this session started (not this session's write)" : "differs from what this session was served"}; this session admits nothing, so it was not restored — the bytes stay on disk for a session that may write to adjudicate` }
|
|
410
|
+
: { path: m.relPath, code: "readonly_layer", reason: stood ? "a read-only inherited memory layer's file differed from its committed content when this session started (not this session's write)" : "a read-only inherited memory layer was modified; changes to inherited layers are not writable from this session" };
|
|
411
|
+
report.rejections.push(rejection);
|
|
412
|
+
if (!admitsNothing)
|
|
413
|
+
candidates.push({ path: m.path, relPath: m.relPath, scope: m.scope, id: m.id, stood, rejection });
|
|
414
|
+
}
|
|
415
|
+
return candidates;
|
|
416
|
+
}
|
|
417
|
+
async committedContentUnadopted(id) {
|
|
418
|
+
const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
|
|
419
|
+
const view = zeroCopy ? undefined : this.backend.restrictedAdoptionView?.({ audit: false });
|
|
420
|
+
if (view === undefined)
|
|
421
|
+
return this.committedContentFor(id);
|
|
422
|
+
try {
|
|
423
|
+
const [entry] = await view.getByIds([id]);
|
|
424
|
+
if (entry !== undefined && entry.id === id)
|
|
425
|
+
return serializeEntryFile(entry);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
}
|
|
429
|
+
return this.backend.readCommittedShadow?.(id) ?? readSafe(join(this.controlDir, "shadow", `${id}.md`));
|
|
430
|
+
}
|
|
431
|
+
async restoreReadOnlyLayers(candidates, report, selfLeg, root) {
|
|
432
|
+
let writers;
|
|
433
|
+
for (const c of candidates) {
|
|
434
|
+
const text = readSafe(c.path);
|
|
435
|
+
const committed = await this.committedContentUnadopted(c.id);
|
|
436
|
+
if (text !== undefined && text === committed) {
|
|
437
|
+
report.rejections = report.rejections.filter((r) => r !== c.rejection);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (committed === undefined)
|
|
441
|
+
continue;
|
|
442
|
+
try {
|
|
443
|
+
writers ??= openLayerWriters(this.controlDir, selfLeg);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
writers = { scopes: new Set(), unscoped: 1 };
|
|
447
|
+
}
|
|
448
|
+
if (writers.unscoped > 0 || writers.scopes.has(c.scope)) {
|
|
449
|
+
c.rejection.reason = `a read-only inherited memory layer's file ${c.stood ? "differed from its committed content when this session started (not this session's write)" : "differs from what this session was served"}; a session that writes this layer is open (the bytes may be its uncommitted edit), so it was not restored`;
|
|
450
|
+
report.warnings.push(`read-only layer file ${quoteId(c.relPath)} was not restored: a session that writes this layer is open, and the bytes may be its uncommitted edit`);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
this.refuseOutsideStore(root, c.path, "restoring the read-only layer file");
|
|
455
|
+
writeFileNoFollow(c.path, committed);
|
|
456
|
+
report.restored.push(c.relPath);
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
report.warnings.push(`read-only layer file ${quoteId(c.relPath)} could NOT be restored to its committed content — the modified bytes are still on disk: ${this.writeFailureText(err)}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async observeReadOnlyLayers(handle, report, sessionId) {
|
|
464
|
+
const served = new Set(handle.materialized.filter((m) => m.readonly).map((m) => m.path));
|
|
465
|
+
const batch = { rows: [], rejections: [], undecided: [], landed: false };
|
|
466
|
+
for (const c of readOnlyLayerCandidates(this.memoryDir, this.controlDir, handle)) {
|
|
467
|
+
const by = { scope: c.scope, rel: c.rel, ...(sessionId !== undefined ? { sessionId } : {}), observedAt: this.now() };
|
|
468
|
+
if (c.id === undefined) {
|
|
469
|
+
batch.rows.push({ ...by, observedRev: c.digest });
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
const committed = await this.committedRowOf(c.id);
|
|
473
|
+
if (committed === "undecidable") {
|
|
474
|
+
const row = { ...by, entryId: c.id, observedRev: c.observedRev, undecided: true, observationId: uuidv7() };
|
|
475
|
+
batch.rows.push(row);
|
|
476
|
+
batch.undecided.push({ candidate: c, row });
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (committed !== undefined && committed.scope === c.scope && committed.slug === c.slug && committed.rev === c.observedRev)
|
|
480
|
+
continue;
|
|
481
|
+
batch.rows.push({ ...by, entryId: c.id, ...(committed !== undefined ? { rev: committed.rev } : {}), observedRev: c.observedRev });
|
|
482
|
+
}
|
|
483
|
+
if (!served.has(c.path)) {
|
|
484
|
+
batch.rejections.push(unexpectedLayerFile(handle, c));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
this.landObservations(batch, report);
|
|
488
|
+
return batch;
|
|
489
|
+
}
|
|
490
|
+
landObservations(batch, report) {
|
|
491
|
+
try {
|
|
492
|
+
recordLayerObservations(this.controlDir, batch.rows);
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
batch.failure = this.writeFailureText(err);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
batch.landed = true;
|
|
499
|
+
delete batch.failure;
|
|
500
|
+
report.rejections.push(...batch.rejections);
|
|
501
|
+
}
|
|
502
|
+
concludeObservations(batch, report) {
|
|
503
|
+
if (batch !== undefined && !batch.landed) {
|
|
504
|
+
report.warnings.push(`${batch.rows.length} unexpected write(s) into read-only memory layers could NOT be recorded — a later session's read may adopt them: ${batch.failure ?? "unknown failure"}`);
|
|
505
|
+
}
|
|
506
|
+
this.discloseUndecidedObservations(landedUndecided(batch).map((u) => u.candidate), report);
|
|
507
|
+
}
|
|
508
|
+
discloseUndecidedObservations(undecided, report) {
|
|
509
|
+
for (const c of undecided) {
|
|
510
|
+
report.warnings.push(`${quoteId(relative(this.memoryDir, c.path))}: changed in a read-only memory layer during this session, and whether a commit backs it could not be established (a memory transaction was pending, or the committed account could not be read) — recorded as undecided (not adopted), settled once a writing session can read the committed account`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async settleUndecidedObservations(handle, report, own) {
|
|
514
|
+
let rows;
|
|
515
|
+
try {
|
|
516
|
+
rows = undecidedLayerObservations(this.controlDir);
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
report.warnings.push(`undecided read-only-layer observations could not be read for settlement — they stay undecided (not adopted): ${this.writeFailureText(err)}`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const verdicts = new Map();
|
|
523
|
+
for (const row of rows) {
|
|
524
|
+
const committed = row.entryId !== undefined ? await this.committedRowOf(row.entryId) : "undecidable";
|
|
525
|
+
if (committed !== "undecidable")
|
|
526
|
+
verdicts.set(row, observationBacked(row, committed) ? "backed" : committed !== undefined ? { rev: committed.rev } : {});
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
settleUndecidedLayerObservations(this.controlDir, verdicts);
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
report.warnings.push(`undecided read-only-layer observations could not be settled — they stay undecided (not adopted): ${this.writeFailureText(err)}`);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const served = new Set(handle.materialized.filter((m) => m.readonly).map((m) => m.path));
|
|
536
|
+
const stillUndecided = [];
|
|
537
|
+
for (const { candidate: c, row: mine } of own) {
|
|
538
|
+
const row = rows.find((r) => sameObservation(r, mine));
|
|
539
|
+
if (row === undefined) {
|
|
540
|
+
const refused = served.has(c.path) ? false : await this.refusedNow(mine.entryId, c.observedRev);
|
|
541
|
+
if (refused === true)
|
|
542
|
+
report.rejections.push(unexpectedLayerFile(handle, c));
|
|
543
|
+
else if (refused === undefined)
|
|
544
|
+
report.warnings.push(`${quoteId(relative(this.memoryDir, c.path))}: changed in a read-only memory layer during this session; another writing session settled its record first, and whether memory refuses these bytes now could not be read`);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const verdict = verdicts.get(row);
|
|
548
|
+
if (verdict === undefined)
|
|
549
|
+
stillUndecided.push(c);
|
|
550
|
+
else if (verdict !== "backed" && !served.has(c.path))
|
|
551
|
+
report.rejections.push(unexpectedLayerFile(handle, c));
|
|
552
|
+
}
|
|
553
|
+
this.discloseUndecidedObservations(stillUndecided, report);
|
|
554
|
+
}
|
|
555
|
+
async refusedNow(entryId, observedRev) {
|
|
556
|
+
const committed = entryId !== undefined ? await this.committedRowOf(entryId) : "undecidable";
|
|
557
|
+
if (entryId === undefined || committed === "undecidable")
|
|
558
|
+
return undefined;
|
|
559
|
+
try {
|
|
560
|
+
return layerObservationLookup(this.controlDir).find(entryId, committed?.rev, observedRev) !== undefined;
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
discloseUnsettleableObservations() {
|
|
567
|
+
if (this.unsettleableDisclosed || typeof this.backend.committedSnapshotOf === "function")
|
|
568
|
+
return;
|
|
569
|
+
this.unsettleableDisclosed = true;
|
|
570
|
+
const sink = this.onIncident;
|
|
571
|
+
if (sink === undefined)
|
|
572
|
+
return;
|
|
573
|
+
try {
|
|
574
|
+
const e = new Error("memory read-only layers: this backend has no committed-snapshot face (committedSnapshotOf), so whether a commit backs a change observed in a read-only layer cannot be established — every such change is recorded as undecided (not adopted), and undecided observations cannot settle on this backend");
|
|
575
|
+
e.code = "memory.observation_settlement_unavailable";
|
|
576
|
+
sink(e);
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async committedRowOf(id) {
|
|
582
|
+
const face = this.backend.committedSnapshotOf;
|
|
583
|
+
if (face === undefined)
|
|
584
|
+
return "undecidable";
|
|
585
|
+
try {
|
|
586
|
+
const snap = await face.call(this.backend, id);
|
|
587
|
+
if (snap.state !== "row")
|
|
588
|
+
return undefined;
|
|
589
|
+
return { rev: snap.rev, ...(snap.binding.state === "bound" ? { scope: snap.binding.scope, slug: snap.binding.slug } : {}) };
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
return "undecidable";
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
writeFailureText(err) {
|
|
596
|
+
return thrownText(withPermissionRemedy(err, this.memoryDir));
|
|
597
|
+
}
|
|
375
598
|
discloseAnnounceFailure(stage, cause) {
|
|
376
599
|
const sink = this.onIncident;
|
|
377
600
|
if (sink === undefined)
|
|
@@ -534,6 +757,7 @@ export class MemoryEngine {
|
|
|
534
757
|
const indexPath = join(writableRoot, MEMORY_INDEX_FILENAME);
|
|
535
758
|
const baseline = handle.planeBaseline !== undefined ? handle.planeBaseline.index : handle.indexText;
|
|
536
759
|
try {
|
|
760
|
+
this.refuseOutsideStore(handle.memoryDir, indexPath, "sweeping the index");
|
|
537
761
|
const first = existsSync(indexPath) ? readSafe(indexPath) : undefined;
|
|
538
762
|
if (first !== undefined && first !== baseline) {
|
|
539
763
|
const onDisk = readSafe(indexPath);
|
|
@@ -557,7 +781,7 @@ export class MemoryEngine {
|
|
|
557
781
|
}
|
|
558
782
|
}
|
|
559
783
|
catch (err) {
|
|
560
|
-
out.failures.push({ path: MEMORY_INDEX_FILENAME, detail: `index sweep failed: ${
|
|
784
|
+
out.failures.push({ path: MEMORY_INDEX_FILENAME, detail: `index sweep failed: ${this.writeFailureText(err)}` });
|
|
561
785
|
}
|
|
562
786
|
}
|
|
563
787
|
const scanned = scanEntryFiles(writableRoot, {
|
|
@@ -726,6 +950,14 @@ export class MemoryEngine {
|
|
|
726
950
|
return this.challengeAndAnnounce(ids.map((entryId) => ({ eventId: `${requestId}:${entryId}`, entryId, reason })));
|
|
727
951
|
}
|
|
728
952
|
async eraseMemoryEntries(input) {
|
|
953
|
+
try {
|
|
954
|
+
return await this.eraseMemoryEntriesCore(input);
|
|
955
|
+
}
|
|
956
|
+
catch (err) {
|
|
957
|
+
throw withPermissionRemedy(err, this.memoryDir);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async eraseMemoryEntriesCore(input) {
|
|
729
961
|
const captured = captureErasureInput(input);
|
|
730
962
|
const bad = erasureRequestInvalid(captured);
|
|
731
963
|
if (bad !== undefined) {
|
|
@@ -859,6 +1091,7 @@ export class MemoryEngine {
|
|
|
859
1091
|
if (kept.length === lines.length)
|
|
860
1092
|
continue;
|
|
861
1093
|
try {
|
|
1094
|
+
this.refuseOutsideStore(landingPathOf(this.memoryDir), indexPath, "rewriting the index");
|
|
862
1095
|
writeFileNoFollow(indexPath, kept.join("\n"));
|
|
863
1096
|
}
|
|
864
1097
|
catch {
|
|
@@ -867,7 +1100,7 @@ export class MemoryEngine {
|
|
|
867
1100
|
}
|
|
868
1101
|
}
|
|
869
1102
|
catch (err) {
|
|
870
|
-
uncleared.push(`${join(this.memoryDir, "<scope homes>", MEMORY_INDEX_FILENAME)} (index sweep failed: ${
|
|
1103
|
+
uncleared.push(`${join(this.memoryDir, "<scope homes>", MEMORY_INDEX_FILENAME)} (index sweep failed: ${this.writeFailureText(err)})`);
|
|
871
1104
|
}
|
|
872
1105
|
if (uncleared.length > 0) {
|
|
873
1106
|
try {
|
|
@@ -1168,12 +1401,30 @@ export class MemoryEngine {
|
|
|
1168
1401
|
adoptRootScopeLayout(this.memoryDir, this.controlDir, { ownerCandidate: writeScope, onIncident: this.onIncident, convertSidecars: convertV1SidecarKeys });
|
|
1169
1402
|
}
|
|
1170
1403
|
async materialize(scopes, writeScope, opts) {
|
|
1404
|
+
const legSeat = {};
|
|
1405
|
+
try {
|
|
1406
|
+
return await this.materializeCore(scopes, writeScope, opts, legSeat);
|
|
1407
|
+
}
|
|
1408
|
+
catch (err) {
|
|
1409
|
+
if (legSeat.leg !== undefined && opts?.sessionId !== undefined)
|
|
1410
|
+
this.endLeg(opts.sessionId, legSeat.leg);
|
|
1411
|
+
throw withPermissionRemedy(err, this.memoryDir);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
async materializeCore(scopes, writeScope, opts, legSeat) {
|
|
1171
1415
|
ensureDirExists(this.memoryDir);
|
|
1416
|
+
const physicalRoot = landingPathOf(this.memoryDir);
|
|
1417
|
+
this.mountedRoot ??= physicalRoot;
|
|
1418
|
+
if (physicalRoot !== this.mountedRoot) {
|
|
1419
|
+
const e = new Error(`${TARGET_OUTSIDE_STORE}: the memory dir ${this.memoryDir} now resolves to ${physicalRoot}, not ${this.mountedRoot} where this engine first mounted it — refused; open a new engine on the directory you mean`);
|
|
1420
|
+
e.code = TARGET_OUTSIDE_STORE;
|
|
1421
|
+
throw e;
|
|
1422
|
+
}
|
|
1172
1423
|
ensureDirExists(this.controlDir);
|
|
1173
1424
|
await this.adoptLayoutBeforeMount(writeScope);
|
|
1174
|
-
chmodNoFollow(this.memoryDir, 0o755);
|
|
1175
1425
|
const restricted = opts?.adoptionRestricted === true;
|
|
1176
1426
|
let sessionAccountFailed = false;
|
|
1427
|
+
let accountLeg;
|
|
1177
1428
|
const readBackend = restricted
|
|
1178
1429
|
? (this.backend.restrictedAdoptionView?.({ audit: true, writeScope }) ?? this.backend)
|
|
1179
1430
|
: this.backend;
|
|
@@ -1182,9 +1433,13 @@ export class MemoryEngine {
|
|
|
1182
1433
|
scopeDirs.set(scope, registerScope(this.memoryDir, this.controlDir, scope));
|
|
1183
1434
|
if (writeScope !== null && !scopeDirs.has(writeScope))
|
|
1184
1435
|
scopeDirs.set(writeScope, registerScope(this.memoryDir, this.controlDir, writeScope));
|
|
1185
|
-
const sweepAnnounced = new Set();
|
|
1186
1436
|
for (const dir of scopeDirs.values())
|
|
1187
|
-
this.
|
|
1437
|
+
this.censusScopeTree(dir);
|
|
1438
|
+
const layerBaseline = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir)
|
|
1439
|
+
? readOnlyLayerCensus([...scopeDirs].filter(([scope]) => scope !== writeScope).map(([, dir]) => dir))
|
|
1440
|
+
: undefined;
|
|
1441
|
+
if (layerBaseline !== undefined)
|
|
1442
|
+
this.discloseUnsettleableObservations();
|
|
1188
1443
|
if (this.provenance === "carry" && opts?.sessionId !== undefined && writeScope !== null && !restricted) {
|
|
1189
1444
|
let accountFailed = false;
|
|
1190
1445
|
try {
|
|
@@ -1300,10 +1555,19 @@ export class MemoryEngine {
|
|
|
1300
1555
|
}
|
|
1301
1556
|
}
|
|
1302
1557
|
}
|
|
1303
|
-
|
|
1558
|
+
accountLeg = legSeat.leg = uuidv7();
|
|
1559
|
+
openSessionAccount(this.controlDir, { sessionId: opts.sessionId, now: this.now, unattributed, writeScope, leg: accountLeg });
|
|
1304
1560
|
}
|
|
1305
1561
|
catch (err) {
|
|
1306
1562
|
accountFailed = true;
|
|
1563
|
+
if (accountLeg !== undefined) {
|
|
1564
|
+
try {
|
|
1565
|
+
if (sessionAccountLeg(this.controlDir, opts.sessionId) !== accountLeg)
|
|
1566
|
+
accountLeg = legSeat.leg = undefined;
|
|
1567
|
+
}
|
|
1568
|
+
catch {
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1307
1571
|
this.discloseSessionAccountIncident("crash-residue attribution is degraded for this session (the harvest boundary refuses fail-closed while this stands)", err);
|
|
1308
1572
|
}
|
|
1309
1573
|
if (accountFailed)
|
|
@@ -1334,10 +1598,13 @@ export class MemoryEngine {
|
|
|
1334
1598
|
...(opts?.shadowNames !== undefined ? { shadowNames: opts.shadowNames } : {}),
|
|
1335
1599
|
...(restricted ? { adoptionRestricted: true } : {}),
|
|
1336
1600
|
...(sessionAccountFailed ? { sessionAccountFailed: true } : {}),
|
|
1601
|
+
...(layerBaseline !== undefined ? { layerBaseline } : {}),
|
|
1602
|
+
...(accountLeg !== undefined ? { accountLeg } : {}),
|
|
1337
1603
|
};
|
|
1338
1604
|
if (handle.writableRoot !== null)
|
|
1339
1605
|
ensureDirExists(handle.writableRoot);
|
|
1340
1606
|
const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
|
|
1607
|
+
const projectionRoot = zeroCopy ? undefined : this.mountedRoot;
|
|
1341
1608
|
for (const [scope, dir] of scopeDirs) {
|
|
1342
1609
|
const scopeEntries = (byScope.get(scope) ?? []).slice();
|
|
1343
1610
|
scopeEntries.sort((a, b) => (mtimeById.get(b.id) ?? 0) - (mtimeById.get(a.id) ?? 0));
|
|
@@ -1359,8 +1626,8 @@ export class MemoryEngine {
|
|
|
1359
1626
|
const { deleted: _stubDrop, ...stubFm } = entry.frontmatter;
|
|
1360
1627
|
const text = stub ? serializeEntryFile({ id: entry.id, frontmatter: stubFm, body: STUB_ARCHIVED_LINE }) : fullText;
|
|
1361
1628
|
const target = join(dir, `${entry.slug}.md`);
|
|
1362
|
-
if (
|
|
1363
|
-
this.writeIfChanged(target, text);
|
|
1629
|
+
if (projectionRoot !== undefined)
|
|
1630
|
+
this.writeIfChanged(target, text, projectionRoot);
|
|
1364
1631
|
const onDiskRev = stub ? revOfText(text, entry.id) : entry.rev;
|
|
1365
1632
|
const path = canonicalize(target);
|
|
1366
1633
|
handle.materialized.push({
|
|
@@ -1444,6 +1711,15 @@ export class MemoryEngine {
|
|
|
1444
1711
|
catch {
|
|
1445
1712
|
}
|
|
1446
1713
|
}
|
|
1714
|
+
const indexRefusals = materializeHealWarnings.filter((w) => w.startsWith(INDEX_WRITE_REFUSED));
|
|
1715
|
+
if (indexRefusals.length > 0) {
|
|
1716
|
+
try {
|
|
1717
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at: this.now(), items: capItems(indexRefusals.map((w) => inlineUntrusted(w, 500))) });
|
|
1718
|
+
}
|
|
1719
|
+
catch (err) {
|
|
1720
|
+
this.discloseAnnounceFailure("materialize index-write enqueue", err);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1447
1723
|
if (indexWrite && handle.indexOnDiskUntrusted !== true) {
|
|
1448
1724
|
const dropNotes = materializeHealWarnings.filter((w) => w.includes("opaque external-origin handle"));
|
|
1449
1725
|
if (dropNotes.length > 0) {
|
|
@@ -1455,16 +1731,20 @@ export class MemoryEngine {
|
|
|
1455
1731
|
}
|
|
1456
1732
|
}
|
|
1457
1733
|
}
|
|
1458
|
-
for (const [scope, dir] of scopeDirs) {
|
|
1459
|
-
if (scope === writeScope)
|
|
1460
|
-
continue;
|
|
1461
|
-
this.chmodScopeTree(dir, 0o555, 0o444, {
|
|
1462
|
-
announced: sweepAnnounced,
|
|
1463
|
-
});
|
|
1464
|
-
}
|
|
1465
1734
|
if (handle.writableRoot !== null) {
|
|
1466
1735
|
const writableRoot = handle.writableRoot;
|
|
1467
1736
|
const files = new Map();
|
|
1737
|
+
const refused = new Map();
|
|
1738
|
+
const probeWritable = (p) => {
|
|
1739
|
+
try {
|
|
1740
|
+
accessSync(p, fsConstants.W_OK);
|
|
1741
|
+
}
|
|
1742
|
+
catch (err) {
|
|
1743
|
+
if (err.code !== "ENOENT" && !refused.has(p))
|
|
1744
|
+
refused.set(p, err);
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
probeWritable(writableRoot);
|
|
1468
1748
|
for (const f of scanEntryFiles(writableRoot, { maxDepth: this.maxDepth })) {
|
|
1469
1749
|
const canonical = canonicalize(f.path);
|
|
1470
1750
|
if (canonical !== writableRoot && !canonical.startsWith(`${writableRoot}${sep}`))
|
|
@@ -1472,6 +1752,19 @@ export class MemoryEngine {
|
|
|
1472
1752
|
const text = readSafe(canonical);
|
|
1473
1753
|
if (text !== undefined)
|
|
1474
1754
|
files.set(canonical, planeDigest(text));
|
|
1755
|
+
probeWritable(dirname(canonical));
|
|
1756
|
+
probeWritable(canonical);
|
|
1757
|
+
}
|
|
1758
|
+
const [firstRefused] = refused;
|
|
1759
|
+
if (firstRefused !== undefined) {
|
|
1760
|
+
const [path, err] = firstRefused;
|
|
1761
|
+
const remedy = permissionRemedyOf(Object.assign(new Error("write refused"), { code: err?.code, path }), this.memoryDir);
|
|
1762
|
+
try {
|
|
1763
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at: this.now(), items: [inlineUntrusted(`this session's memory write home cannot be written at ${refused.size} path(s) (${path}${refused.size > 1 ? ", …" : ""}): ${remedy ?? thrownText(err)}`, 700)] });
|
|
1764
|
+
}
|
|
1765
|
+
catch (err) {
|
|
1766
|
+
this.discloseAnnounceFailure("write-home permission enqueue", err);
|
|
1767
|
+
}
|
|
1475
1768
|
}
|
|
1476
1769
|
const indexOnDisk = readSafe(join(writableRoot, MEMORY_INDEX_FILENAME));
|
|
1477
1770
|
handle.planeBaseline = { files, ...(indexOnDisk !== undefined ? { index: indexOnDisk } : {}) };
|
|
@@ -1524,14 +1817,17 @@ export class MemoryEngine {
|
|
|
1524
1817
|
};
|
|
1525
1818
|
}
|
|
1526
1819
|
gateWrite(handle, canonicalPath, content) {
|
|
1527
|
-
const
|
|
1528
|
-
if (
|
|
1820
|
+
const inside = (root) => canonicalPath === root || canonicalPath.startsWith(`${root}${sep}`);
|
|
1821
|
+
if (!inside(handle.memoryDir))
|
|
1529
1822
|
return { ok: true };
|
|
1530
|
-
|
|
1823
|
+
const root = handle.writableRoot;
|
|
1824
|
+
if (handle.writeScope === null || root === null || !inside(root)) {
|
|
1531
1825
|
return {
|
|
1532
1826
|
ok: false,
|
|
1533
1827
|
code: "read_only_layering",
|
|
1534
|
-
reason:
|
|
1828
|
+
reason: handle.writeScope === null || root === null
|
|
1829
|
+
? "this session's memory is read-only (no write scope) — the engine does not accept writes into the memory domain. Nothing was written."
|
|
1830
|
+
: `this path is in a read-only inherited memory layer — this session writes memory only under ${root}. Nothing was written.`,
|
|
1535
1831
|
muted: false,
|
|
1536
1832
|
};
|
|
1537
1833
|
}
|
|
@@ -1555,7 +1851,18 @@ export class MemoryEngine {
|
|
|
1555
1851
|
return { ok: false, code: f.code, reason, muted };
|
|
1556
1852
|
}
|
|
1557
1853
|
async harvest(handle, opts) {
|
|
1558
|
-
const
|
|
1854
|
+
const out = {};
|
|
1855
|
+
try {
|
|
1856
|
+
out.report = await this.harvestCore(handle, opts);
|
|
1857
|
+
}
|
|
1858
|
+
catch (err) {
|
|
1859
|
+
throw withPermissionRemedy(err, this.memoryDir);
|
|
1860
|
+
}
|
|
1861
|
+
finally {
|
|
1862
|
+
if (opts?.phase === "terminal" && opts.sessionId !== undefined && this.endSessionLeg(handle, opts.sessionId) === "stale" && out.report !== undefined && !out.report.warnings.includes(STALE_LEG_NOTE))
|
|
1863
|
+
out.report.warnings.push(STALE_LEG_NOTE);
|
|
1864
|
+
}
|
|
1865
|
+
const report = out.report;
|
|
1559
1866
|
try {
|
|
1560
1867
|
const named = report.reportId !== undefined ? { reportId: report.reportId } : {};
|
|
1561
1868
|
const gateItems = gateAnnouncementItems(report);
|
|
@@ -1606,7 +1913,16 @@ export class MemoryEngine {
|
|
|
1606
1913
|
const writeScope = handle.writeScope;
|
|
1607
1914
|
const captureRead = opts?.sessionId !== undefined ? await this.sessionCaptureOptOutOrFault(opts.sessionId) : { fault: false };
|
|
1608
1915
|
const captureOptOut = captureRead.record;
|
|
1609
|
-
|
|
1916
|
+
const admitsNothing = writeScope === null || opts?.admitNothing !== undefined || captureOptOut !== undefined || captureRead.fault;
|
|
1917
|
+
const readOnlyRestorable = await this.detectReadOnlyLayerChanges(handle, report, admitsNothing);
|
|
1918
|
+
let observed;
|
|
1919
|
+
try {
|
|
1920
|
+
observed = await this.observeReadOnlyLayers(handle, report, opts?.sessionId);
|
|
1921
|
+
}
|
|
1922
|
+
catch (err) {
|
|
1923
|
+
report.warnings.push(`read-only memory layers could not be examined for unexpected writes (nothing was recorded): ${this.writeFailureText(err)}`);
|
|
1924
|
+
}
|
|
1925
|
+
if (admitsNothing) {
|
|
1610
1926
|
try {
|
|
1611
1927
|
this.backend.auditRestrictedDivergence?.([...new Set([...handle.scopes, ...(writeScope !== null ? [writeScope] : [])])], (handle.adoptionRestricted === true || captureOptOut !== undefined) && writeScope !== null ? { idlessWriteScope: writeScope } : undefined);
|
|
1612
1928
|
}
|
|
@@ -1627,9 +1943,10 @@ export class MemoryEngine {
|
|
|
1627
1943
|
}
|
|
1628
1944
|
}
|
|
1629
1945
|
catch (err) {
|
|
1630
|
-
report.warnings.push(`capture opt-out boundary sweep failed: ${
|
|
1946
|
+
report.warnings.push(`capture opt-out boundary sweep failed: ${this.writeFailureText(err)} — residue on the writable memory root may await a later session's adoption`);
|
|
1631
1947
|
}
|
|
1632
1948
|
}
|
|
1949
|
+
this.concludeObservations(observed, report);
|
|
1633
1950
|
if (opts?.admitNothing !== undefined)
|
|
1634
1951
|
report.warnings.push(opts.admitNothing.reason);
|
|
1635
1952
|
else if (captureOptOut !== undefined) {
|
|
@@ -1643,6 +1960,7 @@ export class MemoryEngine {
|
|
|
1643
1960
|
if (carry && handle.sessionAccountFailed === true) {
|
|
1644
1961
|
report.ok = false;
|
|
1645
1962
|
report.incident = { kind: "sidecar_corrupt", detail: "memory session account could not be opened at materialize — the crash-residue classification for this session never persisted, so the harvest refuses fail-closed (repair the control plane and re-materialize)" };
|
|
1963
|
+
this.concludeObservations(observed, report);
|
|
1646
1964
|
return report;
|
|
1647
1965
|
}
|
|
1648
1966
|
try {
|
|
@@ -1650,9 +1968,15 @@ export class MemoryEngine {
|
|
|
1650
1968
|
}
|
|
1651
1969
|
catch (err) {
|
|
1652
1970
|
report.ok = false;
|
|
1653
|
-
report.incident = { kind: "sidecar_corrupt", detail: `memory control plane refused: ${
|
|
1971
|
+
report.incident = { kind: "sidecar_corrupt", detail: `memory control plane refused: ${this.writeFailureText(err)}` };
|
|
1972
|
+
this.concludeObservations(observed, report);
|
|
1654
1973
|
return report;
|
|
1655
1974
|
}
|
|
1975
|
+
if (observed !== undefined && !observed.landed)
|
|
1976
|
+
this.landObservations(observed, report);
|
|
1977
|
+
if (observed !== undefined && !observed.landed)
|
|
1978
|
+
this.concludeObservations(observed, report);
|
|
1979
|
+
await this.settleUndecidedObservations(handle, report, landedUndecided(observed));
|
|
1656
1980
|
try {
|
|
1657
1981
|
if (carry) {
|
|
1658
1982
|
reconcileHolds(this.controlDir, { homeOf: (scope) => scopeDirFor(this.memoryDir, this.controlDir, scope), now: this.now });
|
|
@@ -1834,7 +2158,7 @@ export class MemoryEngine {
|
|
|
1834
2158
|
return true;
|
|
1835
2159
|
}
|
|
1836
2160
|
catch (err) {
|
|
1837
|
-
report.warnings.push(`memory index could NOT be restored to its pre-session state: ${
|
|
2161
|
+
report.warnings.push(`memory index could NOT be restored to its pre-session state: ${this.writeFailureText(err)}`);
|
|
1838
2162
|
return false;
|
|
1839
2163
|
}
|
|
1840
2164
|
};
|
|
@@ -1864,39 +2188,7 @@ export class MemoryEngine {
|
|
|
1864
2188
|
signalContainment((c) => (c.indexRolledBack = true));
|
|
1865
2189
|
report.warnings.push(`memory harvest refused by an incident while this session is marked externally exposed — no entries were committed; instruction-form files were withheld and the derived index rolled back: ${inlineUntrusted(pollutedReason, 200)}`);
|
|
1866
2190
|
};
|
|
1867
|
-
|
|
1868
|
-
const text = readSafe(m.path);
|
|
1869
|
-
if (text === undefined) {
|
|
1870
|
-
report.missing.push(m.relPath);
|
|
1871
|
-
continue;
|
|
1872
|
-
}
|
|
1873
|
-
if (revOfText(text, m.id) !== m.rev) {
|
|
1874
|
-
report.rejections.push({ path: m.relPath, code: "readonly_layer", reason: "a read-only inherited memory layer was modified; changes to inherited layers are not writable from this session" });
|
|
1875
|
-
const committed = await this.committedContentFor(m.id);
|
|
1876
|
-
if (committed !== undefined) {
|
|
1877
|
-
let fd;
|
|
1878
|
-
try {
|
|
1879
|
-
fd = openNoFollowForMode(m.path);
|
|
1880
|
-
if (fd !== undefined)
|
|
1881
|
-
fchmodSync(fd, 0o644);
|
|
1882
|
-
writeFileNoFollow(m.path, committed);
|
|
1883
|
-
report.restored.push(m.relPath);
|
|
1884
|
-
}
|
|
1885
|
-
catch {
|
|
1886
|
-
}
|
|
1887
|
-
finally {
|
|
1888
|
-
if (fd !== undefined) {
|
|
1889
|
-
try {
|
|
1890
|
-
fchmodSync(fd, 0o444);
|
|
1891
|
-
}
|
|
1892
|
-
catch {
|
|
1893
|
-
}
|
|
1894
|
-
closeSync(fd);
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
2191
|
+
await this.restoreReadOnlyLayers(readOnlyRestorable, report, handle.accountLeg, handle.memoryDir);
|
|
1900
2192
|
const missingWritable = writable.filter((m) => !recordByPath.has(m.path) && !existsSync(m.path) && !presentIds.has(m.id));
|
|
1901
2193
|
report.missing.push(...missingWritable.map((m) => m.relPath));
|
|
1902
2194
|
if (writable.length > 0 && missingWritable.length / writable.length > MASS_DELETION_FUSE_RATIO) {
|
|
@@ -1923,11 +2215,13 @@ export class MemoryEngine {
|
|
|
1923
2215
|
if (content === undefined)
|
|
1924
2216
|
continue;
|
|
1925
2217
|
try {
|
|
2218
|
+
this.refuseOutsideStore(handle.memoryDir, m.path, "restoring");
|
|
1926
2219
|
mkdirSync(dirname(m.path), { recursive: true });
|
|
1927
2220
|
writeFileNoFollow(m.path, content);
|
|
1928
2221
|
report.restored.push(m.relPath);
|
|
1929
2222
|
}
|
|
1930
|
-
catch {
|
|
2223
|
+
catch (err) {
|
|
2224
|
+
report.warnings.push(`${m.relPath}: the committed copy of this missing memory file could NOT be restored: ${this.writeFailureText(err)}`);
|
|
1931
2225
|
}
|
|
1932
2226
|
}
|
|
1933
2227
|
const baselinePaths = new Set(writable.map((m) => m.path));
|
|
@@ -1964,6 +2258,8 @@ export class MemoryEngine {
|
|
|
1964
2258
|
const pendingProjections = [];
|
|
1965
2259
|
const idsAddedThisHarvest = new Set();
|
|
1966
2260
|
const modelIdAdds = new Set();
|
|
2261
|
+
const stoodAsFound = new Set();
|
|
2262
|
+
let observedTable;
|
|
1967
2263
|
let processed = 0;
|
|
1968
2264
|
for (let i = 0; i < kept.length; i++) {
|
|
1969
2265
|
const f = kept[i];
|
|
@@ -1980,10 +2276,14 @@ export class MemoryEngine {
|
|
|
1980
2276
|
continue;
|
|
1981
2277
|
}
|
|
1982
2278
|
const fastBaseId = idByBasePath.get(f.canonical);
|
|
2279
|
+
if (fastBaseId !== undefined && !stubByPath.has(f.canonical) && revOfText(f.text, fastBaseId) !== revByBasePath.get(f.canonical) && handle.planeBaseline?.files.get(f.canonical) === planeDigest(f.text)) {
|
|
2280
|
+
stoodAsFound.add(rel);
|
|
2281
|
+
continue;
|
|
2282
|
+
}
|
|
1983
2283
|
if (fastBaseId !== undefined && !stubByPath.has(f.canonical) && revOfText(f.text, fastBaseId) === revByBasePath.get(f.canonical)) {
|
|
1984
2284
|
const fastDebt = projectionDebts.get(debtSeat(writeScope, homeRel(writableRoot, f.canonical)));
|
|
1985
2285
|
if (fastDebt !== undefined && fastDebt.entryId === fastBaseId)
|
|
1986
|
-
await this.repairProjectionSeat(seatOf(f.canonical), f.canonical, fastBaseId, debtClears);
|
|
2286
|
+
await this.repairProjectionSeat(seatOf(f.canonical), f.canonical, fastBaseId, debtClears, report.warnings);
|
|
1987
2287
|
continue;
|
|
1988
2288
|
}
|
|
1989
2289
|
processed++;
|
|
@@ -2043,6 +2343,21 @@ export class MemoryEngine {
|
|
|
2043
2343
|
const parsed = f.parsed;
|
|
2044
2344
|
let baseId = idByBasePath.get(f.canonical);
|
|
2045
2345
|
let id = baseId ?? parsed.id;
|
|
2346
|
+
if (baseId === undefined && handle.planeBaseline?.files.get(f.canonical) === planeDigest(f.text)) {
|
|
2347
|
+
const observed = (observedTable ??= layerObservationLookup(this.controlDir));
|
|
2348
|
+
const committedRow = parsed.id !== undefined && observed.covers(parsed.id) ? await this.committedRowOf(parsed.id) : undefined;
|
|
2349
|
+
if (committedRow === "undecidable") {
|
|
2350
|
+
report.rejections.push({ path: rel, code: "deferred", reason: "memory write deferred: this file's id is named by the read-only-layer observation table and the committed account could not be read whole — retried next harvest" });
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
2353
|
+
const recorded = parsed.id === undefined
|
|
2354
|
+
? observed.findNote(writeScope, homeRel(writableRoot, f.canonical), planeDigest(f.text)) !== undefined
|
|
2355
|
+
: observed.covers(parsed.id) && observed.find(parsed.id, committedRow?.rev, revOfText(f.text, parsed.id)) !== undefined;
|
|
2356
|
+
if (recorded) {
|
|
2357
|
+
report.rejections.push({ path: rel, code: "readonly_layer", reason: "this file appeared while a session held this memory layer read-only (recorded as an unexpected write) — it is not committed as this session's; remove it, or edit it to commit this session's own version" });
|
|
2358
|
+
continue;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2046
2361
|
let debtReconciled = false;
|
|
2047
2362
|
if (id === undefined && projectionDebts.size > 0) {
|
|
2048
2363
|
const debt = projectionDebts.get(debtSeat(writeScope, homeRel(writableRoot, f.canonical)));
|
|
@@ -2129,7 +2444,7 @@ export class MemoryEngine {
|
|
|
2129
2444
|
report.restored.push(rel);
|
|
2130
2445
|
}
|
|
2131
2446
|
catch (err) {
|
|
2132
|
-
report.warnings.push(`${rel}: the committed content could not be restored after the hold captured this session's edit — the entry is unavailable on the plane until release or the next restore: ${
|
|
2447
|
+
report.warnings.push(`${rel}: the committed content could not be restored after the hold captured this session's edit — the entry is unavailable on the plane until release or the next restore: ${this.writeFailureText(err)}`);
|
|
2133
2448
|
}
|
|
2134
2449
|
}
|
|
2135
2450
|
}
|
|
@@ -2176,7 +2491,7 @@ export class MemoryEngine {
|
|
|
2176
2491
|
if (revOfText(f.text, id) === revByBasePath.get(f.canonical)) {
|
|
2177
2492
|
const seatDebt = projectionDebts.get(debtSeat(writeScope, homeRel(writableRoot, f.canonical)));
|
|
2178
2493
|
if (seatDebt !== undefined && seatDebt.entryId === id)
|
|
2179
|
-
await this.repairProjectionSeat(seatOf(f.canonical), f.canonical, id, debtClears);
|
|
2494
|
+
await this.repairProjectionSeat(seatOf(f.canonical), f.canonical, id, debtClears, report.warnings);
|
|
2180
2495
|
continue;
|
|
2181
2496
|
}
|
|
2182
2497
|
pendingProjections.push({ path: f.canonical, entry, needed: minted || parsed.id !== id || needsCompletion(parsed, fm), ...(debtReconciled ? { idCompletion: true } : {}) });
|
|
@@ -2200,7 +2515,7 @@ export class MemoryEngine {
|
|
|
2200
2515
|
report.rejections.push({
|
|
2201
2516
|
path: rel,
|
|
2202
2517
|
code: "filename",
|
|
2203
|
-
reason: `memory write blocked: this file carries the entry id of another memory file (a copy is not a rename) and its frontmatter could not be rewritten (${
|
|
2518
|
+
reason: `memory write blocked: this file carries the entry id of another memory file (a copy is not a rename) and its frontmatter could not be rewritten (${this.writeFailureText(err)}) — remove the "id:" line so it can be committed as a new entry`,
|
|
2204
2519
|
});
|
|
2205
2520
|
continue;
|
|
2206
2521
|
}
|
|
@@ -2219,7 +2534,7 @@ export class MemoryEngine {
|
|
|
2219
2534
|
report.rejections.push({
|
|
2220
2535
|
path: rel,
|
|
2221
2536
|
code: "filename",
|
|
2222
|
-
reason: `memory write blocked: an external-origin commit does not adopt the file's model-written entry id and its frontmatter could not be rewritten (${
|
|
2537
|
+
reason: `memory write blocked: an external-origin commit does not adopt the file's model-written entry id and its frontmatter could not be rewritten (${this.writeFailureText(err)}) — remove the "id:" line so it can be committed under an engine-minted id`,
|
|
2223
2538
|
});
|
|
2224
2539
|
continue;
|
|
2225
2540
|
}
|
|
@@ -2341,7 +2656,7 @@ export class MemoryEngine {
|
|
|
2341
2656
|
report.rejections.push({
|
|
2342
2657
|
path: flipRec.rel,
|
|
2343
2658
|
code: "filename",
|
|
2344
|
-
reason: `memory write blocked: an external-origin commit does not adopt the file's model-written entry id and its frontmatter could not be rewritten (${
|
|
2659
|
+
reason: `memory write blocked: an external-origin commit does not adopt the file's model-written entry id and its frontmatter could not be rewritten (${this.writeFailureText(err)}) — remove the "id:" line so it can be committed under an engine-minted id`,
|
|
2345
2660
|
});
|
|
2346
2661
|
droppedIds.add(p.id);
|
|
2347
2662
|
continue;
|
|
@@ -2458,7 +2773,7 @@ export class MemoryEngine {
|
|
|
2458
2773
|
}
|
|
2459
2774
|
}
|
|
2460
2775
|
report.ok = false;
|
|
2461
|
-
report.incident = { kind: "sidecar_corrupt", detail: `memory commit refused: ${
|
|
2776
|
+
report.incident = { kind: "sidecar_corrupt", detail: `memory commit refused: ${this.writeFailureText(err)}` };
|
|
2462
2777
|
return report;
|
|
2463
2778
|
}
|
|
2464
2779
|
if (lineageArmed) {
|
|
@@ -2514,18 +2829,25 @@ export class MemoryEngine {
|
|
|
2514
2829
|
}
|
|
2515
2830
|
}
|
|
2516
2831
|
const appliedIds = new Set(patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id));
|
|
2832
|
+
const planeAfterFailedWriteBack = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir)
|
|
2833
|
+
? "this plane is the store itself: the next read adopts them as an out-of-session edit unless the committed copy is put back first"
|
|
2834
|
+
: "the next materialize re-projects the committed copy over them";
|
|
2517
2835
|
for (const p of pendingProjections) {
|
|
2518
2836
|
const pRel = relative(handle.memoryDir, p.path);
|
|
2519
2837
|
const pSeat = seatOf(p.path);
|
|
2520
2838
|
if (appliedIds.has(p.entry.id)) {
|
|
2521
|
-
const
|
|
2522
|
-
if (p.idCompletion
|
|
2523
|
-
if (
|
|
2839
|
+
const writeBackFailure = this.writeBackProjection(p.path, p.entry, p.needed);
|
|
2840
|
+
if (p.idCompletion !== true) {
|
|
2841
|
+
if (writeBackFailure !== undefined)
|
|
2842
|
+
report.warnings.push(`${pRel}: the entry committed but its projection write-back FAILED — the plane keeps this session's bytes (${planeAfterFailedWriteBack}): ${writeBackFailure}`);
|
|
2843
|
+
}
|
|
2844
|
+
else {
|
|
2845
|
+
if (writeBackFailure === undefined)
|
|
2524
2846
|
debtClears.push({ ...pSeat, entryId: p.entry.id });
|
|
2525
2847
|
else {
|
|
2526
2848
|
const landed = patchReport.applied.find((a) => a.id === p.entry.id)?.slug;
|
|
2527
2849
|
const suffixNote = landed !== undefined && landed !== p.entry.slug ? ` NOTE: the entry landed at slug ${JSON.stringify(landed)} (collision suffix) — the seat claim will not re-bind; host reconciliation may be needed.` : "";
|
|
2528
|
-
report.warnings.push(`${pRel}: the entry committed but its id write-back FAILED — the projection-debt row stands, so the file re-binds to its committed id at the next harvest (and the next materialize repairs the plane); it is never re-admitted as a duplicate.${suffixNote}`);
|
|
2850
|
+
report.warnings.push(`${pRel}: the entry committed but its id write-back FAILED — the projection-debt row stands, so the file re-binds to its committed id at the next harvest (and the next materialize repairs the plane); it is never re-admitted as a duplicate.${suffixNote} Cause: ${writeBackFailure}`);
|
|
2529
2851
|
}
|
|
2530
2852
|
}
|
|
2531
2853
|
continue;
|
|
@@ -2538,7 +2860,8 @@ export class MemoryEngine {
|
|
|
2538
2860
|
writeFileNoFollow(p.path, committed);
|
|
2539
2861
|
planeHoldsCommitted = true;
|
|
2540
2862
|
}
|
|
2541
|
-
catch {
|
|
2863
|
+
catch (err) {
|
|
2864
|
+
report.warnings.push(`${pRel}: this session's version lost to a concurrent commit and the committed copy could NOT be written back over it — the plane keeps this session's bytes (${planeAfterFailedWriteBack}): ${this.writeFailureText(err)}`);
|
|
2542
2865
|
}
|
|
2543
2866
|
}
|
|
2544
2867
|
else if (committed !== undefined) {
|
|
@@ -2587,15 +2910,17 @@ export class MemoryEngine {
|
|
|
2587
2910
|
}
|
|
2588
2911
|
const headers = await this.backend.listHeaders([...new Set([...handle.scopes, writeScope])]);
|
|
2589
2912
|
handle.indexText = this.rebuildIndex(handle, headers, { write: true, ignoreOnDisk: indexGate !== undefined }, report.warnings);
|
|
2590
|
-
await this.rebaseline(handle, new Set(report.degraded?.pending ?? []), new Set(report.rejections.map((r) => r.path)));
|
|
2913
|
+
await this.rebaseline(handle, new Set([...(report.degraded?.pending ?? []), ...stoodAsFound]), new Set(report.rejections.map((r) => r.path)));
|
|
2591
2914
|
if (carry && lineageSessionId !== undefined && report.degraded === undefined) {
|
|
2592
2915
|
const deferredSeats = [...new Set(report.rejections.filter((r) => r.code === "deferred").map((r) => r.path))];
|
|
2593
2916
|
const shown = deferredSeats.slice(0, MAX_DISCLOSED_DEFERRED_SEATS);
|
|
2594
2917
|
const rest = deferredSeats.length - shown.length;
|
|
2595
2918
|
const seatList = `${shown.join(", ")}${rest > 0 ? ` (+${rest} more — see the rejections)` : ""}`;
|
|
2596
2919
|
try {
|
|
2597
|
-
const closed = closeSessionAccount(this.controlDir, { sessionId: lineageSessionId, now: this.now });
|
|
2598
|
-
if (closed &&
|
|
2920
|
+
const closed = handle.accountLeg !== undefined ? closeSessionAccount(this.controlDir, { sessionId: lineageSessionId, now: this.now, leg: handle.accountLeg }) : "absent";
|
|
2921
|
+
if (closed === "stale" && !report.warnings.includes(STALE_LEG_NOTE))
|
|
2922
|
+
report.warnings.push(STALE_LEG_NOTE);
|
|
2923
|
+
if (closed === "written" && deferredSeats.length > 0) {
|
|
2599
2924
|
report.warnings.push(`memory session account closed with ${deferredSeats.length} file(s) still DEFERRED on the model-visible plane — their projection-debt rows STAND and are re-judged by the next harvest's consult, not by this close: ${seatList}`);
|
|
2600
2925
|
}
|
|
2601
2926
|
}
|
|
@@ -4288,6 +4613,8 @@ export class MemoryEngine {
|
|
|
4288
4613
|
const id = parsed.id ?? handle.baseIds.get(path);
|
|
4289
4614
|
if (id === undefined)
|
|
4290
4615
|
continue;
|
|
4616
|
+
if (uncommitted && handle.baseIds.get(path) === undefined)
|
|
4617
|
+
continue;
|
|
4291
4618
|
const stub = stubs.get(path);
|
|
4292
4619
|
const priorRev = deferred ? handle.baseRevs.get(path) : undefined;
|
|
4293
4620
|
const priorId = deferred ? handle.baseIds.get(path) : undefined;
|
|
@@ -4504,13 +4831,15 @@ export class MemoryEngine {
|
|
|
4504
4831
|
if (opts.write) {
|
|
4505
4832
|
let wrote = false;
|
|
4506
4833
|
try {
|
|
4834
|
+
this.refuseOutsideStore(handle.memoryDir, indexPath, "writing the index");
|
|
4507
4835
|
mkdirSync(dirname(indexPath), { recursive: true });
|
|
4508
4836
|
if (readNoFollowSafe(indexPath) !== text)
|
|
4509
4837
|
writeFileNoFollow(indexPath, text);
|
|
4510
4838
|
wrote = true;
|
|
4511
4839
|
}
|
|
4512
|
-
catch {
|
|
4840
|
+
catch (err) {
|
|
4513
4841
|
handle.indexOnDiskUntrusted = true;
|
|
4842
|
+
warnings?.push(`${INDEX_WRITE_REFUSED}: ${this.writeFailureText(err)}`);
|
|
4514
4843
|
}
|
|
4515
4844
|
if (wrote)
|
|
4516
4845
|
writeIndexRevs(this.controlDir, nextIndexRevs);
|
|
@@ -4593,11 +4922,12 @@ export class MemoryEngine {
|
|
|
4593
4922
|
}
|
|
4594
4923
|
let cleared = false;
|
|
4595
4924
|
try {
|
|
4925
|
+
this.refuseOutsideStore(handle.memoryDir, indexPath, "clearing the index");
|
|
4596
4926
|
writeFileNoFollow(indexPath, "");
|
|
4597
4927
|
cleared = true;
|
|
4598
4928
|
}
|
|
4599
4929
|
catch (err) {
|
|
4600
|
-
detail = `${detail}; the derived index could NOT be cleared: ${
|
|
4930
|
+
detail = `${detail}; the derived index could NOT be cleared: ${this.writeFailureText(err)}`;
|
|
4601
4931
|
}
|
|
4602
4932
|
const outcome = [
|
|
4603
4933
|
captured ? "the offending prose was captured to quarantine" : "the quarantine capture FAILED — the prose was not preserved",
|
|
@@ -4612,7 +4942,16 @@ export class MemoryEngine {
|
|
|
4612
4942
|
contained: cleared,
|
|
4613
4943
|
};
|
|
4614
4944
|
}
|
|
4615
|
-
|
|
4945
|
+
refuseOutsideStore(root, path, act) {
|
|
4946
|
+
const escape = storeTargetEscape(root, path);
|
|
4947
|
+
if (escape === undefined)
|
|
4948
|
+
return;
|
|
4949
|
+
const e = new Error(`${TARGET_OUTSIDE_STORE}: ${act} ${path} was refused — ${escape}; not performed`);
|
|
4950
|
+
e.code = TARGET_OUTSIDE_STORE;
|
|
4951
|
+
throw e;
|
|
4952
|
+
}
|
|
4953
|
+
writeIfChanged(target, text, root) {
|
|
4954
|
+
this.refuseOutsideStore(root, target, "the projection write of");
|
|
4616
4955
|
const current = readNoFollowSafe(target);
|
|
4617
4956
|
if (current === text)
|
|
4618
4957
|
return;
|
|
@@ -4621,19 +4960,19 @@ export class MemoryEngine {
|
|
|
4621
4960
|
}
|
|
4622
4961
|
writeBackProjection(path, entry, needed) {
|
|
4623
4962
|
if (!needed)
|
|
4624
|
-
return
|
|
4963
|
+
return undefined;
|
|
4625
4964
|
const text = serializeEntryFile(entry);
|
|
4626
4965
|
if (readNoFollowSafe(path) === text)
|
|
4627
|
-
return
|
|
4966
|
+
return undefined;
|
|
4628
4967
|
try {
|
|
4629
4968
|
writeFileNoFollow(path, text);
|
|
4630
|
-
return
|
|
4969
|
+
return undefined;
|
|
4631
4970
|
}
|
|
4632
|
-
catch {
|
|
4633
|
-
return
|
|
4971
|
+
catch (err) {
|
|
4972
|
+
return this.writeFailureText(err);
|
|
4634
4973
|
}
|
|
4635
4974
|
}
|
|
4636
|
-
async repairProjectionSeat(seat, canonical, entryId, debtClears) {
|
|
4975
|
+
async repairProjectionSeat(seat, canonical, entryId, debtClears, warnings) {
|
|
4637
4976
|
const committed = await this.committedContentFor(entryId);
|
|
4638
4977
|
if (committed === undefined)
|
|
4639
4978
|
return;
|
|
@@ -4641,7 +4980,8 @@ export class MemoryEngine {
|
|
|
4641
4980
|
try {
|
|
4642
4981
|
writeFileNoFollow(canonical, committed);
|
|
4643
4982
|
}
|
|
4644
|
-
catch {
|
|
4983
|
+
catch (err) {
|
|
4984
|
+
warnings.push(`${relative(this.memoryDir, canonical)}: the committed copy could NOT be written over this id-less seat — the projection-debt row stands and the repair retries next pass: ${this.writeFailureText(err)}`);
|
|
4645
4985
|
return;
|
|
4646
4986
|
}
|
|
4647
4987
|
}
|
|
@@ -4662,18 +5002,18 @@ export class MemoryEngine {
|
|
|
4662
5002
|
return { state: "unknown" };
|
|
4663
5003
|
}
|
|
4664
5004
|
}
|
|
4665
|
-
|
|
5005
|
+
censusScopeTree(dir) {
|
|
4666
5006
|
if (!existsSync(dir))
|
|
4667
5007
|
return;
|
|
4668
|
-
const
|
|
4669
|
-
const
|
|
5008
|
+
const unreadable = [];
|
|
5009
|
+
const links = [];
|
|
4670
5010
|
const walk = (d) => {
|
|
4671
5011
|
let names = [];
|
|
4672
5012
|
try {
|
|
4673
5013
|
names = readdirSync(d);
|
|
4674
5014
|
}
|
|
4675
5015
|
catch {
|
|
4676
|
-
|
|
5016
|
+
unreadable.push(d);
|
|
4677
5017
|
return;
|
|
4678
5018
|
}
|
|
4679
5019
|
for (const name of names) {
|
|
@@ -4683,50 +5023,34 @@ export class MemoryEngine {
|
|
|
4683
5023
|
st = lstatSync(p);
|
|
4684
5024
|
}
|
|
4685
5025
|
catch {
|
|
4686
|
-
|
|
5026
|
+
unreadable.push(p);
|
|
4687
5027
|
continue;
|
|
4688
5028
|
}
|
|
4689
|
-
if (st.isSymbolicLink())
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
}
|
|
4693
|
-
if (st.isDirectory())
|
|
5029
|
+
if (st.isSymbolicLink())
|
|
5030
|
+
links.push(p);
|
|
5031
|
+
else if (st.isDirectory())
|
|
4694
5032
|
walk(p);
|
|
4695
|
-
else if (st.isFile() && name.endsWith(".md") && name !== MEMORY_INDEX_FILENAME)
|
|
4696
|
-
chmodNoFollow(p, fileMode);
|
|
4697
5033
|
}
|
|
4698
|
-
chmodNoFollow(d, dirMode);
|
|
4699
5034
|
};
|
|
4700
5035
|
walk(dir);
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
});
|
|
4720
|
-
if (said !== undefined) {
|
|
4721
|
-
for (const p of skipped)
|
|
4722
|
-
said.add(anomalyKey("unreadable", p));
|
|
4723
|
-
for (const p of links)
|
|
4724
|
-
said.add(anomalyKey("symlink", p));
|
|
4725
|
-
}
|
|
4726
|
-
}
|
|
4727
|
-
catch (err) {
|
|
4728
|
-
this.discloseAnnounceFailure("mode-sweep enqueue", err);
|
|
4729
|
-
}
|
|
5036
|
+
if (unreadable.length + links.length === 0)
|
|
5037
|
+
return;
|
|
5038
|
+
try {
|
|
5039
|
+
enqueueMemoryAnnouncement(this.controlDir, {
|
|
5040
|
+
kind: "gate",
|
|
5041
|
+
at: this.now(),
|
|
5042
|
+
items: unreadable
|
|
5043
|
+
.slice(0, 12)
|
|
5044
|
+
.map((p) => `layer census could not read ${JSON.stringify(inlineUntrusted(relative(dir, p) || "."))} — nothing under it was examined`)
|
|
5045
|
+
.concat(unreadable.length > 12 ? [`…and ${unreadable.length - 12} more unreadable path(s)`] : [])
|
|
5046
|
+
.concat(links
|
|
5047
|
+
.slice(0, 12)
|
|
5048
|
+
.map((p) => `layer census skipped the symlink ${JSON.stringify(inlineUntrusted(relative(dir, p) || "."))} — links are not part of a memory scope tree and are never followed`))
|
|
5049
|
+
.concat(links.length > 12 ? [`…and ${links.length - 12} more skipped symlink(s)`] : []),
|
|
5050
|
+
});
|
|
5051
|
+
}
|
|
5052
|
+
catch (err) {
|
|
5053
|
+
this.discloseAnnounceFailure("layer-census enqueue", err);
|
|
4730
5054
|
}
|
|
4731
5055
|
}
|
|
4732
5056
|
}
|
|
@@ -4813,16 +5137,6 @@ function readSafe(path) {
|
|
|
4813
5137
|
return undefined;
|
|
4814
5138
|
}
|
|
4815
5139
|
}
|
|
4816
|
-
function openNoFollowForMode(path) {
|
|
4817
|
-
for (const flags of [fsConstants.O_RDONLY, fsConstants.O_WRONLY]) {
|
|
4818
|
-
try {
|
|
4819
|
-
return openSync(path, flags | fsConstants.O_NOFOLLOW);
|
|
4820
|
-
}
|
|
4821
|
-
catch {
|
|
4822
|
-
}
|
|
4823
|
-
}
|
|
4824
|
-
return undefined;
|
|
4825
|
-
}
|
|
4826
5140
|
function readNoFollowSafe(path) {
|
|
4827
5141
|
let fd;
|
|
4828
5142
|
try {
|
|
@@ -4837,19 +5151,6 @@ function readNoFollowSafe(path) {
|
|
|
4837
5151
|
closeSync(fd);
|
|
4838
5152
|
}
|
|
4839
5153
|
}
|
|
4840
|
-
function chmodNoFollow(path, mode) {
|
|
4841
|
-
const fd = openNoFollowForMode(path);
|
|
4842
|
-
if (fd === undefined)
|
|
4843
|
-
return;
|
|
4844
|
-
try {
|
|
4845
|
-
fchmodSync(fd, mode);
|
|
4846
|
-
}
|
|
4847
|
-
catch {
|
|
4848
|
-
}
|
|
4849
|
-
finally {
|
|
4850
|
-
closeSync(fd);
|
|
4851
|
-
}
|
|
4852
|
-
}
|
|
4853
5154
|
function needsCompletion(parsed, fm) {
|
|
4854
5155
|
return (!parsed.hadFrontmatter ||
|
|
4855
5156
|
parsed.frontmatter.name !== fm.name ||
|