@sema-agent/core 7.30.1 → 7.31.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 +242 -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 +3 -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 +123 -26
- package/dist/core/memory-engine/engine.js +426 -149
- 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 +16 -23
- package/dist/core/memory-engine/file-backend.js +122 -99
- 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 +11 -0
- package/dist/core/memory-engine/layout.js +20 -4
- 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 +11 -0
- package/dist/core/secret-env.js +20 -0
- 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/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -1
- package/wire-changes/core@7.31.0.json +705 -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, 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,6 +302,11 @@ 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;
|
|
@@ -315,6 +321,7 @@ export class MemoryEngine {
|
|
|
315
321
|
holdSettleTimeoutMs;
|
|
316
322
|
provenance;
|
|
317
323
|
onIncident;
|
|
324
|
+
unsettleableDisclosed = false;
|
|
318
325
|
backendPinnedRoot;
|
|
319
326
|
constructor(opts) {
|
|
320
327
|
if (opts.provenance !== undefined && opts.provenance !== "off" && opts.provenance !== "carry") {
|
|
@@ -360,6 +367,18 @@ export class MemoryEngine {
|
|
|
360
367
|
return this.provenance;
|
|
361
368
|
}
|
|
362
369
|
consolidation;
|
|
370
|
+
endSessionLeg(handle, sessionId) {
|
|
371
|
+
return handle.accountLeg !== undefined ? this.endLeg(sessionId, handle.accountLeg) : undefined;
|
|
372
|
+
}
|
|
373
|
+
endLeg(sessionId, leg) {
|
|
374
|
+
try {
|
|
375
|
+
return terminateSessionAccount(this.controlDir, { sessionId, now: this.now, leg });
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
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);
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
363
382
|
discloseSessionAccountIncident(what, cause) {
|
|
364
383
|
const sink = this.onIncident;
|
|
365
384
|
if (sink === undefined)
|
|
@@ -372,6 +391,208 @@ export class MemoryEngine {
|
|
|
372
391
|
catch {
|
|
373
392
|
}
|
|
374
393
|
}
|
|
394
|
+
async detectReadOnlyLayerChanges(handle, report, admitsNothing) {
|
|
395
|
+
const candidates = [];
|
|
396
|
+
for (const m of handle.materialized.filter((x) => x.readonly)) {
|
|
397
|
+
const text = readSafe(m.path);
|
|
398
|
+
if (text === undefined) {
|
|
399
|
+
report.missing.push(m.relPath);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (revOfText(text, m.id) === m.rev)
|
|
403
|
+
continue;
|
|
404
|
+
if ((await this.committedContentUnadopted(m.id)) === text)
|
|
405
|
+
continue;
|
|
406
|
+
const stood = handle.layerBaseline?.get(m.path) === layerDigest(text);
|
|
407
|
+
const rejection = admitsNothing
|
|
408
|
+
? { 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` }
|
|
409
|
+
: { 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" };
|
|
410
|
+
report.rejections.push(rejection);
|
|
411
|
+
if (!admitsNothing)
|
|
412
|
+
candidates.push({ path: m.path, relPath: m.relPath, scope: m.scope, id: m.id, stood, rejection });
|
|
413
|
+
}
|
|
414
|
+
return candidates;
|
|
415
|
+
}
|
|
416
|
+
async committedContentUnadopted(id) {
|
|
417
|
+
const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
|
|
418
|
+
const view = zeroCopy ? undefined : this.backend.restrictedAdoptionView?.({ audit: false });
|
|
419
|
+
if (view === undefined)
|
|
420
|
+
return this.committedContentFor(id);
|
|
421
|
+
try {
|
|
422
|
+
const [entry] = await view.getByIds([id]);
|
|
423
|
+
if (entry !== undefined && entry.id === id)
|
|
424
|
+
return serializeEntryFile(entry);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
}
|
|
428
|
+
return this.backend.readCommittedShadow?.(id) ?? readSafe(join(this.controlDir, "shadow", `${id}.md`));
|
|
429
|
+
}
|
|
430
|
+
async restoreReadOnlyLayers(candidates, report, selfLeg) {
|
|
431
|
+
let writers;
|
|
432
|
+
for (const c of candidates) {
|
|
433
|
+
const text = readSafe(c.path);
|
|
434
|
+
const committed = await this.committedContentUnadopted(c.id);
|
|
435
|
+
if (text !== undefined && text === committed) {
|
|
436
|
+
report.rejections = report.rejections.filter((r) => r !== c.rejection);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (committed === undefined)
|
|
440
|
+
continue;
|
|
441
|
+
try {
|
|
442
|
+
writers ??= openLayerWriters(this.controlDir, selfLeg);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
writers = { scopes: new Set(), unscoped: 1 };
|
|
446
|
+
}
|
|
447
|
+
if (writers.unscoped > 0 || writers.scopes.has(c.scope)) {
|
|
448
|
+
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`;
|
|
449
|
+
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`);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
writeFileNoFollow(c.path, committed);
|
|
454
|
+
report.restored.push(c.relPath);
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
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)}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async observeReadOnlyLayers(handle, report, sessionId) {
|
|
462
|
+
const served = new Set(handle.materialized.filter((m) => m.readonly).map((m) => m.path));
|
|
463
|
+
const batch = { rows: [], rejections: [], undecided: [], landed: false };
|
|
464
|
+
for (const c of readOnlyLayerCandidates(this.memoryDir, this.controlDir, handle)) {
|
|
465
|
+
const by = { scope: c.scope, rel: c.rel, ...(sessionId !== undefined ? { sessionId } : {}), observedAt: this.now() };
|
|
466
|
+
if (c.id === undefined) {
|
|
467
|
+
batch.rows.push({ ...by, observedRev: c.digest });
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
const committed = await this.committedRowOf(c.id);
|
|
471
|
+
if (committed === "undecidable") {
|
|
472
|
+
const row = { ...by, entryId: c.id, observedRev: c.observedRev, undecided: true, observationId: uuidv7() };
|
|
473
|
+
batch.rows.push(row);
|
|
474
|
+
batch.undecided.push({ candidate: c, row });
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (committed !== undefined && committed.scope === c.scope && committed.slug === c.slug && committed.rev === c.observedRev)
|
|
478
|
+
continue;
|
|
479
|
+
batch.rows.push({ ...by, entryId: c.id, ...(committed !== undefined ? { rev: committed.rev } : {}), observedRev: c.observedRev });
|
|
480
|
+
}
|
|
481
|
+
if (!served.has(c.path)) {
|
|
482
|
+
batch.rejections.push(unexpectedLayerFile(handle, c));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
this.landObservations(batch, report);
|
|
486
|
+
return batch;
|
|
487
|
+
}
|
|
488
|
+
landObservations(batch, report) {
|
|
489
|
+
try {
|
|
490
|
+
recordLayerObservations(this.controlDir, batch.rows);
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
batch.failure = this.writeFailureText(err);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
batch.landed = true;
|
|
497
|
+
delete batch.failure;
|
|
498
|
+
report.rejections.push(...batch.rejections);
|
|
499
|
+
}
|
|
500
|
+
concludeObservations(batch, report) {
|
|
501
|
+
if (batch !== undefined && !batch.landed) {
|
|
502
|
+
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"}`);
|
|
503
|
+
}
|
|
504
|
+
this.discloseUndecidedObservations(landedUndecided(batch).map((u) => u.candidate), report);
|
|
505
|
+
}
|
|
506
|
+
discloseUndecidedObservations(undecided, report) {
|
|
507
|
+
for (const c of undecided) {
|
|
508
|
+
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`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async settleUndecidedObservations(handle, report, own) {
|
|
512
|
+
let rows;
|
|
513
|
+
try {
|
|
514
|
+
rows = undecidedLayerObservations(this.controlDir);
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
report.warnings.push(`undecided read-only-layer observations could not be read for settlement — they stay undecided (not adopted): ${this.writeFailureText(err)}`);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const verdicts = new Map();
|
|
521
|
+
for (const row of rows) {
|
|
522
|
+
const committed = row.entryId !== undefined ? await this.committedRowOf(row.entryId) : "undecidable";
|
|
523
|
+
if (committed !== "undecidable")
|
|
524
|
+
verdicts.set(row, observationBacked(row, committed) ? "backed" : committed !== undefined ? { rev: committed.rev } : {});
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
settleUndecidedLayerObservations(this.controlDir, verdicts);
|
|
528
|
+
}
|
|
529
|
+
catch (err) {
|
|
530
|
+
report.warnings.push(`undecided read-only-layer observations could not be settled — they stay undecided (not adopted): ${this.writeFailureText(err)}`);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const served = new Set(handle.materialized.filter((m) => m.readonly).map((m) => m.path));
|
|
534
|
+
const stillUndecided = [];
|
|
535
|
+
for (const { candidate: c, row: mine } of own) {
|
|
536
|
+
const row = rows.find((r) => sameObservation(r, mine));
|
|
537
|
+
if (row === undefined) {
|
|
538
|
+
const refused = served.has(c.path) ? false : await this.refusedNow(mine.entryId, c.observedRev);
|
|
539
|
+
if (refused === true)
|
|
540
|
+
report.rejections.push(unexpectedLayerFile(handle, c));
|
|
541
|
+
else if (refused === undefined)
|
|
542
|
+
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`);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const verdict = verdicts.get(row);
|
|
546
|
+
if (verdict === undefined)
|
|
547
|
+
stillUndecided.push(c);
|
|
548
|
+
else if (verdict !== "backed" && !served.has(c.path))
|
|
549
|
+
report.rejections.push(unexpectedLayerFile(handle, c));
|
|
550
|
+
}
|
|
551
|
+
this.discloseUndecidedObservations(stillUndecided, report);
|
|
552
|
+
}
|
|
553
|
+
async refusedNow(entryId, observedRev) {
|
|
554
|
+
const committed = entryId !== undefined ? await this.committedRowOf(entryId) : "undecidable";
|
|
555
|
+
if (entryId === undefined || committed === "undecidable")
|
|
556
|
+
return undefined;
|
|
557
|
+
try {
|
|
558
|
+
return layerObservationLookup(this.controlDir).find(entryId, committed?.rev, observedRev) !== undefined;
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
discloseUnsettleableObservations() {
|
|
565
|
+
if (this.unsettleableDisclosed || typeof this.backend.committedSnapshotOf === "function")
|
|
566
|
+
return;
|
|
567
|
+
this.unsettleableDisclosed = true;
|
|
568
|
+
const sink = this.onIncident;
|
|
569
|
+
if (sink === undefined)
|
|
570
|
+
return;
|
|
571
|
+
try {
|
|
572
|
+
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");
|
|
573
|
+
e.code = "memory.observation_settlement_unavailable";
|
|
574
|
+
sink(e);
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async committedRowOf(id) {
|
|
580
|
+
const face = this.backend.committedSnapshotOf;
|
|
581
|
+
if (face === undefined)
|
|
582
|
+
return "undecidable";
|
|
583
|
+
try {
|
|
584
|
+
const snap = await face.call(this.backend, id);
|
|
585
|
+
if (snap.state !== "row")
|
|
586
|
+
return undefined;
|
|
587
|
+
return { rev: snap.rev, ...(snap.binding.state === "bound" ? { scope: snap.binding.scope, slug: snap.binding.slug } : {}) };
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
return "undecidable";
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
writeFailureText(err) {
|
|
594
|
+
return thrownText(withPermissionRemedy(err, this.memoryDir));
|
|
595
|
+
}
|
|
375
596
|
discloseAnnounceFailure(stage, cause) {
|
|
376
597
|
const sink = this.onIncident;
|
|
377
598
|
if (sink === undefined)
|
|
@@ -557,7 +778,7 @@ export class MemoryEngine {
|
|
|
557
778
|
}
|
|
558
779
|
}
|
|
559
780
|
catch (err) {
|
|
560
|
-
out.failures.push({ path: MEMORY_INDEX_FILENAME, detail: `index sweep failed: ${
|
|
781
|
+
out.failures.push({ path: MEMORY_INDEX_FILENAME, detail: `index sweep failed: ${this.writeFailureText(err)}` });
|
|
561
782
|
}
|
|
562
783
|
}
|
|
563
784
|
const scanned = scanEntryFiles(writableRoot, {
|
|
@@ -726,6 +947,14 @@ export class MemoryEngine {
|
|
|
726
947
|
return this.challengeAndAnnounce(ids.map((entryId) => ({ eventId: `${requestId}:${entryId}`, entryId, reason })));
|
|
727
948
|
}
|
|
728
949
|
async eraseMemoryEntries(input) {
|
|
950
|
+
try {
|
|
951
|
+
return await this.eraseMemoryEntriesCore(input);
|
|
952
|
+
}
|
|
953
|
+
catch (err) {
|
|
954
|
+
throw withPermissionRemedy(err, this.memoryDir);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
async eraseMemoryEntriesCore(input) {
|
|
729
958
|
const captured = captureErasureInput(input);
|
|
730
959
|
const bad = erasureRequestInvalid(captured);
|
|
731
960
|
if (bad !== undefined) {
|
|
@@ -867,7 +1096,7 @@ export class MemoryEngine {
|
|
|
867
1096
|
}
|
|
868
1097
|
}
|
|
869
1098
|
catch (err) {
|
|
870
|
-
uncleared.push(`${join(this.memoryDir, "<scope homes>", MEMORY_INDEX_FILENAME)} (index sweep failed: ${
|
|
1099
|
+
uncleared.push(`${join(this.memoryDir, "<scope homes>", MEMORY_INDEX_FILENAME)} (index sweep failed: ${this.writeFailureText(err)})`);
|
|
871
1100
|
}
|
|
872
1101
|
if (uncleared.length > 0) {
|
|
873
1102
|
try {
|
|
@@ -1168,12 +1397,23 @@ export class MemoryEngine {
|
|
|
1168
1397
|
adoptRootScopeLayout(this.memoryDir, this.controlDir, { ownerCandidate: writeScope, onIncident: this.onIncident, convertSidecars: convertV1SidecarKeys });
|
|
1169
1398
|
}
|
|
1170
1399
|
async materialize(scopes, writeScope, opts) {
|
|
1400
|
+
const legSeat = {};
|
|
1401
|
+
try {
|
|
1402
|
+
return await this.materializeCore(scopes, writeScope, opts, legSeat);
|
|
1403
|
+
}
|
|
1404
|
+
catch (err) {
|
|
1405
|
+
if (legSeat.leg !== undefined && opts?.sessionId !== undefined)
|
|
1406
|
+
this.endLeg(opts.sessionId, legSeat.leg);
|
|
1407
|
+
throw withPermissionRemedy(err, this.memoryDir);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
async materializeCore(scopes, writeScope, opts, legSeat) {
|
|
1171
1411
|
ensureDirExists(this.memoryDir);
|
|
1172
1412
|
ensureDirExists(this.controlDir);
|
|
1173
1413
|
await this.adoptLayoutBeforeMount(writeScope);
|
|
1174
|
-
chmodNoFollow(this.memoryDir, 0o755);
|
|
1175
1414
|
const restricted = opts?.adoptionRestricted === true;
|
|
1176
1415
|
let sessionAccountFailed = false;
|
|
1416
|
+
let accountLeg;
|
|
1177
1417
|
const readBackend = restricted
|
|
1178
1418
|
? (this.backend.restrictedAdoptionView?.({ audit: true, writeScope }) ?? this.backend)
|
|
1179
1419
|
: this.backend;
|
|
@@ -1182,9 +1422,13 @@ export class MemoryEngine {
|
|
|
1182
1422
|
scopeDirs.set(scope, registerScope(this.memoryDir, this.controlDir, scope));
|
|
1183
1423
|
if (writeScope !== null && !scopeDirs.has(writeScope))
|
|
1184
1424
|
scopeDirs.set(writeScope, registerScope(this.memoryDir, this.controlDir, writeScope));
|
|
1185
|
-
const sweepAnnounced = new Set();
|
|
1186
1425
|
for (const dir of scopeDirs.values())
|
|
1187
|
-
this.
|
|
1426
|
+
this.censusScopeTree(dir);
|
|
1427
|
+
const layerBaseline = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir)
|
|
1428
|
+
? readOnlyLayerCensus([...scopeDirs].filter(([scope]) => scope !== writeScope).map(([, dir]) => dir))
|
|
1429
|
+
: undefined;
|
|
1430
|
+
if (layerBaseline !== undefined)
|
|
1431
|
+
this.discloseUnsettleableObservations();
|
|
1188
1432
|
if (this.provenance === "carry" && opts?.sessionId !== undefined && writeScope !== null && !restricted) {
|
|
1189
1433
|
let accountFailed = false;
|
|
1190
1434
|
try {
|
|
@@ -1300,10 +1544,19 @@ export class MemoryEngine {
|
|
|
1300
1544
|
}
|
|
1301
1545
|
}
|
|
1302
1546
|
}
|
|
1303
|
-
|
|
1547
|
+
accountLeg = legSeat.leg = uuidv7();
|
|
1548
|
+
openSessionAccount(this.controlDir, { sessionId: opts.sessionId, now: this.now, unattributed, writeScope, leg: accountLeg });
|
|
1304
1549
|
}
|
|
1305
1550
|
catch (err) {
|
|
1306
1551
|
accountFailed = true;
|
|
1552
|
+
if (accountLeg !== undefined) {
|
|
1553
|
+
try {
|
|
1554
|
+
if (sessionAccountLeg(this.controlDir, opts.sessionId) !== accountLeg)
|
|
1555
|
+
accountLeg = legSeat.leg = undefined;
|
|
1556
|
+
}
|
|
1557
|
+
catch {
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1307
1560
|
this.discloseSessionAccountIncident("crash-residue attribution is degraded for this session (the harvest boundary refuses fail-closed while this stands)", err);
|
|
1308
1561
|
}
|
|
1309
1562
|
if (accountFailed)
|
|
@@ -1334,6 +1587,8 @@ export class MemoryEngine {
|
|
|
1334
1587
|
...(opts?.shadowNames !== undefined ? { shadowNames: opts.shadowNames } : {}),
|
|
1335
1588
|
...(restricted ? { adoptionRestricted: true } : {}),
|
|
1336
1589
|
...(sessionAccountFailed ? { sessionAccountFailed: true } : {}),
|
|
1590
|
+
...(layerBaseline !== undefined ? { layerBaseline } : {}),
|
|
1591
|
+
...(accountLeg !== undefined ? { accountLeg } : {}),
|
|
1337
1592
|
};
|
|
1338
1593
|
if (handle.writableRoot !== null)
|
|
1339
1594
|
ensureDirExists(handle.writableRoot);
|
|
@@ -1359,7 +1614,7 @@ export class MemoryEngine {
|
|
|
1359
1614
|
const { deleted: _stubDrop, ...stubFm } = entry.frontmatter;
|
|
1360
1615
|
const text = stub ? serializeEntryFile({ id: entry.id, frontmatter: stubFm, body: STUB_ARCHIVED_LINE }) : fullText;
|
|
1361
1616
|
const target = join(dir, `${entry.slug}.md`);
|
|
1362
|
-
if (!
|
|
1617
|
+
if (!zeroCopy)
|
|
1363
1618
|
this.writeIfChanged(target, text);
|
|
1364
1619
|
const onDiskRev = stub ? revOfText(text, entry.id) : entry.rev;
|
|
1365
1620
|
const path = canonicalize(target);
|
|
@@ -1444,6 +1699,15 @@ export class MemoryEngine {
|
|
|
1444
1699
|
catch {
|
|
1445
1700
|
}
|
|
1446
1701
|
}
|
|
1702
|
+
const indexRefusals = materializeHealWarnings.filter((w) => w.startsWith(INDEX_WRITE_REFUSED));
|
|
1703
|
+
if (indexRefusals.length > 0) {
|
|
1704
|
+
try {
|
|
1705
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at: this.now(), items: capItems(indexRefusals.map((w) => inlineUntrusted(w, 500))) });
|
|
1706
|
+
}
|
|
1707
|
+
catch (err) {
|
|
1708
|
+
this.discloseAnnounceFailure("materialize index-write enqueue", err);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1447
1711
|
if (indexWrite && handle.indexOnDiskUntrusted !== true) {
|
|
1448
1712
|
const dropNotes = materializeHealWarnings.filter((w) => w.includes("opaque external-origin handle"));
|
|
1449
1713
|
if (dropNotes.length > 0) {
|
|
@@ -1455,16 +1719,20 @@ export class MemoryEngine {
|
|
|
1455
1719
|
}
|
|
1456
1720
|
}
|
|
1457
1721
|
}
|
|
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
1722
|
if (handle.writableRoot !== null) {
|
|
1466
1723
|
const writableRoot = handle.writableRoot;
|
|
1467
1724
|
const files = new Map();
|
|
1725
|
+
const refused = new Map();
|
|
1726
|
+
const probeWritable = (p) => {
|
|
1727
|
+
try {
|
|
1728
|
+
accessSync(p, fsConstants.W_OK);
|
|
1729
|
+
}
|
|
1730
|
+
catch (err) {
|
|
1731
|
+
if (err.code !== "ENOENT" && !refused.has(p))
|
|
1732
|
+
refused.set(p, err);
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
probeWritable(writableRoot);
|
|
1468
1736
|
for (const f of scanEntryFiles(writableRoot, { maxDepth: this.maxDepth })) {
|
|
1469
1737
|
const canonical = canonicalize(f.path);
|
|
1470
1738
|
if (canonical !== writableRoot && !canonical.startsWith(`${writableRoot}${sep}`))
|
|
@@ -1472,6 +1740,19 @@ export class MemoryEngine {
|
|
|
1472
1740
|
const text = readSafe(canonical);
|
|
1473
1741
|
if (text !== undefined)
|
|
1474
1742
|
files.set(canonical, planeDigest(text));
|
|
1743
|
+
probeWritable(dirname(canonical));
|
|
1744
|
+
probeWritable(canonical);
|
|
1745
|
+
}
|
|
1746
|
+
const [firstRefused] = refused;
|
|
1747
|
+
if (firstRefused !== undefined) {
|
|
1748
|
+
const [path, err] = firstRefused;
|
|
1749
|
+
const remedy = permissionRemedyOf(Object.assign(new Error("write refused"), { code: err?.code, path }), this.memoryDir);
|
|
1750
|
+
try {
|
|
1751
|
+
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)] });
|
|
1752
|
+
}
|
|
1753
|
+
catch (err) {
|
|
1754
|
+
this.discloseAnnounceFailure("write-home permission enqueue", err);
|
|
1755
|
+
}
|
|
1475
1756
|
}
|
|
1476
1757
|
const indexOnDisk = readSafe(join(writableRoot, MEMORY_INDEX_FILENAME));
|
|
1477
1758
|
handle.planeBaseline = { files, ...(indexOnDisk !== undefined ? { index: indexOnDisk } : {}) };
|
|
@@ -1524,14 +1805,17 @@ export class MemoryEngine {
|
|
|
1524
1805
|
};
|
|
1525
1806
|
}
|
|
1526
1807
|
gateWrite(handle, canonicalPath, content) {
|
|
1527
|
-
const
|
|
1528
|
-
if (
|
|
1808
|
+
const inside = (root) => canonicalPath === root || canonicalPath.startsWith(`${root}${sep}`);
|
|
1809
|
+
if (!inside(handle.memoryDir))
|
|
1529
1810
|
return { ok: true };
|
|
1530
|
-
|
|
1811
|
+
const root = handle.writableRoot;
|
|
1812
|
+
if (handle.writeScope === null || root === null || !inside(root)) {
|
|
1531
1813
|
return {
|
|
1532
1814
|
ok: false,
|
|
1533
1815
|
code: "read_only_layering",
|
|
1534
|
-
reason:
|
|
1816
|
+
reason: handle.writeScope === null || root === null
|
|
1817
|
+
? "this session's memory is read-only (no write scope) — the engine does not accept writes into the memory domain. Nothing was written."
|
|
1818
|
+
: `this path is in a read-only inherited memory layer — this session writes memory only under ${root}. Nothing was written.`,
|
|
1535
1819
|
muted: false,
|
|
1536
1820
|
};
|
|
1537
1821
|
}
|
|
@@ -1555,7 +1839,18 @@ export class MemoryEngine {
|
|
|
1555
1839
|
return { ok: false, code: f.code, reason, muted };
|
|
1556
1840
|
}
|
|
1557
1841
|
async harvest(handle, opts) {
|
|
1558
|
-
const
|
|
1842
|
+
const out = {};
|
|
1843
|
+
try {
|
|
1844
|
+
out.report = await this.harvestCore(handle, opts);
|
|
1845
|
+
}
|
|
1846
|
+
catch (err) {
|
|
1847
|
+
throw withPermissionRemedy(err, this.memoryDir);
|
|
1848
|
+
}
|
|
1849
|
+
finally {
|
|
1850
|
+
if (opts?.phase === "terminal" && opts.sessionId !== undefined && this.endSessionLeg(handle, opts.sessionId) === "stale" && out.report !== undefined && !out.report.warnings.includes(STALE_LEG_NOTE))
|
|
1851
|
+
out.report.warnings.push(STALE_LEG_NOTE);
|
|
1852
|
+
}
|
|
1853
|
+
const report = out.report;
|
|
1559
1854
|
try {
|
|
1560
1855
|
const named = report.reportId !== undefined ? { reportId: report.reportId } : {};
|
|
1561
1856
|
const gateItems = gateAnnouncementItems(report);
|
|
@@ -1606,7 +1901,16 @@ export class MemoryEngine {
|
|
|
1606
1901
|
const writeScope = handle.writeScope;
|
|
1607
1902
|
const captureRead = opts?.sessionId !== undefined ? await this.sessionCaptureOptOutOrFault(opts.sessionId) : { fault: false };
|
|
1608
1903
|
const captureOptOut = captureRead.record;
|
|
1609
|
-
|
|
1904
|
+
const admitsNothing = writeScope === null || opts?.admitNothing !== undefined || captureOptOut !== undefined || captureRead.fault;
|
|
1905
|
+
const readOnlyRestorable = await this.detectReadOnlyLayerChanges(handle, report, admitsNothing);
|
|
1906
|
+
let observed;
|
|
1907
|
+
try {
|
|
1908
|
+
observed = await this.observeReadOnlyLayers(handle, report, opts?.sessionId);
|
|
1909
|
+
}
|
|
1910
|
+
catch (err) {
|
|
1911
|
+
report.warnings.push(`read-only memory layers could not be examined for unexpected writes (nothing was recorded): ${this.writeFailureText(err)}`);
|
|
1912
|
+
}
|
|
1913
|
+
if (admitsNothing) {
|
|
1610
1914
|
try {
|
|
1611
1915
|
this.backend.auditRestrictedDivergence?.([...new Set([...handle.scopes, ...(writeScope !== null ? [writeScope] : [])])], (handle.adoptionRestricted === true || captureOptOut !== undefined) && writeScope !== null ? { idlessWriteScope: writeScope } : undefined);
|
|
1612
1916
|
}
|
|
@@ -1627,9 +1931,10 @@ export class MemoryEngine {
|
|
|
1627
1931
|
}
|
|
1628
1932
|
}
|
|
1629
1933
|
catch (err) {
|
|
1630
|
-
report.warnings.push(`capture opt-out boundary sweep failed: ${
|
|
1934
|
+
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
1935
|
}
|
|
1632
1936
|
}
|
|
1937
|
+
this.concludeObservations(observed, report);
|
|
1633
1938
|
if (opts?.admitNothing !== undefined)
|
|
1634
1939
|
report.warnings.push(opts.admitNothing.reason);
|
|
1635
1940
|
else if (captureOptOut !== undefined) {
|
|
@@ -1643,6 +1948,7 @@ export class MemoryEngine {
|
|
|
1643
1948
|
if (carry && handle.sessionAccountFailed === true) {
|
|
1644
1949
|
report.ok = false;
|
|
1645
1950
|
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)" };
|
|
1951
|
+
this.concludeObservations(observed, report);
|
|
1646
1952
|
return report;
|
|
1647
1953
|
}
|
|
1648
1954
|
try {
|
|
@@ -1650,9 +1956,15 @@ export class MemoryEngine {
|
|
|
1650
1956
|
}
|
|
1651
1957
|
catch (err) {
|
|
1652
1958
|
report.ok = false;
|
|
1653
|
-
report.incident = { kind: "sidecar_corrupt", detail: `memory control plane refused: ${
|
|
1959
|
+
report.incident = { kind: "sidecar_corrupt", detail: `memory control plane refused: ${this.writeFailureText(err)}` };
|
|
1960
|
+
this.concludeObservations(observed, report);
|
|
1654
1961
|
return report;
|
|
1655
1962
|
}
|
|
1963
|
+
if (observed !== undefined && !observed.landed)
|
|
1964
|
+
this.landObservations(observed, report);
|
|
1965
|
+
if (observed !== undefined && !observed.landed)
|
|
1966
|
+
this.concludeObservations(observed, report);
|
|
1967
|
+
await this.settleUndecidedObservations(handle, report, landedUndecided(observed));
|
|
1656
1968
|
try {
|
|
1657
1969
|
if (carry) {
|
|
1658
1970
|
reconcileHolds(this.controlDir, { homeOf: (scope) => scopeDirFor(this.memoryDir, this.controlDir, scope), now: this.now });
|
|
@@ -1834,7 +2146,7 @@ export class MemoryEngine {
|
|
|
1834
2146
|
return true;
|
|
1835
2147
|
}
|
|
1836
2148
|
catch (err) {
|
|
1837
|
-
report.warnings.push(`memory index could NOT be restored to its pre-session state: ${
|
|
2149
|
+
report.warnings.push(`memory index could NOT be restored to its pre-session state: ${this.writeFailureText(err)}`);
|
|
1838
2150
|
return false;
|
|
1839
2151
|
}
|
|
1840
2152
|
};
|
|
@@ -1864,39 +2176,7 @@ export class MemoryEngine {
|
|
|
1864
2176
|
signalContainment((c) => (c.indexRolledBack = true));
|
|
1865
2177
|
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
2178
|
};
|
|
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
|
-
}
|
|
2179
|
+
await this.restoreReadOnlyLayers(readOnlyRestorable, report, handle.accountLeg);
|
|
1900
2180
|
const missingWritable = writable.filter((m) => !recordByPath.has(m.path) && !existsSync(m.path) && !presentIds.has(m.id));
|
|
1901
2181
|
report.missing.push(...missingWritable.map((m) => m.relPath));
|
|
1902
2182
|
if (writable.length > 0 && missingWritable.length / writable.length > MASS_DELETION_FUSE_RATIO) {
|
|
@@ -1927,7 +2207,8 @@ export class MemoryEngine {
|
|
|
1927
2207
|
writeFileNoFollow(m.path, content);
|
|
1928
2208
|
report.restored.push(m.relPath);
|
|
1929
2209
|
}
|
|
1930
|
-
catch {
|
|
2210
|
+
catch (err) {
|
|
2211
|
+
report.warnings.push(`${m.relPath}: the committed copy of this missing memory file could NOT be restored: ${this.writeFailureText(err)}`);
|
|
1931
2212
|
}
|
|
1932
2213
|
}
|
|
1933
2214
|
const baselinePaths = new Set(writable.map((m) => m.path));
|
|
@@ -1964,6 +2245,8 @@ export class MemoryEngine {
|
|
|
1964
2245
|
const pendingProjections = [];
|
|
1965
2246
|
const idsAddedThisHarvest = new Set();
|
|
1966
2247
|
const modelIdAdds = new Set();
|
|
2248
|
+
const stoodAsFound = new Set();
|
|
2249
|
+
let observedTable;
|
|
1967
2250
|
let processed = 0;
|
|
1968
2251
|
for (let i = 0; i < kept.length; i++) {
|
|
1969
2252
|
const f = kept[i];
|
|
@@ -1980,10 +2263,14 @@ export class MemoryEngine {
|
|
|
1980
2263
|
continue;
|
|
1981
2264
|
}
|
|
1982
2265
|
const fastBaseId = idByBasePath.get(f.canonical);
|
|
2266
|
+
if (fastBaseId !== undefined && !stubByPath.has(f.canonical) && revOfText(f.text, fastBaseId) !== revByBasePath.get(f.canonical) && handle.planeBaseline?.files.get(f.canonical) === planeDigest(f.text)) {
|
|
2267
|
+
stoodAsFound.add(rel);
|
|
2268
|
+
continue;
|
|
2269
|
+
}
|
|
1983
2270
|
if (fastBaseId !== undefined && !stubByPath.has(f.canonical) && revOfText(f.text, fastBaseId) === revByBasePath.get(f.canonical)) {
|
|
1984
2271
|
const fastDebt = projectionDebts.get(debtSeat(writeScope, homeRel(writableRoot, f.canonical)));
|
|
1985
2272
|
if (fastDebt !== undefined && fastDebt.entryId === fastBaseId)
|
|
1986
|
-
await this.repairProjectionSeat(seatOf(f.canonical), f.canonical, fastBaseId, debtClears);
|
|
2273
|
+
await this.repairProjectionSeat(seatOf(f.canonical), f.canonical, fastBaseId, debtClears, report.warnings);
|
|
1987
2274
|
continue;
|
|
1988
2275
|
}
|
|
1989
2276
|
processed++;
|
|
@@ -2043,6 +2330,21 @@ export class MemoryEngine {
|
|
|
2043
2330
|
const parsed = f.parsed;
|
|
2044
2331
|
let baseId = idByBasePath.get(f.canonical);
|
|
2045
2332
|
let id = baseId ?? parsed.id;
|
|
2333
|
+
if (baseId === undefined && handle.planeBaseline?.files.get(f.canonical) === planeDigest(f.text)) {
|
|
2334
|
+
const observed = (observedTable ??= layerObservationLookup(this.controlDir));
|
|
2335
|
+
const committedRow = parsed.id !== undefined && observed.covers(parsed.id) ? await this.committedRowOf(parsed.id) : undefined;
|
|
2336
|
+
if (committedRow === "undecidable") {
|
|
2337
|
+
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" });
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
const recorded = parsed.id === undefined
|
|
2341
|
+
? observed.findNote(writeScope, homeRel(writableRoot, f.canonical), planeDigest(f.text)) !== undefined
|
|
2342
|
+
: observed.covers(parsed.id) && observed.find(parsed.id, committedRow?.rev, revOfText(f.text, parsed.id)) !== undefined;
|
|
2343
|
+
if (recorded) {
|
|
2344
|
+
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" });
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2046
2348
|
let debtReconciled = false;
|
|
2047
2349
|
if (id === undefined && projectionDebts.size > 0) {
|
|
2048
2350
|
const debt = projectionDebts.get(debtSeat(writeScope, homeRel(writableRoot, f.canonical)));
|
|
@@ -2129,7 +2431,7 @@ export class MemoryEngine {
|
|
|
2129
2431
|
report.restored.push(rel);
|
|
2130
2432
|
}
|
|
2131
2433
|
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: ${
|
|
2434
|
+
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
2435
|
}
|
|
2134
2436
|
}
|
|
2135
2437
|
}
|
|
@@ -2176,7 +2478,7 @@ export class MemoryEngine {
|
|
|
2176
2478
|
if (revOfText(f.text, id) === revByBasePath.get(f.canonical)) {
|
|
2177
2479
|
const seatDebt = projectionDebts.get(debtSeat(writeScope, homeRel(writableRoot, f.canonical)));
|
|
2178
2480
|
if (seatDebt !== undefined && seatDebt.entryId === id)
|
|
2179
|
-
await this.repairProjectionSeat(seatOf(f.canonical), f.canonical, id, debtClears);
|
|
2481
|
+
await this.repairProjectionSeat(seatOf(f.canonical), f.canonical, id, debtClears, report.warnings);
|
|
2180
2482
|
continue;
|
|
2181
2483
|
}
|
|
2182
2484
|
pendingProjections.push({ path: f.canonical, entry, needed: minted || parsed.id !== id || needsCompletion(parsed, fm), ...(debtReconciled ? { idCompletion: true } : {}) });
|
|
@@ -2200,7 +2502,7 @@ export class MemoryEngine {
|
|
|
2200
2502
|
report.rejections.push({
|
|
2201
2503
|
path: rel,
|
|
2202
2504
|
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 (${
|
|
2505
|
+
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
2506
|
});
|
|
2205
2507
|
continue;
|
|
2206
2508
|
}
|
|
@@ -2219,7 +2521,7 @@ export class MemoryEngine {
|
|
|
2219
2521
|
report.rejections.push({
|
|
2220
2522
|
path: rel,
|
|
2221
2523
|
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 (${
|
|
2524
|
+
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
2525
|
});
|
|
2224
2526
|
continue;
|
|
2225
2527
|
}
|
|
@@ -2341,7 +2643,7 @@ export class MemoryEngine {
|
|
|
2341
2643
|
report.rejections.push({
|
|
2342
2644
|
path: flipRec.rel,
|
|
2343
2645
|
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 (${
|
|
2646
|
+
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
2647
|
});
|
|
2346
2648
|
droppedIds.add(p.id);
|
|
2347
2649
|
continue;
|
|
@@ -2458,7 +2760,7 @@ export class MemoryEngine {
|
|
|
2458
2760
|
}
|
|
2459
2761
|
}
|
|
2460
2762
|
report.ok = false;
|
|
2461
|
-
report.incident = { kind: "sidecar_corrupt", detail: `memory commit refused: ${
|
|
2763
|
+
report.incident = { kind: "sidecar_corrupt", detail: `memory commit refused: ${this.writeFailureText(err)}` };
|
|
2462
2764
|
return report;
|
|
2463
2765
|
}
|
|
2464
2766
|
if (lineageArmed) {
|
|
@@ -2514,18 +2816,25 @@ export class MemoryEngine {
|
|
|
2514
2816
|
}
|
|
2515
2817
|
}
|
|
2516
2818
|
const appliedIds = new Set(patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id));
|
|
2819
|
+
const planeAfterFailedWriteBack = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir)
|
|
2820
|
+
? "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"
|
|
2821
|
+
: "the next materialize re-projects the committed copy over them";
|
|
2517
2822
|
for (const p of pendingProjections) {
|
|
2518
2823
|
const pRel = relative(handle.memoryDir, p.path);
|
|
2519
2824
|
const pSeat = seatOf(p.path);
|
|
2520
2825
|
if (appliedIds.has(p.entry.id)) {
|
|
2521
|
-
const
|
|
2522
|
-
if (p.idCompletion
|
|
2523
|
-
if (
|
|
2826
|
+
const writeBackFailure = this.writeBackProjection(p.path, p.entry, p.needed);
|
|
2827
|
+
if (p.idCompletion !== true) {
|
|
2828
|
+
if (writeBackFailure !== undefined)
|
|
2829
|
+
report.warnings.push(`${pRel}: the entry committed but its projection write-back FAILED — the plane keeps this session's bytes (${planeAfterFailedWriteBack}): ${writeBackFailure}`);
|
|
2830
|
+
}
|
|
2831
|
+
else {
|
|
2832
|
+
if (writeBackFailure === undefined)
|
|
2524
2833
|
debtClears.push({ ...pSeat, entryId: p.entry.id });
|
|
2525
2834
|
else {
|
|
2526
2835
|
const landed = patchReport.applied.find((a) => a.id === p.entry.id)?.slug;
|
|
2527
2836
|
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}`);
|
|
2837
|
+
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
2838
|
}
|
|
2530
2839
|
}
|
|
2531
2840
|
continue;
|
|
@@ -2538,7 +2847,8 @@ export class MemoryEngine {
|
|
|
2538
2847
|
writeFileNoFollow(p.path, committed);
|
|
2539
2848
|
planeHoldsCommitted = true;
|
|
2540
2849
|
}
|
|
2541
|
-
catch {
|
|
2850
|
+
catch (err) {
|
|
2851
|
+
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
2852
|
}
|
|
2543
2853
|
}
|
|
2544
2854
|
else if (committed !== undefined) {
|
|
@@ -2587,15 +2897,17 @@ export class MemoryEngine {
|
|
|
2587
2897
|
}
|
|
2588
2898
|
const headers = await this.backend.listHeaders([...new Set([...handle.scopes, writeScope])]);
|
|
2589
2899
|
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)));
|
|
2900
|
+
await this.rebaseline(handle, new Set([...(report.degraded?.pending ?? []), ...stoodAsFound]), new Set(report.rejections.map((r) => r.path)));
|
|
2591
2901
|
if (carry && lineageSessionId !== undefined && report.degraded === undefined) {
|
|
2592
2902
|
const deferredSeats = [...new Set(report.rejections.filter((r) => r.code === "deferred").map((r) => r.path))];
|
|
2593
2903
|
const shown = deferredSeats.slice(0, MAX_DISCLOSED_DEFERRED_SEATS);
|
|
2594
2904
|
const rest = deferredSeats.length - shown.length;
|
|
2595
2905
|
const seatList = `${shown.join(", ")}${rest > 0 ? ` (+${rest} more — see the rejections)` : ""}`;
|
|
2596
2906
|
try {
|
|
2597
|
-
const closed = closeSessionAccount(this.controlDir, { sessionId: lineageSessionId, now: this.now });
|
|
2598
|
-
if (closed &&
|
|
2907
|
+
const closed = handle.accountLeg !== undefined ? closeSessionAccount(this.controlDir, { sessionId: lineageSessionId, now: this.now, leg: handle.accountLeg }) : "absent";
|
|
2908
|
+
if (closed === "stale" && !report.warnings.includes(STALE_LEG_NOTE))
|
|
2909
|
+
report.warnings.push(STALE_LEG_NOTE);
|
|
2910
|
+
if (closed === "written" && deferredSeats.length > 0) {
|
|
2599
2911
|
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
2912
|
}
|
|
2601
2913
|
}
|
|
@@ -4288,6 +4600,8 @@ export class MemoryEngine {
|
|
|
4288
4600
|
const id = parsed.id ?? handle.baseIds.get(path);
|
|
4289
4601
|
if (id === undefined)
|
|
4290
4602
|
continue;
|
|
4603
|
+
if (uncommitted && handle.baseIds.get(path) === undefined)
|
|
4604
|
+
continue;
|
|
4291
4605
|
const stub = stubs.get(path);
|
|
4292
4606
|
const priorRev = deferred ? handle.baseRevs.get(path) : undefined;
|
|
4293
4607
|
const priorId = deferred ? handle.baseIds.get(path) : undefined;
|
|
@@ -4509,8 +4823,9 @@ export class MemoryEngine {
|
|
|
4509
4823
|
writeFileNoFollow(indexPath, text);
|
|
4510
4824
|
wrote = true;
|
|
4511
4825
|
}
|
|
4512
|
-
catch {
|
|
4826
|
+
catch (err) {
|
|
4513
4827
|
handle.indexOnDiskUntrusted = true;
|
|
4828
|
+
warnings?.push(`${INDEX_WRITE_REFUSED}: ${this.writeFailureText(err)}`);
|
|
4514
4829
|
}
|
|
4515
4830
|
if (wrote)
|
|
4516
4831
|
writeIndexRevs(this.controlDir, nextIndexRevs);
|
|
@@ -4597,7 +4912,7 @@ export class MemoryEngine {
|
|
|
4597
4912
|
cleared = true;
|
|
4598
4913
|
}
|
|
4599
4914
|
catch (err) {
|
|
4600
|
-
detail = `${detail}; the derived index could NOT be cleared: ${
|
|
4915
|
+
detail = `${detail}; the derived index could NOT be cleared: ${this.writeFailureText(err)}`;
|
|
4601
4916
|
}
|
|
4602
4917
|
const outcome = [
|
|
4603
4918
|
captured ? "the offending prose was captured to quarantine" : "the quarantine capture FAILED — the prose was not preserved",
|
|
@@ -4621,19 +4936,19 @@ export class MemoryEngine {
|
|
|
4621
4936
|
}
|
|
4622
4937
|
writeBackProjection(path, entry, needed) {
|
|
4623
4938
|
if (!needed)
|
|
4624
|
-
return
|
|
4939
|
+
return undefined;
|
|
4625
4940
|
const text = serializeEntryFile(entry);
|
|
4626
4941
|
if (readNoFollowSafe(path) === text)
|
|
4627
|
-
return
|
|
4942
|
+
return undefined;
|
|
4628
4943
|
try {
|
|
4629
4944
|
writeFileNoFollow(path, text);
|
|
4630
|
-
return
|
|
4945
|
+
return undefined;
|
|
4631
4946
|
}
|
|
4632
|
-
catch {
|
|
4633
|
-
return
|
|
4947
|
+
catch (err) {
|
|
4948
|
+
return this.writeFailureText(err);
|
|
4634
4949
|
}
|
|
4635
4950
|
}
|
|
4636
|
-
async repairProjectionSeat(seat, canonical, entryId, debtClears) {
|
|
4951
|
+
async repairProjectionSeat(seat, canonical, entryId, debtClears, warnings) {
|
|
4637
4952
|
const committed = await this.committedContentFor(entryId);
|
|
4638
4953
|
if (committed === undefined)
|
|
4639
4954
|
return;
|
|
@@ -4641,7 +4956,8 @@ export class MemoryEngine {
|
|
|
4641
4956
|
try {
|
|
4642
4957
|
writeFileNoFollow(canonical, committed);
|
|
4643
4958
|
}
|
|
4644
|
-
catch {
|
|
4959
|
+
catch (err) {
|
|
4960
|
+
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
4961
|
return;
|
|
4646
4962
|
}
|
|
4647
4963
|
}
|
|
@@ -4662,18 +4978,18 @@ export class MemoryEngine {
|
|
|
4662
4978
|
return { state: "unknown" };
|
|
4663
4979
|
}
|
|
4664
4980
|
}
|
|
4665
|
-
|
|
4981
|
+
censusScopeTree(dir) {
|
|
4666
4982
|
if (!existsSync(dir))
|
|
4667
4983
|
return;
|
|
4668
|
-
const
|
|
4669
|
-
const
|
|
4984
|
+
const unreadable = [];
|
|
4985
|
+
const links = [];
|
|
4670
4986
|
const walk = (d) => {
|
|
4671
4987
|
let names = [];
|
|
4672
4988
|
try {
|
|
4673
4989
|
names = readdirSync(d);
|
|
4674
4990
|
}
|
|
4675
4991
|
catch {
|
|
4676
|
-
|
|
4992
|
+
unreadable.push(d);
|
|
4677
4993
|
return;
|
|
4678
4994
|
}
|
|
4679
4995
|
for (const name of names) {
|
|
@@ -4683,50 +4999,34 @@ export class MemoryEngine {
|
|
|
4683
4999
|
st = lstatSync(p);
|
|
4684
5000
|
}
|
|
4685
5001
|
catch {
|
|
4686
|
-
|
|
4687
|
-
continue;
|
|
4688
|
-
}
|
|
4689
|
-
if (st.isSymbolicLink()) {
|
|
4690
|
-
sweptLinks.push(p);
|
|
5002
|
+
unreadable.push(p);
|
|
4691
5003
|
continue;
|
|
4692
5004
|
}
|
|
4693
|
-
if (st.
|
|
5005
|
+
if (st.isSymbolicLink())
|
|
5006
|
+
links.push(p);
|
|
5007
|
+
else if (st.isDirectory())
|
|
4694
5008
|
walk(p);
|
|
4695
|
-
else if (st.isFile() && name.endsWith(".md") && name !== MEMORY_INDEX_FILENAME)
|
|
4696
|
-
chmodNoFollow(p, fileMode);
|
|
4697
5009
|
}
|
|
4698
|
-
chmodNoFollow(d, dirMode);
|
|
4699
5010
|
};
|
|
4700
5011
|
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
|
-
}
|
|
5012
|
+
if (unreadable.length + links.length === 0)
|
|
5013
|
+
return;
|
|
5014
|
+
try {
|
|
5015
|
+
enqueueMemoryAnnouncement(this.controlDir, {
|
|
5016
|
+
kind: "gate",
|
|
5017
|
+
at: this.now(),
|
|
5018
|
+
items: unreadable
|
|
5019
|
+
.slice(0, 12)
|
|
5020
|
+
.map((p) => `layer census could not read ${JSON.stringify(inlineUntrusted(relative(dir, p) || "."))} — nothing under it was examined`)
|
|
5021
|
+
.concat(unreadable.length > 12 ? [`…and ${unreadable.length - 12} more unreadable path(s)`] : [])
|
|
5022
|
+
.concat(links
|
|
5023
|
+
.slice(0, 12)
|
|
5024
|
+
.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`))
|
|
5025
|
+
.concat(links.length > 12 ? [`…and ${links.length - 12} more skipped symlink(s)`] : []),
|
|
5026
|
+
});
|
|
5027
|
+
}
|
|
5028
|
+
catch (err) {
|
|
5029
|
+
this.discloseAnnounceFailure("layer-census enqueue", err);
|
|
4730
5030
|
}
|
|
4731
5031
|
}
|
|
4732
5032
|
}
|
|
@@ -4813,16 +5113,6 @@ function readSafe(path) {
|
|
|
4813
5113
|
return undefined;
|
|
4814
5114
|
}
|
|
4815
5115
|
}
|
|
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
5116
|
function readNoFollowSafe(path) {
|
|
4827
5117
|
let fd;
|
|
4828
5118
|
try {
|
|
@@ -4837,19 +5127,6 @@ function readNoFollowSafe(path) {
|
|
|
4837
5127
|
closeSync(fd);
|
|
4838
5128
|
}
|
|
4839
5129
|
}
|
|
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
5130
|
function needsCompletion(parsed, fm) {
|
|
4854
5131
|
return (!parsed.hadFrontmatter ||
|
|
4855
5132
|
parsed.frontmatter.name !== fm.name ||
|