@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { HarvestRejection, MemorySessionHandle } from "./types.js";
|
|
2
|
+
export declare const LAYER_OBSERVATIONS_FILE = "layer-observations.json";
|
|
3
|
+
/** One recorded unexpected write into a read-only layer. */
|
|
4
|
+
export interface LayerObservation {
|
|
5
|
+
/** The layer (scope) the bytes were observed in. */
|
|
6
|
+
scope: string;
|
|
7
|
+
/** Scope-home-relative, `/`-separated path the bytes were observed at (disclosure only). */
|
|
8
|
+
rel: string;
|
|
9
|
+
/** The entry id the observed file carries; absent ⇔ an id-less note. */
|
|
10
|
+
entryId?: string;
|
|
11
|
+
/** The entry's committed rev when observed; absent ⇔ the id had no committed row. */
|
|
12
|
+
rev?: string;
|
|
13
|
+
/** The observed content's identity: its entry rev (what the read side computes from the file), or
|
|
14
|
+
* for an id-less note the sha256 of its bytes. */
|
|
15
|
+
observedRev: string;
|
|
16
|
+
/** The observing session, when the harvest was given one. */
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
observedAt: number;
|
|
19
|
+
/** Whether a commit backs these bytes could not be established when they were observed (a
|
|
20
|
+
* transaction journal was pending — the committed account could not be read whole). The row — one
|
|
21
|
+
* per observation — then stands against ANY committed rev (never adopted) until the first session
|
|
22
|
+
* that may write settles it, once its preflight has recovered the journal
|
|
23
|
+
* ({@link settleUndecidedLayerObservations}): the committed account backs its bytes at its seat ⇒
|
|
24
|
+
* this observation is removed (the bytes are committed content); it does not ⇒ pinned to the
|
|
25
|
+
* committed rev read, an ordinary row from then on, retired when that rev moves (superseded, as
|
|
26
|
+
* every ordinary row is); the account still cannot be read ⇒ it stays undecided. Only id-bearing
|
|
27
|
+
* rows (a note has no commit question) and never with `rev`. */
|
|
28
|
+
undecided?: true;
|
|
29
|
+
/** An undecided row's own identity (random, minted with the row): two observations alike in every
|
|
30
|
+
* other field — the same session in the same millisecond — stay two rows. Undecided rows only. */
|
|
31
|
+
observationId?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Record observations (locked, journaled; an existing key keeps its first row). Nothing is pruned
|
|
34
|
+
* here: the rev a harvest read before this lock need not be current any more, so it cannot decide
|
|
35
|
+
* which other rows are stale — and a row against a rev the entry has left can never match anyway. */
|
|
36
|
+
export declare function recordLayerObservations(controlDir: string, rows: readonly LayerObservation[]): void;
|
|
37
|
+
/** The table as a read-side lookup. */
|
|
38
|
+
export interface LayerObservationLookup {
|
|
39
|
+
/** Whether any row names `entryId` (a cheap pre-test before the committed rev is fetched). */
|
|
40
|
+
covers(entryId: string): boolean;
|
|
41
|
+
/** The row naming exactly this content (`observedRev`) of `entryId` against its committed rev
|
|
42
|
+
* `rev` (absent ⇔ the id has no committed row) — or an undecided row for that content, which stands
|
|
43
|
+
* against any rev until it is settled — else undefined. */
|
|
44
|
+
find(entryId: string, rev: string | undefined, observedRev: string): LayerObservation | undefined;
|
|
45
|
+
/** The row naming exactly these bytes (`digest`) of an id-less note at (`scope`, `rel`), or undefined. */
|
|
46
|
+
findNote(scope: string, rel: string, digest: string): LayerObservation | undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Read the table (throws the strict family's refusal on a corrupt table). */
|
|
49
|
+
export declare function layerObservationLookup(controlDir: string): LayerObservationLookup;
|
|
50
|
+
/** The disclosure line for a recorded divergence met on the adopting read (one spelling, every
|
|
51
|
+
* door). The verdict leads: the render boundary caps an item at a label's length. */
|
|
52
|
+
export declare function observedDivergenceNotice(rel: string, served: boolean, undecided?: boolean): string;
|
|
53
|
+
/** The undecided rows ({@link LayerObservation.undecided}) awaiting settlement. */
|
|
54
|
+
export declare function undecidedLayerObservations(controlDir: string): LayerObservation[];
|
|
55
|
+
/** Settle undecided rows (locked, journaled), each by its own key (one observation — never another
|
|
56
|
+
* observation of the same entry): `"backed"` removes the row (the committed account backs its bytes
|
|
57
|
+
* at its seat: those bytes are committed content, so no unexpected write is lost), `{ rev }` pins
|
|
58
|
+
* it to the committed rev the settlement read (`rev` absent ⇔ no committed row) — an ordinary row,
|
|
59
|
+
* retired like any other when that rev moves. A row without a verdict stays undecided. */
|
|
60
|
+
export declare function settleUndecidedLayerObservations(controlDir: string, verdicts: ReadonlyMap<LayerObservation, "backed" | {
|
|
61
|
+
rev?: string;
|
|
62
|
+
}>): void;
|
|
63
|
+
/** The census digest: whole-file bytes (sha256), so "the bytes that stood when the window opened"
|
|
64
|
+
* means the bytes. */
|
|
65
|
+
export declare function layerDigest(text: string): string;
|
|
66
|
+
/** The read-only layers' census (materialize, zero-copy): canonical path → {@link layerDigest} of
|
|
67
|
+
* every entry file under `homes`, scanned at the read side's depth (what the adopting read sees). */
|
|
68
|
+
export declare function readOnlyLayerCensus(homes: readonly string[]): Map<string, string>;
|
|
69
|
+
/** A file of a read-only layer whose bytes changed since the materialize-time census. */
|
|
70
|
+
export interface ReadOnlyObservationCandidate {
|
|
71
|
+
path: string;
|
|
72
|
+
scope: string;
|
|
73
|
+
rel: string;
|
|
74
|
+
slug: string;
|
|
75
|
+
digest: string;
|
|
76
|
+
id?: string;
|
|
77
|
+
/** The entry rev of the bytes, or their digest for an id-less note. */
|
|
78
|
+
observedRev: string;
|
|
79
|
+
}
|
|
80
|
+
/** The observation half's candidates: files of the layers `handle` holds read-only whose bytes differ
|
|
81
|
+
* from its materialize-time census (none without a census). Throws when the scope registry cannot
|
|
82
|
+
* be read (the homes are resolved through it). */
|
|
83
|
+
export declare function readOnlyLayerCandidates(memoryDir: string, controlDir: string, handle: Pick<MemorySessionHandle, "layerBaseline" | "scopes" | "writeScope">): ReadOnlyObservationCandidate[];
|
|
84
|
+
/** What a harvest's observation half judged at detection: the rows (written AS JUDGED — a retry writes
|
|
85
|
+
* the same rows, never a re-judgment against a later account), the rejections and undecided
|
|
86
|
+
* candidates they carry, whether they landed, and the failure of the last attempt that did not. */
|
|
87
|
+
export interface ObservationBatch {
|
|
88
|
+
rows: LayerObservation[];
|
|
89
|
+
rejections: HarvestRejection[];
|
|
90
|
+
/** Each undecided candidate with the row it was recorded as (its observation identity). */
|
|
91
|
+
undecided: Array<{
|
|
92
|
+
candidate: ReadOnlyObservationCandidate;
|
|
93
|
+
row: LayerObservation;
|
|
94
|
+
}>;
|
|
95
|
+
landed: boolean;
|
|
96
|
+
failure?: string;
|
|
97
|
+
}
|
|
98
|
+
/** The undecided candidates a LANDED batch recorded (an unlanded one claims none). */
|
|
99
|
+
export declare function landedUndecided(batch: ObservationBatch | undefined): ObservationBatch["undecided"];
|
|
100
|
+
/** Whether two rows are the same observation (the table's key — for an undecided row, one
|
|
101
|
+
* observation: session and time included). */
|
|
102
|
+
export declare function sameObservation(a: LayerObservation, b: LayerObservation): boolean;
|
|
103
|
+
/** Whether the committed row backs an observation's bytes at its seat (same rev, bound there) — the
|
|
104
|
+
* observation was a commit, not an unexpected write. */
|
|
105
|
+
export declare function observationBacked(row: Pick<LayerObservation, "scope" | "rel" | "observedRev">, committed: {
|
|
106
|
+
rev: string;
|
|
107
|
+
scope?: string;
|
|
108
|
+
slug?: string;
|
|
109
|
+
} | undefined): boolean;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { relative, sep } from "node:path";
|
|
4
|
+
import { inlineUntrusted } from "../untrusted-text.js";
|
|
5
|
+
import { scanEntryFiles } from "./entry-scan.js";
|
|
6
|
+
import { entryFromFile, parseEntryFile } from "./frontmatter.js";
|
|
7
|
+
import { ControlPlaneCorruptError, canonicalize, lockedStrictUpdate, readStrictSidecar, scopeDirFor } from "./layout.js";
|
|
8
|
+
export const LAYER_OBSERVATIONS_FILE = "layer-observations.json";
|
|
9
|
+
const WHAT = "memory layer-observation table";
|
|
10
|
+
const str = (v) => typeof v === "string" && v.length > 0;
|
|
11
|
+
function coerce(raw) {
|
|
12
|
+
if (raw === undefined)
|
|
13
|
+
return { version: 1, rows: [] };
|
|
14
|
+
const rec = raw;
|
|
15
|
+
if (typeof rec !== "object" || rec === null || rec.version !== 1 || !Array.isArray(rec.rows))
|
|
16
|
+
throw new ControlPlaneCorruptError(`${WHAT} has the wrong shape or an unrecognized version — refusing (fail-closed)`);
|
|
17
|
+
const rows = rec.rows;
|
|
18
|
+
for (const r of rows) {
|
|
19
|
+
const row = r;
|
|
20
|
+
if (typeof row !== "object" || row === null || !str(row.scope) || !str(row.rel) || (row.entryId !== undefined && !str(row.entryId)) || !str(row.observedRev) || (row.rev !== undefined && (row.entryId === undefined || !str(row.rev))) || (row.sessionId !== undefined && !str(row.sessionId)) || typeof row.observedAt !== "number" || !Number.isFinite(row.observedAt) || (row.undecided !== undefined && (row.undecided !== true || row.entryId === undefined || row.rev !== undefined)) || (row.observationId !== undefined && (!str(row.observationId) || row.undecided !== true))) {
|
|
21
|
+
throw new ControlPlaneCorruptError(`${WHAT}: a row is malformed — refusing (fail-closed; an unevaluable row must not read as "no unexpected write")`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return rec;
|
|
25
|
+
}
|
|
26
|
+
const keyOf = (o) => o.entryId === undefined
|
|
27
|
+
? JSON.stringify([null, o.scope, o.rel, o.observedRev])
|
|
28
|
+
: o.undecided === true
|
|
29
|
+
? JSON.stringify([o.entryId, { undecided: true }, o.observedRev, o.scope, o.rel, o.sessionId ?? null, o.observedAt, o.observationId ?? null])
|
|
30
|
+
: JSON.stringify([o.entryId, o.rev ?? null, o.observedRev]);
|
|
31
|
+
const contentKey = (entryId, observedRev) => JSON.stringify([entryId, observedRev]);
|
|
32
|
+
export function recordLayerObservations(controlDir, rows) {
|
|
33
|
+
if (rows.length === 0)
|
|
34
|
+
return;
|
|
35
|
+
lockedStrictUpdate(controlDir, LAYER_OBSERVATIONS_FILE, WHAT, coerce, (rec) => {
|
|
36
|
+
const byKey = new Map(rec.rows.map((r) => [keyOf(r), r]));
|
|
37
|
+
for (const r of rows)
|
|
38
|
+
if (!byKey.has(keyOf(r)))
|
|
39
|
+
byKey.set(keyOf(r), { ...r });
|
|
40
|
+
return { next: { version: 1, rows: [...byKey.values()] }, result: undefined };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function layerObservationLookup(controlDir) {
|
|
44
|
+
const rows = coerce(readStrictSidecar(controlDir, LAYER_OBSERVATIONS_FILE, WHAT)).rows;
|
|
45
|
+
const ids = new Set(rows.flatMap((r) => (r.entryId !== undefined ? [r.entryId] : [])));
|
|
46
|
+
const byKey = new Map(rows.map((r) => [keyOf(r), r]));
|
|
47
|
+
const undecidedByContent = new Map(rows.flatMap((r) => (r.undecided === true && r.entryId !== undefined ? [[contentKey(r.entryId, r.observedRev), r]] : [])));
|
|
48
|
+
return {
|
|
49
|
+
covers: (entryId) => ids.has(entryId),
|
|
50
|
+
find: (entryId, rev, observedRev) => ids.has(entryId) ? (byKey.get(keyOf({ scope: "", rel: "", entryId, rev, observedRev })) ?? undecidedByContent.get(contentKey(entryId, observedRev))) : undefined,
|
|
51
|
+
findNote: (scope, rel, digest) => byKey.get(keyOf({ scope, rel, observedRev: digest })),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function observedDivergenceNotice(rel, served, undecided = false) {
|
|
55
|
+
const file = JSON.stringify(inlineUntrusted(rel));
|
|
56
|
+
if (undecided)
|
|
57
|
+
return `change NOT adopted yet: ${file} changed while a session held its layer read-only, and whether a commit backs it could not be established — re-judged once a writing session can read the committed account`;
|
|
58
|
+
return served
|
|
59
|
+
? `change NOT adopted: ${file} was written while a session held its layer read-only — memory serves the committed version, not these bytes`
|
|
60
|
+
: `file NOT adopted: ${file} appeared while a session held its layer read-only — it is not memory; left on disk for review`;
|
|
61
|
+
}
|
|
62
|
+
export function undecidedLayerObservations(controlDir) {
|
|
63
|
+
return coerce(readStrictSidecar(controlDir, LAYER_OBSERVATIONS_FILE, WHAT)).rows.filter((r) => r.undecided === true);
|
|
64
|
+
}
|
|
65
|
+
function ordinaryRow(r, rev) {
|
|
66
|
+
return {
|
|
67
|
+
scope: r.scope,
|
|
68
|
+
rel: r.rel,
|
|
69
|
+
...(r.entryId !== undefined ? { entryId: r.entryId } : {}),
|
|
70
|
+
...(rev !== undefined ? { rev } : {}),
|
|
71
|
+
observedRev: r.observedRev,
|
|
72
|
+
...(r.sessionId !== undefined ? { sessionId: r.sessionId } : {}),
|
|
73
|
+
observedAt: r.observedAt,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function settleUndecidedLayerObservations(controlDir, verdicts) {
|
|
77
|
+
if (verdicts.size === 0)
|
|
78
|
+
return;
|
|
79
|
+
const byKey = new Map([...verdicts].map(([row, v]) => [keyOf(row), v]));
|
|
80
|
+
lockedStrictUpdate(controlDir, LAYER_OBSERVATIONS_FILE, WHAT, coerce, (rec) => {
|
|
81
|
+
const next = new Map();
|
|
82
|
+
for (const r of rec.rows) {
|
|
83
|
+
const v = r.undecided === true ? byKey.get(keyOf(r)) : undefined;
|
|
84
|
+
if (v === "backed")
|
|
85
|
+
continue;
|
|
86
|
+
const settled = v === undefined ? r : ordinaryRow(r, v.rev);
|
|
87
|
+
if (!next.has(keyOf(settled)))
|
|
88
|
+
next.set(keyOf(settled), settled);
|
|
89
|
+
}
|
|
90
|
+
return { next: { version: 1, rows: [...next.values()] }, result: undefined };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function layerDigest(text) {
|
|
94
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
95
|
+
}
|
|
96
|
+
function readText(path) {
|
|
97
|
+
try {
|
|
98
|
+
return readFileSync(path, "utf8");
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function readOnlyLayerCensus(homes) {
|
|
105
|
+
const files = new Map();
|
|
106
|
+
for (const home of homes) {
|
|
107
|
+
for (const f of scanEntryFiles(home)) {
|
|
108
|
+
const path = canonicalize(f.path);
|
|
109
|
+
const text = readText(path);
|
|
110
|
+
if (text !== undefined)
|
|
111
|
+
files.set(path, layerDigest(text));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return files;
|
|
115
|
+
}
|
|
116
|
+
export function readOnlyLayerCandidates(memoryDir, controlDir, handle) {
|
|
117
|
+
const census = handle.layerBaseline;
|
|
118
|
+
if (census === undefined)
|
|
119
|
+
return [];
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const scope of new Set(handle.scopes)) {
|
|
122
|
+
if (scope === handle.writeScope)
|
|
123
|
+
continue;
|
|
124
|
+
const home = canonicalize(scopeDirFor(memoryDir, controlDir, scope));
|
|
125
|
+
for (const f of scanEntryFiles(home)) {
|
|
126
|
+
const path = canonicalize(f.path);
|
|
127
|
+
const text = readText(path);
|
|
128
|
+
if (text === undefined || (path !== home && !path.startsWith(`${home}${sep}`)))
|
|
129
|
+
continue;
|
|
130
|
+
const digest = layerDigest(text);
|
|
131
|
+
if (census.get(path) === digest)
|
|
132
|
+
continue;
|
|
133
|
+
const id = parseEntryFile(text).id;
|
|
134
|
+
const rel = relative(home, path).split(sep).join("/");
|
|
135
|
+
out.push({ path, scope, rel, slug: f.slug, digest, ...(id !== undefined ? { id, observedRev: entryFromFile(text, id, f.slug, scope).rev } : { observedRev: digest }) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
export function landedUndecided(batch) {
|
|
141
|
+
return batch?.landed === true ? batch.undecided : [];
|
|
142
|
+
}
|
|
143
|
+
export function sameObservation(a, b) {
|
|
144
|
+
return keyOf(a) === keyOf(b);
|
|
145
|
+
}
|
|
146
|
+
export function observationBacked(row, committed) {
|
|
147
|
+
return committed !== undefined && committed.scope === row.scope && committed.slug === row.rel.replace(/\.md$/, "") && committed.rev === row.observedRev;
|
|
148
|
+
}
|
|
@@ -259,8 +259,41 @@ export declare function adoptRootScopeLayout(rootPath: string, controlDir: strin
|
|
|
259
259
|
export declare const canonicalize: typeof physicalPathOfExisting;
|
|
260
260
|
/** True iff `child` (canonicalized) is `root` or inside it. */
|
|
261
261
|
export declare function isContainedIn(root: string, child: string): boolean;
|
|
262
|
+
/** The code an act on a store path is refused with when it would land outside the store ({@link storeTargetEscape}). */
|
|
263
|
+
export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
|
|
264
|
+
/** Where an act at `p` lands: the physical path of its normalized spelling (the spelling a child path
|
|
265
|
+
* joined under it carries), a MISSING tail rejoined under the deepest existing directory (what the act
|
|
266
|
+
* creates). Any other resolution fault (EACCES, ELOOP, ENOTDIR) is THROWN as itself: an act that
|
|
267
|
+
* cannot be placed is not performed — a failed resolution never implies the act would fail too
|
|
268
|
+
* (a link whose own mode refuses `realpath` is still followed by `unlink` and `rename`). */
|
|
269
|
+
export declare function landingPathOf(p: string): string;
|
|
270
|
+
/**
|
|
271
|
+
* Where a write, rename or delete at `target` would physically LAND, judged against the store's
|
|
272
|
+
* `physicalRoot`: `undefined` = inside, otherwise the reason. The target's directory, resolved through
|
|
273
|
+
* every link on its existing prefix (a missing tail is what the act creates, under the deepest existing
|
|
274
|
+
* directory), must be the root or inside it. `physicalRoot` is the root's physical form as the store
|
|
275
|
+
* was opened and is not re-resolved here: a store mounted through a link is judged by what the link
|
|
276
|
+
* resolved to, and a root spelling re-pointed after opening moves the acts, not the store. The final
|
|
277
|
+
* component is not judged: `rename` and `unlink` act on it and never through it, and the engine's
|
|
278
|
+
* writes open it no-follow — a link there is replaced, removed or refused, and what it points at is
|
|
279
|
+
* untouched. A lexical spelling under the root proves nothing: a directory under it replaced by a link
|
|
280
|
+
* keeps the spelling while the act follows the link out. A directory that cannot be resolved throws its
|
|
281
|
+
* fault ({@link landingPathOf}).
|
|
282
|
+
*/
|
|
283
|
+
export declare function storeTargetEscape(physicalRoot: string, target: string): string | undefined;
|
|
262
284
|
/** mkdir -p convenience used by the engine/backend (0o700 like the file stores). */
|
|
263
285
|
export declare function ensureDirExists(dir: string): void;
|
|
286
|
+
/** The one remedy every surfaced permission refusal carries — `undefined` unless `err` is a write
|
|
287
|
+
* the file system refused on PERMISSIONS (`EACCES` / `EPERM`) at a path INSIDE `memoryDir` (a
|
|
288
|
+
* refusal elsewhere — a control plane kept outside the store — is not this store's to fix). The
|
|
289
|
+
* engine never changes permission bits on a store — a session's read-only layers are its policy,
|
|
290
|
+
* not file-system state — so bits that refuse a write were set by someone else, and nothing here
|
|
291
|
+
* clears them (an engine from 7.30.x or earlier left inherited layers read-only on disk). */
|
|
292
|
+
export declare function permissionRemedyOf(err: unknown, memoryDir: string): string | undefined;
|
|
293
|
+
/** Attach {@link permissionRemedyOf} to a permission refusal inside `memoryDir` — in place, so the
|
|
294
|
+
* error keeps its class and `code` (callers branch on both), and once (an inner seat that already
|
|
295
|
+
* named its store wins over an outer one). Any other value is returned untouched. */
|
|
296
|
+
export declare function withPermissionRemedy<T>(err: T, memoryDir: string): T;
|
|
264
297
|
/** B2/L4 — the control-plane subdir holding gate-rejected originals. ONE name for all three writers
|
|
265
298
|
* (engine harvest, engine derived-index rung, backend inbound gate): they write the same directory,
|
|
266
299
|
* so its name and its permissions must not depend on which of them ran first. */
|
|
@@ -313,10 +346,13 @@ export interface QuarantineOutcome {
|
|
|
313
346
|
* without native bindings. What this buys is the leaf variant, which is the cheap one; the ancestor
|
|
314
347
|
* variant stays open and is recorded with the rest of the family rather than papered over.
|
|
315
348
|
*
|
|
316
|
-
* Throws like `writeFileSync`, including ELOOP when the final component IS a symlink.
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
349
|
+
* Throws like `writeFileSync`, including ELOOP when the final component IS a symlink. A HARD link is the
|
|
350
|
+
* same tamper by other means — the file's bytes live under a second name, possibly outside the store, and
|
|
351
|
+
* an in-place write would change them there too — so an opened file with more than one name is refused
|
|
352
|
+
* the same way, before anything is truncated (the check reads the opened descriptor: no window). The
|
|
353
|
+
* write itself goes through `writeFileSync` on the open descriptor, which writes every byte or throws —
|
|
354
|
+
* a bare `writeSync` may short-write, and reporting a truncated restore as contained (after the
|
|
355
|
+
* truncation already destroyed the destination) is the failure this guard exists to prevent.
|
|
320
356
|
*/
|
|
321
357
|
export declare function writeFileNoFollow(path: string, content: string): void;
|
|
322
358
|
/** {@link writeFileNoFollow}'s NO-REPLACE twin: write `content` only if nothing is at `path`. `"written"`
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const { O_WRONLY, O_RDONLY, O_CREAT,
|
|
1
|
+
import { closeSync, constants as fsConstants, copyFileSync, existsSync, fstatSync, fsyncSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync, writeSync } from "node:fs";
|
|
2
|
+
const { O_WRONLY, O_RDONLY, O_CREAT, O_NOFOLLOW, O_NONBLOCK, O_EXCL } = fsConstants;
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
6
6
|
import { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
7
|
-
import { physicalPathOfExisting, priorCallerCasedPathOf } from "../physical-path.js";
|
|
7
|
+
import { physicalPathOf, physicalPathOfExisting, priorCallerCasedPathOf } from "../physical-path.js";
|
|
8
8
|
import { thrownLabel, thrownText } from "../thrown-value.js";
|
|
9
9
|
const SCOPES_FILE = "scopes.json";
|
|
10
10
|
export const CURSORS_FILE = "cursors.json";
|
|
@@ -687,10 +687,8 @@ function moveLooseContent(root, controlDir, rec, owner, label, renew, sink, resu
|
|
|
687
687
|
const labels = new Set(Object.values(rec.scopes));
|
|
688
688
|
const { names, probes } = looseRootContent(root, labels, controlDir);
|
|
689
689
|
const home = join(root, label);
|
|
690
|
-
if (names.length > 0)
|
|
690
|
+
if (names.length > 0)
|
|
691
691
|
ensureScopeHome(home);
|
|
692
|
-
chmodSync(home, 0o755);
|
|
693
|
-
}
|
|
694
692
|
let moved = 0;
|
|
695
693
|
for (const name of names) {
|
|
696
694
|
renew();
|
|
@@ -726,15 +724,53 @@ export function isContainedIn(root, child) {
|
|
|
726
724
|
const c = canonicalize(child);
|
|
727
725
|
return c === r || c.startsWith(`${r}${sep}`);
|
|
728
726
|
}
|
|
727
|
+
export const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
|
|
728
|
+
export function landingPathOf(p) {
|
|
729
|
+
const normalized = resolve(p);
|
|
730
|
+
try {
|
|
731
|
+
return physicalPathOf(normalized);
|
|
732
|
+
}
|
|
733
|
+
catch (err) {
|
|
734
|
+
if (err.code !== "ENOENT")
|
|
735
|
+
throw err;
|
|
736
|
+
return canonicalize(normalized);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
export function storeTargetEscape(physicalRoot, target) {
|
|
740
|
+
const dir = landingPathOf(dirname(target));
|
|
741
|
+
return dir === physicalRoot || dir.startsWith(`${physicalRoot}${sep}`) ? undefined : `its directory resolves to ${dir}, outside the store root ${physicalRoot}`;
|
|
742
|
+
}
|
|
729
743
|
export function ensureDirExists(dir) {
|
|
730
744
|
if (!existsSync(dir))
|
|
731
745
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
732
746
|
}
|
|
747
|
+
export function permissionRemedyOf(err, memoryDir) {
|
|
748
|
+
const e = typeof err === "object" && err !== null ? err : undefined;
|
|
749
|
+
if (e === undefined || (e.code !== "EACCES" && e.code !== "EPERM") || typeof e.path !== "string" || !isContainedIn(memoryDir, e.path))
|
|
750
|
+
return undefined;
|
|
751
|
+
return `file permissions refused this memory write (the engine never changes permission bits) — if an engine from 7.30.x or earlier left read-only bits on this store, run \`chmod -R u+w ${shellQuoted(memoryDir)}\` once no such engine still runs on it (an older engine sets the bits again whenever it mounts a layer read-only)`;
|
|
752
|
+
}
|
|
753
|
+
function shellQuoted(s) {
|
|
754
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
755
|
+
}
|
|
756
|
+
const remedied = new WeakSet();
|
|
757
|
+
export function withPermissionRemedy(err, memoryDir) {
|
|
758
|
+
const remedy = permissionRemedyOf(err, memoryDir);
|
|
759
|
+
if (remedy === undefined || !(err instanceof Error) || remedied.has(err))
|
|
760
|
+
return err;
|
|
761
|
+
err.message = `${err.message} — ${remedy}`;
|
|
762
|
+
remedied.add(err);
|
|
763
|
+
return err;
|
|
764
|
+
}
|
|
733
765
|
export const QUARANTINE_DIR = "quarantine";
|
|
734
766
|
const DELETED_TOMBSTONE = "---\ndeleted: true\n---\n";
|
|
735
767
|
export function writeFileNoFollow(path, content) {
|
|
736
|
-
const fd = openSync(path, O_WRONLY | O_CREAT |
|
|
768
|
+
const fd = openSync(path, O_WRONLY | O_CREAT | O_NOFOLLOW, 0o644);
|
|
737
769
|
try {
|
|
770
|
+
const links = fstatSync(fd).nlink;
|
|
771
|
+
if (links > 1)
|
|
772
|
+
throw Object.assign(new Error(`${path} has ${links} names (a hard link) — a write through one name changes the file under the others; not written`), { path });
|
|
773
|
+
ftruncateSync(fd, 0);
|
|
738
774
|
writeFileSync(fd, content, "utf8");
|
|
739
775
|
}
|
|
740
776
|
finally {
|
|
@@ -391,7 +391,9 @@ export interface MaterializedFile {
|
|
|
391
391
|
rev: string;
|
|
392
392
|
/** True when this file is a budget stub (frontmatter+description only, body archived — §2.3). */
|
|
393
393
|
stub: boolean;
|
|
394
|
-
/** True when the file belongs to a read-only inherited layer (
|
|
394
|
+
/** True when the file belongs to a read-only inherited layer (read-only by this session's policy —
|
|
395
|
+
* its file mode is left as found; never harvested as a write, a change to it is a `readonly_layer`
|
|
396
|
+
* rejection and the committed copy is restored). */
|
|
395
397
|
readonly: boolean;
|
|
396
398
|
}
|
|
397
399
|
/** The write face's home, for a call site that has established a write face exists (`writeScope !==
|
|
@@ -500,6 +502,28 @@ export interface MemorySessionHandle {
|
|
|
500
502
|
files: Map<string, string>;
|
|
501
503
|
index?: string;
|
|
502
504
|
};
|
|
505
|
+
/**
|
|
506
|
+
* The read-only layers' census: every entry file of every layer this session holds read-only,
|
|
507
|
+
* canonical path → digest of its bytes, taken at materialize BEFORE the adopting read — what stood
|
|
508
|
+
* when this session's window opened. The harvest's observation half records, in the store's
|
|
509
|
+
* session-observation table, every id-bearing file of those layers whose bytes changed since and
|
|
510
|
+
* that no commit backs (the read side never adopts a recorded divergence); the detection half
|
|
511
|
+
* words a divergence that already stood as not this session's write. Absent ⇔ not a zero-copy
|
|
512
|
+
* mount (the observed plane is not the store the read side adopts from), or a handle minted
|
|
513
|
+
* before the field — nothing is recorded then.
|
|
514
|
+
*/
|
|
515
|
+
layerBaseline?: Map<string, string>;
|
|
516
|
+
/**
|
|
517
|
+
* The session-account LEG this mount opened (minted at materialize, stored on the account row).
|
|
518
|
+
* The harvest closes the account, and a terminal harvest (or a failed prepare's release) ends the
|
|
519
|
+
* leg, only while the row still carries this leg — a resume that re-opened the row makes this
|
|
520
|
+
* mount's close / end stale, and they are not written over the later leg. Absent ⇔ this mount
|
|
521
|
+
* opened no account (no session id, provenance "off", an adoption-restricted mount, an open that
|
|
522
|
+
* failed and is known not to have landed) — nothing of an account is written for it then. An open
|
|
523
|
+
* that failed after it landed, or whose landing could not be read back, keeps its leg (every write
|
|
524
|
+
* made with it is leg-checked).
|
|
525
|
+
*/
|
|
526
|
+
accountLeg?: string;
|
|
503
527
|
}
|
|
504
528
|
/** Stable rejection codes a harvest gate can produce (model-visible gate events — 镜头 I). */
|
|
505
529
|
export type HarvestRejectionCode = "outside_root" | "symlink" | "secret" | "injection" | "filename" | "too_large" | "file_cap" | "readonly_layer" | "stub_modified" | "nested_too_deep" | "quarantine_failed" | "unreadable" | "deferred" | "polluted" | "restricted_divergence" | "invalid";
|
|
@@ -539,7 +563,7 @@ export interface MemoryAnnouncement {
|
|
|
539
563
|
* #479's seat is that every face of one harvest names the same id, and a throat that minted its
|
|
540
564
|
* own would hand the same occurrence two values). Present on the harvest-tail enqueues (the gate
|
|
541
565
|
* items and the inbound-reject items); absent on every announcement with no report behind it
|
|
542
|
-
* (backend adoption notices,
|
|
566
|
+
* (backend adoption notices, layer-census notes, index-heal and index-write notes) and on a hand-built report that
|
|
543
567
|
* carries no id — omitted, never fabricated.
|
|
544
568
|
*
|
|
545
569
|
* STRUCTURE-ONLY, deliberately: {@link import("./engine.js").renderAnnouncements} does not read
|
|
@@ -13,7 +13,7 @@ import type { Hooks } from "./hooks.js";
|
|
|
13
13
|
import type { ToolExecuteContext } from "./tool-spec.js";
|
|
14
14
|
import type { TaskSpec } from "./types.js";
|
|
15
15
|
/** The fields of a spec the seat carries — one `Pick`, shared by the mint's input and the apply's target. */
|
|
16
|
-
export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "clientContext" | "onQuestion" | "hooks" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "memory" | "compaction" | "skillListingBudgetFraction" | "skillListingMaxDescChars">;
|
|
16
|
+
export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "clientContext" | "onQuestion" | "hooks" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "memory" | "compaction" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "fileHistory">;
|
|
17
17
|
/**
|
|
18
18
|
* The seat a run hands its children. Every member is the parent's own value, frozen at mint (arrays copied and
|
|
19
19
|
* frozen; the facts object copied). Members present iff the parent carries them — an absent member is "nothing to
|
|
@@ -43,6 +43,8 @@ export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "cli
|
|
|
43
43
|
* · `skillListingBudgetFraction` / `skillListingMaxDescChars` — the listing-budget knobs, the same shape for the
|
|
44
44
|
* same reason: a fraction the parent tuned is a fraction OF EACH LEG'S OWN window, so it travels as a number
|
|
45
45
|
* and every leg multiplies it by its own.
|
|
46
|
+
* · `fileHistory` — the request's file-history off switch (`"disabled"`, its only word), for the edits its delegates
|
|
47
|
+
* make on its behalf too. An ORDINARY member (fill law); with one word no child can author it back on.
|
|
46
48
|
*/
|
|
47
49
|
export interface ParentSpecSeat {
|
|
48
50
|
readonly principal?: string;
|
|
@@ -58,6 +60,7 @@ export interface ParentSpecSeat {
|
|
|
58
60
|
readonly compaction?: TaskSpec["compaction"];
|
|
59
61
|
readonly skillListingBudgetFraction?: number;
|
|
60
62
|
readonly skillListingMaxDescChars?: number;
|
|
63
|
+
readonly fileHistory?: "disabled";
|
|
61
64
|
}
|
|
62
65
|
/** Mint the seat off the parent's REBOUND spec and its frozen question face (spec over deps — the caller resolves it once,
|
|
63
66
|
* the same value the parent's own question routing reads). Absent fields stay absent; `oneShot` only when `true`. */
|
|
@@ -12,6 +12,7 @@ export function parentSpecSeatOf(spec, frozenOnQuestion) {
|
|
|
12
12
|
...(spec.compaction !== undefined ? { compaction: Object.freeze({ ...spec.compaction }) } : {}),
|
|
13
13
|
...(spec.skillListingBudgetFraction !== undefined ? { skillListingBudgetFraction: spec.skillListingBudgetFraction } : {}),
|
|
14
14
|
...(spec.skillListingMaxDescChars !== undefined ? { skillListingMaxDescChars: spec.skillListingMaxDescChars } : {}),
|
|
15
|
+
...(spec.fileHistory !== undefined ? { fileHistory: spec.fileHistory } : {}),
|
|
15
16
|
...(spec.memory?.enabled === false ? { memoryOff: true } : {}),
|
|
16
17
|
});
|
|
17
18
|
}
|
|
@@ -31,6 +32,7 @@ export function applyParentSpecSeat(child, seat) {
|
|
|
31
32
|
...fill("compaction", (seat.compaction !== undefined ? { ...seat.compaction } : undefined)),
|
|
32
33
|
...fill("skillListingBudgetFraction", seat.skillListingBudgetFraction),
|
|
33
34
|
...fill("skillListingMaxDescChars", seat.skillListingMaxDescChars),
|
|
35
|
+
...fill("fileHistory", seat.fileHistory),
|
|
34
36
|
...(seat.oneShot === true ? { oneShot: true } : {}),
|
|
35
37
|
...(seat.memoryPersistenceCapable === false ? { memoryPersistenceCapable: false } : fill("memoryPersistenceCapable", seat.memoryPersistenceCapable)),
|
|
36
38
|
...(seat.memoryOff === true ? { memory: memoryTurnedOff(child.memory) } : {}),
|
|
@@ -74,6 +74,7 @@ function syntheticCheckpoint(scope) {
|
|
|
74
74
|
preview: { probe: true, note: "park wiring self-check display stub" },
|
|
75
75
|
previewWithheld: "oversize",
|
|
76
76
|
requiresRealApproval: true,
|
|
77
|
+
mandated: true,
|
|
77
78
|
denialLimitFallback: { consecutive: 3, total: 3, limit: "consecutive", autoDenyAfterMs: 0 },
|
|
78
79
|
origin: "denial_limit_fallback",
|
|
79
80
|
batchToolCallIds: [`${scope}:call`],
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { AnnouncedListingSnapshot, AnnouncedListingSnapshotInput, AnnouncedMcpFailure } from "../../internal/harness.js";
|
|
2
|
+
import type { SessionTreeEntry } from "../../internal/harness-types.js";
|
|
3
|
+
import type { DateAnchorRead } from "./contracts.js";
|
|
2
4
|
import type { McpDroppedTool, McpServerStatus } from "../mcp.js";
|
|
3
5
|
import type { RunnerDeps } from "../types.js";
|
|
4
6
|
/** Upper bound on the advisory-digest arm kept in the record (oldest dropped past it). A digest is
|
|
@@ -96,3 +98,27 @@ export declare function loadAnnounceOnceLedger(session: AnnounceOnceSession, che
|
|
|
96
98
|
* present-only arms, copied; `undefined` when nothing was announced on any face (the resume seed
|
|
97
99
|
* then falls to the lower rungs — unchanged contract). */
|
|
98
100
|
export declare function serializeAnnouncedListings(ref: AnnouncedListingSnapshotInput): AnnouncedListingSnapshot | undefined;
|
|
101
|
+
/** The custom-entry kind that carries the session's date anchor (engine-owned; never model-visible). */
|
|
102
|
+
export declare const DATE_ANCHOR_CUSTOM_TYPE = "engine:date-anchor";
|
|
103
|
+
/**
|
|
104
|
+
* Read the session's date anchor off the active branch: the nearest anchor entry NEWER than the nearest
|
|
105
|
+
* compaction, honored only when it was written for that same segment. An anchor written by a leg whose
|
|
106
|
+
* prefix predates a compaction that landed in between therefore reads as absent — the one rule that makes a
|
|
107
|
+
* compaction re-anchor the date. An unreadable branch returns undefined (the leg renders today, writes nothing).
|
|
108
|
+
*/
|
|
109
|
+
export declare function readDateAnchor(session: {
|
|
110
|
+
getBranch(): Promise<ReadonlyArray<SessionTreeEntry>>;
|
|
111
|
+
}): Promise<DateAnchorRead | undefined>;
|
|
112
|
+
/**
|
|
113
|
+
* The anchor write a leg owes when it settles, or undefined: owed when prepare found no anchor for the segment
|
|
114
|
+
* (the leg rendered today) or the model has since been told a newer date; never after an unreadable branch.
|
|
115
|
+
* Best-effort — a lost write costs one re-rendered prefix or one repeated reminder next leg, never a wrong date.
|
|
116
|
+
*/
|
|
117
|
+
export declare function owedDateAnchor(session: {
|
|
118
|
+
appendCustomEntry(customType: string, data?: unknown): Promise<string>;
|
|
119
|
+
}, seat: {
|
|
120
|
+
legDate: string;
|
|
121
|
+
anchor: DateAnchorRead | undefined;
|
|
122
|
+
} | undefined, state: {
|
|
123
|
+
announcedDate: string;
|
|
124
|
+
} | undefined): (() => Promise<void>) | undefined;
|
|
@@ -112,3 +112,40 @@ export function serializeAnnouncedListings(ref) {
|
|
|
112
112
|
};
|
|
113
113
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
114
114
|
}
|
|
115
|
+
export const DATE_ANCHOR_CUSTOM_TYPE = "engine:date-anchor";
|
|
116
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
117
|
+
function normalizeDateAnchor(v) {
|
|
118
|
+
if (typeof v !== "object" || v === null)
|
|
119
|
+
return undefined;
|
|
120
|
+
const { prefixDate, announcedDate, segment } = v;
|
|
121
|
+
if (typeof prefixDate !== "string" || !ISO_DATE.test(prefixDate) || typeof announcedDate !== "string" || !ISO_DATE.test(announcedDate))
|
|
122
|
+
return undefined;
|
|
123
|
+
if (segment !== null && (typeof segment !== "string" || segment.length > 200))
|
|
124
|
+
return undefined;
|
|
125
|
+
return { prefixDate, announcedDate, segment };
|
|
126
|
+
}
|
|
127
|
+
export async function readDateAnchor(session) {
|
|
128
|
+
let branch;
|
|
129
|
+
try {
|
|
130
|
+
branch = await session.getBranch();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
let found;
|
|
136
|
+
let i = branch.length - 1;
|
|
137
|
+
for (; i >= 0 && branch[i].type !== "compaction"; i--) {
|
|
138
|
+
const e = branch[i];
|
|
139
|
+
if (found === undefined && e.type === "custom" && e.customType === DATE_ANCHOR_CUSTOM_TYPE)
|
|
140
|
+
found = normalizeDateAnchor(e.data);
|
|
141
|
+
}
|
|
142
|
+
const segment = i >= 0 ? branch[i].id : null;
|
|
143
|
+
return { current: found !== undefined && found.segment === segment ? { prefixDate: found.prefixDate, announcedDate: found.announcedDate } : undefined, segment };
|
|
144
|
+
}
|
|
145
|
+
export function owedDateAnchor(session, seat, state) {
|
|
146
|
+
const read = seat?.anchor;
|
|
147
|
+
if (read === undefined || state === undefined || read.current?.announcedDate === state.announcedDate)
|
|
148
|
+
return undefined;
|
|
149
|
+
const data = { prefixDate: seat.legDate, announcedDate: state.announcedDate, segment: read.segment };
|
|
150
|
+
return () => session.appendCustomEntry(DATE_ANCHOR_CUSTOM_TYPE, data).then(() => undefined, () => undefined);
|
|
151
|
+
}
|