@sema-agent/core 5.65.0 → 7.0.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 +58 -0
- package/dist/agents/subagent.d.ts +2 -2
- package/dist/agents/subagent.js +11 -0
- package/dist/agents/verify.d.ts +1 -1
- package/dist/brain/anthropic.js +1 -1
- package/dist/brain/errors.d.ts +29 -0
- package/dist/brain/errors.js +20 -0
- package/dist/brain/open-responses.js +2 -2
- package/dist/brain/route-adjudicator.d.ts +8 -1
- package/dist/brain/route-adjudicator.js +1 -0
- package/dist/brain/status-sink.js +12 -1
- package/dist/brain/stream-engine.js +17 -6
- package/dist/core/auto-compaction.d.ts +26 -0
- package/dist/core/auto-compaction.js +7 -2
- package/dist/core/auto-mode-arming.d.ts +138 -0
- package/dist/core/auto-mode-arming.js +181 -0
- package/dist/core/auto-mode-defaults.d.ts +13 -0
- package/dist/core/auto-mode-defaults.js +5 -0
- package/dist/core/auto-mode-prompt.d.ts +14 -3
- package/dist/core/auto-mode-prompt.js +10 -7
- package/dist/core/auto-mode-rebuild.d.ts +75 -0
- package/dist/core/auto-mode-rebuild.js +41 -0
- package/dist/core/auto-mode.d.ts +15 -0
- package/dist/core/auto-mode.js +4 -2
- package/dist/core/checkpoint-store.d.ts +18 -0
- package/dist/core/context-edit.d.ts +47 -5
- package/dist/core/context-guard.d.ts +1 -1
- package/dist/core/file-history-retention.d.ts +106 -0
- package/dist/core/file-history-retention.js +36 -0
- package/dist/core/file-history-store.d.ts +768 -0
- package/dist/core/file-history-store.js +880 -0
- package/dist/core/governance-codes.d.ts +13 -3
- package/dist/core/governance-codes.js +15 -0
- package/dist/core/hooks.d.ts +39 -7
- package/dist/core/hooks.js +38 -21
- package/dist/core/lsp.d.ts +2 -2
- package/dist/core/mcp.d.ts +29 -7
- package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
- package/dist/core/memory-engine/consolidation-driver.js +71 -4
- package/dist/core/memory-engine/consolidation.d.ts +25 -2
- package/dist/core/memory-engine/consolidation.js +4 -1
- package/dist/core/memory-engine/distiller.d.ts +84 -1
- package/dist/core/memory-engine/distiller.js +68 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +328 -15
- package/dist/core/memory-engine/engine.js +355 -29
- package/dist/core/memory-engine/file-backend.d.ts +30 -0
- package/dist/core/memory-engine/file-backend.js +14 -13
- package/dist/core/memory-engine/frontmatter.d.ts +22 -1
- package/dist/core/memory-engine/frontmatter.js +3 -0
- package/dist/core/memory-engine/header-hints.d.ts +5 -0
- package/dist/core/memory-engine/index.d.ts +5 -4
- package/dist/core/memory-engine/index.js +5 -4
- package/dist/core/memory-engine/layout.d.ts +88 -2
- package/dist/core/memory-engine/layout.js +112 -3
- package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
- package/dist/core/memory-engine/provenance-wording.js +3 -0
- package/dist/core/memory-engine/tools.d.ts +89 -8
- package/dist/core/memory-engine/tools.js +263 -22
- package/dist/core/memory-engine/types.d.ts +64 -1
- package/dist/core/memory-recall.d.ts +6 -0
- package/dist/core/memory.d.ts +27 -1
- package/dist/core/memory.js +16 -2
- package/dist/core/permission-rule-consent.d.ts +62 -2
- package/dist/core/permission-rule-consent.js +105 -14
- package/dist/core/permission-rule-model.d.ts +118 -16
- package/dist/core/permission-rule-model.js +57 -9
- package/dist/core/permission-rule-session.d.ts +124 -0
- package/dist/core/permission-rule-session.js +121 -0
- package/dist/core/permission-rule-store.d.ts +65 -2
- package/dist/core/permission-rule-store.js +75 -16
- package/dist/core/permission-rule-sync.d.ts +9 -0
- package/dist/core/permission-rule-sync.js +52 -19
- package/dist/core/retention-policy.d.ts +9 -0
- package/dist/core/retention-policy.js +5 -2
- package/dist/core/retention.d.ts +13 -2
- package/dist/core/runner/assemble-result.d.ts +19 -1
- package/dist/core/runner/assemble-result.js +17 -2
- package/dist/core/runner/compaction-call-options.d.ts +93 -0
- package/dist/core/runner/compaction-call-options.js +3 -0
- package/dist/core/runner/memory-capture-optout.d.ts +80 -0
- package/dist/core/runner/memory-capture-optout.js +53 -0
- package/dist/core/runner/prepare-config-doors.d.ts +5 -0
- package/dist/core/runner/prepare-config-doors.js +16 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
- package/dist/core/runner/prepare-hands-readface.js +99 -7
- package/dist/core/runner/prepare-memory.d.ts +88 -0
- package/dist/core/runner/prepare-memory.js +305 -24
- package/dist/core/runner/prepare-task.d.ts +141 -1
- package/dist/core/runner/prepare-task.js +478 -81
- package/dist/core/runner/runtask.d.ts +9 -20
- package/dist/core/runner/runtask.js +133 -96
- package/dist/core/runner/session-file-state-replay.d.ts +18 -10
- package/dist/core/runner/session-file-state-replay.js +52 -1
- package/dist/core/runner/tool-disclosure.js +2 -1
- package/dist/core/runner/turn-attachments.d.ts +22 -12
- package/dist/core/session-store.d.ts +1 -1
- package/dist/core/session-store.js +6 -1
- package/dist/core/session.d.ts +34 -1
- package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/file-history-store-contract.js +720 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +15 -1
- package/dist/core/task-registry-shared.js +11 -1
- package/dist/core/tool-errors.js +1 -0
- package/dist/core/tool-policy.d.ts +172 -1
- package/dist/core/tool-policy.js +32 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +31 -2
- package/dist/core/types.d.ts +795 -89
- package/dist/core/types.js +4 -3
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/core/untrusted-text.js +8 -0
- package/dist/core/workflow-run-store-contract.js +6 -2
- package/dist/core/workflow-run-store.d.ts +4 -1
- package/dist/engine/compaction/compaction.d.ts +88 -10
- package/dist/engine/compaction/compaction.js +109 -30
- package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
- package/dist/engine/execution-env/node-execution-env.js +28 -0
- package/dist/engine/harness/agent-harness.d.ts +52 -1
- package/dist/engine/harness/agent-harness.js +36 -1
- package/dist/engine/harness/types.d.ts +26 -1
- package/dist/engine/llm/types.d.ts +50 -4
- package/dist/engine/loop/agent-loop.d.ts +5 -1
- package/dist/engine/loop/agent-loop.js +25 -0
- package/dist/engine/loop/types.d.ts +19 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
- package/dist/engine/session/session.js +1 -1
- package/dist/index.d.ts +20 -9
- package/dist/index.js +16 -7
- package/dist/orchestration/run-workflow-tool.d.ts +20 -2
- package/dist/orchestration/run-workflow-tool.js +22 -3
- package/dist/orchestration/workflow-governance.d.ts +59 -1
- package/dist/orchestration/workflow-governance.js +61 -8
- package/dist/orchestration/workflow-meta.d.ts +4 -2
- package/dist/orchestration/workflow-primitives.js +56 -13
- package/dist/orchestration/workflow-types.d.ts +78 -2
- package/dist/orchestration/workflow.d.ts +20 -0
- package/dist/orchestration/workflow.js +163 -14
- package/dist/prompt-assembly/event-registry.js +1 -1
- package/dist/prompts/default.d.ts +7 -7
- package/dist/stores/file/file-history-store.d.ts +368 -0
- package/dist/stores/file/file-history-store.js +1248 -0
- package/dist/stores/file/index.d.ts +22 -13
- package/dist/stores/file/index.js +4 -4
- package/dist/stores/file/permission-rule-store.d.ts +11 -0
- package/dist/stores/file/permission-rule-store.js +23 -9
- package/dist/stores/file/strategy-store.d.ts +3 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
- package/dist/tools/fs/bash-readonly-classifier.js +106 -4
- package/dist/tools/fs/fs-bash.js +9 -5
- package/dist/tools/fs/fs-shared.d.ts +52 -1
- package/dist/tools/fs/fs-shared.js +14 -0
- package/dist/tools/fs/fs-write.d.ts +5 -5
- package/dist/tools/fs/fs-write.js +71 -14
- package/dist/tools/fs/index.d.ts +6 -1
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/web.js +2 -1
- package/package.json +5 -1
- package/test/export-surface.snapshot.json +169 -23
- package/dist/core/file-snapshot-store.d.ts +0 -165
- package/dist/core/file-snapshot-store.js +0 -259
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
- package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
- package/dist/stores/file/file-snapshot-store.d.ts +0 -58
- package/dist/stores/file/file-snapshot-store.js +0 -353
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
import { type FileHistoryRetentionPolicy } from "./file-history-retention.js";
|
|
2
|
+
import type { ExecutionEnv } from "../internal/harness-types.js";
|
|
3
|
+
/** rel path of `child` under `root` in "/" form (win root) / verbatim form (posix root); null when not under.
|
|
4
|
+
* Fold "\" only under a WIN-family root ({@link isWinFormPath}); a POSIX tree may legally contain literal
|
|
5
|
+
* backslashes in FILE NAMES, and folding them would corrupt both the track key and the restore target. */
|
|
6
|
+
declare function relUnder(root: string, child: string): string | null;
|
|
7
|
+
/** Join root + a "/"-form relPath using the ROOT's native separator (keys stay family-consistent). */
|
|
8
|
+
declare function joinUnder(root: string, rel: string): string;
|
|
9
|
+
/** The parent directory of an addressed path, as a pure family-aware string op (no host node:path). */
|
|
10
|
+
declare function parentDirOf(absPath: string): string | undefined;
|
|
11
|
+
/** The re-join verdict of {@link resolveTrackKey}: an absolute path to operate on, or a NAMED
|
|
12
|
+
* refusal (the fail-closed arm — a recorded key that would escape `root` is never coerced into
|
|
13
|
+
* something writable). A-082 idx5: named so the fence can be part of the public surface. */
|
|
14
|
+
export type FileHistoryTrackKeyResolution = {
|
|
15
|
+
ok: true;
|
|
16
|
+
abs: string;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
21
|
+
/** The path family a track key was MINTED under — the half a bare key string cannot carry. */
|
|
22
|
+
export type FileHistoryKeyFamily = "posix" | "win";
|
|
23
|
+
/**
|
|
24
|
+
* A track key's SELF-DESCRIPTION (design/381 §3 amendment, closing the A-082 r2 registered
|
|
25
|
+
* residual): a key is a bare string, so it carries the shape it was minted in but never the family
|
|
26
|
+
* it was minted under. Recorded beside every tracked path and carried by the session-sync envelope,
|
|
27
|
+
* so a graph that arrives from ANOTHER MACHINE can be judged at the door instead of silently
|
|
28
|
+
* re-joining under the local root as an oddly-named in-root file.
|
|
29
|
+
*/
|
|
30
|
+
export interface FileHistoryTrackKeyDescriptor {
|
|
31
|
+
/** `"root-relative"` = re-join against the CURRENT root; `"absolute"` = restore where captured. */
|
|
32
|
+
kind: "root-relative" | "absolute";
|
|
33
|
+
family: FileHistoryKeyFamily;
|
|
34
|
+
}
|
|
35
|
+
/** The family of a root — the one place a backend asks "which family am I operating in?". */
|
|
36
|
+
export declare function trackKeyFamilyOf(root: string): FileHistoryKeyFamily;
|
|
37
|
+
/**
|
|
38
|
+
* {@link trackKeyOf} plus the self-description a backend must record with it. Mint through THIS and
|
|
39
|
+
* a graph stays judgeable after it crosses machines; mint through the bare key alone and the
|
|
40
|
+
* provenance is gone for good (the residual this closes).
|
|
41
|
+
*/
|
|
42
|
+
export declare function describeTrackKey(root: string, absPath: string): {
|
|
43
|
+
key: string;
|
|
44
|
+
} & FileHistoryTrackKeyDescriptor;
|
|
45
|
+
/**
|
|
46
|
+
* Track-key coordinate (design/381 DV-6, the CC `wZu` relativization ported to the trusted task
|
|
47
|
+
* root): a path under `root` is recorded root-RELATIVE (worktree/moved-root safe — restore re-joins
|
|
48
|
+
* it against the CURRENT root), a path outside `root` is recorded verbatim-absolute.
|
|
49
|
+
*
|
|
50
|
+
* **Exported as part of the "MUST reuse, NOT re-implement" set** (A-082 idx5, the same §3 分工
|
|
51
|
+
* clause that governs the two env halves): every backend mints keys from the `(absPath, root)` pair
|
|
52
|
+
* `trackEdit`/`snapshot` hand it and re-joins them in `restore`, so this pair — not the env halves —
|
|
53
|
+
* is where a hand-rolled backend re-introduces path escape. The escape check lives INSIDE the pair
|
|
54
|
+
* (mint refuses to record an ambiguous rel form; {@link resolveTrackKey} refuses an unsafe recorded
|
|
55
|
+
* one), so a backend that calls them cannot re-open the hole, and one that re-derives them must
|
|
56
|
+
* re-derive the family-conditional bare-drive rule and the '..' fail-closed arm correctly on its own.
|
|
57
|
+
*/
|
|
58
|
+
export declare function trackKeyOf(root: string, absPath: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Re-join a recorded track key against the CURRENT root (identity for absolute keys) — the
|
|
61
|
+
* {@link trackKeyOf} mirror, exported under the same "MUST reuse" clause. A key that would escape
|
|
62
|
+
* the root is REFUSED by name, never coerced: `{ok:false}` is the fail-closed arm a backend must
|
|
63
|
+
* surface as a per-file `refused` row, not silently drop.
|
|
64
|
+
*
|
|
65
|
+
* CROSS-FAMILY PROVENANCE (A-082 codex r2, registered in slice 1 — CLOSED here in slice 2 exactly as
|
|
66
|
+
* that note prescribed). A key is a bare STRING: it carries the shape it was minted in, never the
|
|
67
|
+
* FAMILY it was minted under. Every key minted against this scope's own root resolves by the mirror
|
|
68
|
+
* above — but a scope carried in from another machine (the {@link FileHistoryExport} envelope,
|
|
69
|
+
* `adoptScope` across hosts) can hold a key whose family differs from the current root's, and under
|
|
70
|
+
* a POSIX root a win-absolute record (`D:\other\x`) used to re-join UNDER the root as an oddly-named
|
|
71
|
+
* in-root file (contained, never traversal — but not the file the record meant). The closure is the
|
|
72
|
+
* §3 amendment the note named: a SELF-DESCRIBING key ({@link FileHistoryTrackKeyDescriptor}: kind +
|
|
73
|
+
* source family) recorded beside every tracked path and carried by the versioned envelope, judged
|
|
74
|
+
* HERE through the optional `minted` argument. Omit it and this function is byte-identical to
|
|
75
|
+
* before; pass it and a foreign-family record is refused by name instead of coerced.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveTrackKey(root: string, key: string, minted?: FileHistoryTrackKeyDescriptor): FileHistoryTrackKeyResolution;
|
|
78
|
+
export interface FileHistoryError {
|
|
79
|
+
/** `not_found` = no boundary for that (scope, entryId); `aborted` = the signal fired (or the
|
|
80
|
+
* fencing discarded a timed-out attempt); `conflict` = a non-destructive-target law refused
|
|
81
|
+
* (adopt/import onto a scope with its own history) — and ONLY that, because a caller decides
|
|
82
|
+
* whether to stay quiet on it: the engine's fork-carry seat treats `conflict` as "already carried,
|
|
83
|
+
* or grown its own" and says nothing, so an adoption that failed for any OTHER reason (a corrupt
|
|
84
|
+
* source graph, a blob it cannot reach, a consumed-id ledger it cannot carry — including the
|
|
85
|
+
* tombstones a publish owes for unreadable residue it is about to replace) must NOT wear this
|
|
86
|
+
* code or the failure disappears into that silence — those are `adopt_failed`, which the
|
|
87
|
+
* ADOPTION FAMILY shares: `importHistory` publishes under the same target law and answers the
|
|
88
|
+
* same code for the same non-conflict failures (its `unsupported_format` names a bad ENVELOPE or
|
|
89
|
+
* work its shape drove; a fault in the PUBLISH step itself — the disk refusing a good graph —
|
|
90
|
+
* answers `adopt_failed`); `unsupported_format`
|
|
91
|
+
* = a foreign-epoch or wrong-version envelope; the rest name the verb that failed. */
|
|
92
|
+
code: "track_failed" | "snapshot_failed" | "restore_failed" | "adopt_failed" | "not_found" | "aborted" | "unsupported_format" | "conflict";
|
|
93
|
+
message: string;
|
|
94
|
+
}
|
|
95
|
+
export type FileHistoryResult = {
|
|
96
|
+
ok: true;
|
|
97
|
+
} | {
|
|
98
|
+
ok: false;
|
|
99
|
+
error: FileHistoryError;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* {@link FileHistoryStore.trackEdit}'s answer. `minted` is the ANNUL HANDLE in boolean form (#491):
|
|
103
|
+
* true = THIS call created the first-touch record, so the caller may {@link
|
|
104
|
+
* FileHistoryStore.annulTrack} it if the edit that record was minted for never lands; false = the
|
|
105
|
+
* path was already tracked (a cheap no-op), and annulling would destroy a record some EARLIER edit
|
|
106
|
+
* legitimately owns. A backend that cannot tell answers `false` — the safe side (a stale record is
|
|
107
|
+
* a cowork hazard, but discarding someone else's pristine baseline is worse).
|
|
108
|
+
*/
|
|
109
|
+
export type FileHistoryTrackResult = {
|
|
110
|
+
ok: true;
|
|
111
|
+
minted: boolean;
|
|
112
|
+
} | {
|
|
113
|
+
ok: false;
|
|
114
|
+
error: FileHistoryError;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Three-state discriminant + full per-file ledger on EVERY arm (design/381 DV-15). Refusals are
|
|
118
|
+
* never folded into benign skips; `base_unavailable` (DV-14) is a refusal reason.
|
|
119
|
+
*/
|
|
120
|
+
export type FileHistoryRestoreResult = {
|
|
121
|
+
/** `"complete"` = every target applied or identical; `"partial"` = ≥1 refused or failed, others
|
|
122
|
+
* applied; `"failed"` = the operation itself could not run (e.g. no such boundary). */
|
|
123
|
+
outcome: "complete" | "partial" | "failed";
|
|
124
|
+
/** Track keys written or deleted (the converged set). */
|
|
125
|
+
filesChanged: string[];
|
|
126
|
+
/** Track keys already equal to the target (untouched — content-hash proven, never mtime). */
|
|
127
|
+
identicalSkipped: string[];
|
|
128
|
+
/** Per-file security/consistency refusals (skipped, counted, never failing the whole restore). */
|
|
129
|
+
refused: Array<{
|
|
130
|
+
path: string;
|
|
131
|
+
reason: string;
|
|
132
|
+
}>;
|
|
133
|
+
/** Per-file operational failures (env write/remove errors, aborts after k writes). */
|
|
134
|
+
failed: Array<{
|
|
135
|
+
path: string;
|
|
136
|
+
reason: string;
|
|
137
|
+
}>;
|
|
138
|
+
/** Files whose recorded POSIX mode could not be re-applied (env without the optional mode face). */
|
|
139
|
+
modeNotPreserved?: number;
|
|
140
|
+
/** Present on outcome:"failed" (operation-level cause) and optionally on "partial". */
|
|
141
|
+
error?: FileHistoryError;
|
|
142
|
+
};
|
|
143
|
+
export interface FileHistoryDiffStats {
|
|
144
|
+
filesChanged: string[];
|
|
145
|
+
insertions: number;
|
|
146
|
+
deletions: number;
|
|
147
|
+
}
|
|
148
|
+
/** The line-diff bound (see {@link countRestoreLineDiff}): a pair whose edit distance exceeds this
|
|
149
|
+
* is counted as a whole-file replacement. Public so a preview UI can state what it is showing. */
|
|
150
|
+
export declare const FILE_HISTORY_DIFF_LINE_BUDGET = 2000;
|
|
151
|
+
/**
|
|
152
|
+
* The per-file line arithmetic behind {@link FileHistoryStore.diffStats} — ONE rule both bundled
|
|
153
|
+
* backends use, because "how much will this rewind change" is a claim, and two backends answering it
|
|
154
|
+
* differently would be two claims.
|
|
155
|
+
*
|
|
156
|
+
* `undefined` on either side means "does not exist" (a creation or a deletion), so every arm of the
|
|
157
|
+
* restore is expressible: bytes → bytes is a line diff, absent → bytes is a pure insertion, bytes →
|
|
158
|
+
* absent a pure deletion.
|
|
159
|
+
*
|
|
160
|
+
* TWO HONESTY BOUNDS, both stated rather than tuned in silence:
|
|
161
|
+
* · BINARY content (a NUL byte anywhere) contributes 0/0 while still being reported as CHANGED —
|
|
162
|
+
* "lines" is not a fact about a binary file, and printing a number derived from stray `\n` bytes
|
|
163
|
+
* would be inventing one.
|
|
164
|
+
* · Beyond {@link FILE_HISTORY_DIFF_LINE_BUDGET} the greedy diff STOPS and the pair is counted as a
|
|
165
|
+
* whole-file replacement (every current line deleted, every target line inserted). That is not a
|
|
166
|
+
* fallback to a worse estimate so much as the shape such a pair actually has — a diff that large
|
|
167
|
+
* IS a rewrite — and it keeps a preview from turning into an O(N·M) walk over two big files.
|
|
168
|
+
*/
|
|
169
|
+
export declare function countRestoreLineDiff(current: Uint8Array | undefined, target: Uint8Array | undefined): {
|
|
170
|
+
insertions: number;
|
|
171
|
+
deletions: number;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Versioned session-sync envelope; `formatVersion` is the refuse-not-coerce gate (design/381 §5.3).
|
|
175
|
+
* COMPLETE-state export: the tracked set and EVERY retained version record travel — including v1
|
|
176
|
+
* records no surviving boundary references (they are restore()'s pristine fallback and reap()
|
|
177
|
+
* retains them independently) and the parent-identity guards (root-relative under root) that
|
|
178
|
+
* restore's preflight needs. An import that received only boundary references could neither run the
|
|
179
|
+
* parent-chain check nor serve a tracked-later-than-target restore; round-trip-after-reap is a
|
|
180
|
+
* contract-kit test.
|
|
181
|
+
*/
|
|
182
|
+
export interface FileHistoryExport {
|
|
183
|
+
formatVersion: "file-history/1";
|
|
184
|
+
tracked: Array<{
|
|
185
|
+
/** Root-relative when captured under the task root, verbatim-absolute otherwise. */
|
|
186
|
+
relOrAbsPath: string;
|
|
187
|
+
/**
|
|
188
|
+
* The key's SELF-DESCRIPTION (§3 amendment, A-082 r2). A bare key string cannot say which
|
|
189
|
+
* family minted it, so a graph that crosses machines was previously re-joined against the local
|
|
190
|
+
* root on a guess. Both members travel with every path and the import door refuses an envelope
|
|
191
|
+
* whose values are outside the closed sets (refuse-not-coerce, same posture as `formatVersion`);
|
|
192
|
+
* {@link resolveTrackKey} then judges each foreign-family key at restore time.
|
|
193
|
+
*/
|
|
194
|
+
keyKind: FileHistoryTrackKeyDescriptor["kind"];
|
|
195
|
+
keyFamily: FileHistoryKeyFamily;
|
|
196
|
+
/**
|
|
197
|
+
* DV-14: false ⇒ the pristine base could not be captured (durable base-unavailable marker); the
|
|
198
|
+
* chain then starts above v1 and restore-below-base must keep refusing after import. Travels
|
|
199
|
+
* with the graph; never inferred.
|
|
200
|
+
*/
|
|
201
|
+
baseAvailable: boolean;
|
|
202
|
+
/** Full retained chain (v1 first iff baseAvailable). `blobHash:null` = existed-not marker. */
|
|
203
|
+
versions: Array<{
|
|
204
|
+
version: number;
|
|
205
|
+
blobHash: string | null;
|
|
206
|
+
mode?: number;
|
|
207
|
+
parentIdent?: string;
|
|
208
|
+
backupTime: string;
|
|
209
|
+
}>;
|
|
210
|
+
}>;
|
|
211
|
+
boundaries: Array<{
|
|
212
|
+
entryId: string;
|
|
213
|
+
files: Array<{
|
|
214
|
+
relOrAbsPath: string;
|
|
215
|
+
version: number;
|
|
216
|
+
}>;
|
|
217
|
+
}>;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The IDENTITY of an envelope — sha256 over its canonical (sorted) form, so the same graph digests
|
|
221
|
+
* the same however a backend ordered its rows. It is what makes `importHistory`'s idempotent
|
|
222
|
+
* re-publish decidable WITHOUT extra bookkeeping: a target already holding a graph that digests
|
|
223
|
+
* equal to the incoming one is the SAME import arriving twice (ok), while any other non-empty target
|
|
224
|
+
* is locally-grown history the import must never overwrite (typed `conflict`). Exported under the
|
|
225
|
+
* "MUST reuse" clause: two backends that computed identity differently would disagree about which
|
|
226
|
+
* retries are safe.
|
|
227
|
+
*/
|
|
228
|
+
export declare function fileHistoryExportDigest(data: FileHistoryExport): string;
|
|
229
|
+
/**
|
|
230
|
+
* The import DOOR's structural judgment, shared by every backend (a second implementation is a second
|
|
231
|
+
* opinion about what is safe to accept): the envelope must carry THIS format version and every row
|
|
232
|
+
* must be shaped and self-described from the closed sets — `unsupported_format` otherwise, never a
|
|
233
|
+
* coercion. Backends call this BEFORE fetching a single blob, so a foreign-epoch payload costs
|
|
234
|
+
* nothing and changes nothing.
|
|
235
|
+
*/
|
|
236
|
+
export declare function validateFileHistoryExport(data: FileHistoryExport): FileHistoryResult;
|
|
237
|
+
/**
|
|
238
|
+
* design/381 §3 — per-edited-file rewind history (CC FileHistory topology).
|
|
239
|
+
*
|
|
240
|
+
* Replaces the E19 whole-tree FileSnapshotStore. Three verbs mirror the reference module:
|
|
241
|
+
* `trackEdit` (backup-before-first-edit), `snapshot` (version the tracked set at a turn boundary),
|
|
242
|
+
* `restore` (converge ONLY the tracked set to a recorded boundary).
|
|
243
|
+
*
|
|
244
|
+
* Store discipline (contract-kit enforced):
|
|
245
|
+
* - NEVER throw: every failure is encoded in the returned result (FileSystem posture).
|
|
246
|
+
* - snapshot MUST NOT fail the caller's turn: a failed boundary capture degrades to "no boundary"
|
|
247
|
+
* with disclosure (onError phase:"rewind"). trackEdit is DIFFERENT (DV-14): its `{ok:false}`
|
|
248
|
+
* means "no durable first-touch state exists" and the CALLER's default policy is to REFUSE the
|
|
249
|
+
* edit (typed, loud) — silently proceeding would let a post-recovery retry mint already-modified
|
|
250
|
+
* bytes as pristine v1. Any proceed-unprotected policy is an explicit, adjudicated caller arm,
|
|
251
|
+
* never the store's own behavior.
|
|
252
|
+
* - Structurally out of scope: files the agent never edited. No store method may enumerate, read,
|
|
253
|
+
* or write ANY path outside the tracked set plus its recorded restore targets. (The cowork
|
|
254
|
+
* guarantee lives HERE, as a contract clause.)
|
|
255
|
+
* - BOUNDED by default: one boundary per turn, forever, is unbounded growth. Both bundled backends
|
|
256
|
+
* trim their own scope after each commit (see {@link FileHistoryStore.reap} for the rule, the
|
|
257
|
+
* default and the host override); an out-of-tree backend owes its own bound or an explicit
|
|
258
|
+
* statement that the host must schedule the GC.
|
|
259
|
+
*/
|
|
260
|
+
export interface FileHistoryStore {
|
|
261
|
+
/**
|
|
262
|
+
* Backup `absPath`'s CURRENT state before an agent edit (first touch per scope only; later calls
|
|
263
|
+
* are cheap no-ops). The first touch is ONE atomic durable record — pristine blob+record,
|
|
264
|
+
* existence=false marker (rewind of an agent-CREATED file deletes it), or base-unavailable marker
|
|
265
|
+
* (DV-14). Reads bytes via `env` (remote-safe); stores them host-side. `root` is the trusted task
|
|
266
|
+
* root: paths under it are recorded root-RELATIVE (worktree/moved-root safe), paths outside it
|
|
267
|
+
* verbatim-absolute; the parent-identity guard is recorded in the same frame.
|
|
268
|
+
* `{ok:false}` = NO durable first-touch state was persisted — see the discipline block: the
|
|
269
|
+
* caller's default is to REFUSE the edit (DV-14), because proceeding leaves the path
|
|
270
|
+
* indistinguishable from never-touched. `{ok:true, minted}` — see {@link FileHistoryTrackResult}:
|
|
271
|
+
* `minted:true` means this call created the record, which is what makes {@link annulTrack} legal
|
|
272
|
+
* for it (#491).
|
|
273
|
+
*/
|
|
274
|
+
trackEdit(scope: string, absPath: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryTrackResult>;
|
|
275
|
+
/**
|
|
276
|
+
* Discard the first-touch record `trackEdit` MINTED for `absPath` — the compensating verb for the
|
|
277
|
+
* one ordering the topology cannot avoid (#491): the pre-image capture must run BEFORE the write,
|
|
278
|
+
* so a write that then fails (a lost create race, ENOSPC, EACCES, a read-only mount) leaves a
|
|
279
|
+
* record for an edit that never happened. Left standing, an `existed-not` v1 makes an untouched
|
|
280
|
+
* path a rewind DELETE target: whoever writes those bytes next — the user, a peer process — gets
|
|
281
|
+
* them removed by a restore that reports `complete`, which is precisely the structural
|
|
282
|
+
* out-of-scope guarantee this contract opens with. So the record must be retractable.
|
|
283
|
+
*
|
|
284
|
+
* Discipline (contract-kit enforced): NEVER-throw like every other verb, and no env IO AT ALL
|
|
285
|
+
* unless the caller asks for the proof read below. Retraction is legal ONLY while the record is
|
|
286
|
+
* still exactly what `trackEdit` minted — a record referenced by ANY committed boundary, or one that has grown
|
|
287
|
+
* past its first version, is REFUSED (typed `conflict`) and left intact, because history that a
|
|
288
|
+
* boundary already froze is no longer the caller's to withdraw. An absent record is an idempotent
|
|
289
|
+
* `{ok:true}` (the retry path). Callers annul ONLY what their own `trackEdit` reported as
|
|
290
|
+
* `minted:true`: a path that was already tracked belongs to the edit that first touched it, and
|
|
291
|
+
* discarding that baseline would be the fake-pristine hole DV-14 exists to weld shut.
|
|
292
|
+
*
|
|
293
|
+
* `verify` is the EVIDENCE arm (adversarial round 2). A caller whose write failed with a code the
|
|
294
|
+
* FileSystem contract defines as "nothing was written" (`already_exists`, `precondition_failed`)
|
|
295
|
+
* has proof and passes nothing. Every other failure — a plain write is not contractually atomic,
|
|
296
|
+
* so ENOSPC, a dying transport or a thrown backend may have TRUNCATED the file before reporting —
|
|
297
|
+
* must hand an `env` here: the store then re-reads the path ONCE and retracts only if the world
|
|
298
|
+
* still matches what the mint recorded (bytes still absent for an existed-not mint, still the same
|
|
299
|
+
* hash for a captured one). A file that changed keeps its record, because the alternative is
|
|
300
|
+
* deleting the only baseline of a file the agent really did modify — damage rewind could no longer
|
|
301
|
+
* reach, which is strictly worse than a stale record.
|
|
302
|
+
*/
|
|
303
|
+
annulTrack(scope: string, absPath: string, root: string, verify?: {
|
|
304
|
+
env: ExecutionEnv;
|
|
305
|
+
signal?: AbortSignal;
|
|
306
|
+
}): Promise<FileHistoryResult>;
|
|
307
|
+
/**
|
|
308
|
+
* Record a history boundary keyed by `entryId` — the TURN'S OWN initial session entry, minted
|
|
309
|
+
* after that entry is appended and BEFORE any tool executes, so the boundary means "file state
|
|
310
|
+
* when `entryId`'s turn began" (≡ the reference's user-message-uuid snapshot; every turn
|
|
311
|
+
* incarnation appends a fresh entry, so keys never alias across re-branches from the same fork
|
|
312
|
+
* point). For each tracked file: re-stat via `env`; unchanged (metadata may prove inequality,
|
|
313
|
+
* content hash proves equality — DV-13: never mtime) ⇒ reference the prior version; changed ⇒
|
|
314
|
+
* store a new version; missing ⇒ record a deletion marker. Zero directory-tree traversal, zero
|
|
315
|
+
* size gate. Idempotent per (scope, entryId) over COMMITTED boundaries.
|
|
316
|
+
*
|
|
317
|
+
* Commit visibility + fencing + CAPTURE LEASE (contract-kit enforced): a boundary becomes visible
|
|
318
|
+
* to canRestore/restore ONLY after its complete version mapping is durable. Each attempt is
|
|
319
|
+
* FENCED (a timed-out/aborted attempt may never commit — {@link boundaryPublishVerdict}, no grace
|
|
320
|
+
* threshold), and the boundary's capture window is a LEASE that the caller closes before the
|
|
321
|
+
* first tool executes: after lease close, a fresh CAPTURE (any env read) for that entryId is
|
|
322
|
+
* refused — a retry may only PUBLISH a mapping whose bytes were fully captured before close.
|
|
323
|
+
* The close is NOT persisted and the store has no close verb (design/381 §10 S-6): close is the
|
|
324
|
+
* calling Runner's in-process discipline; after a crash the new process has no capture path for
|
|
325
|
+
* the old entryId (its own prepare mints only its own turn's boundary), and abandoned staging is
|
|
326
|
+
* judged by COMPLETENESS alone — complete ⇒ publishable, incomplete ⇒ abandoned (that turn has no
|
|
327
|
+
* boundary) — with ZERO env reads on the retry path. A turn whose pre-turn capture failed simply
|
|
328
|
+
* has no boundary (disclosed; the reference behaves the same) — there is NO mid-turn re-capture,
|
|
329
|
+
* because re-reading after edits would publish post-turn bytes under a pre-turn key. An entryId
|
|
330
|
+
* whose boundary was COMMITTED and later REAPED is closed by the same law and for the same reason
|
|
331
|
+
* (see {@link FileHistoryStore.reap}): it refuses typed with zero env reads rather than
|
|
332
|
+
* re-capturing a world that has moved on. Retention can therefore make a boundary unavailable,
|
|
333
|
+
* but never wrong.
|
|
334
|
+
*/
|
|
335
|
+
snapshot(scope: string, entryId: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryResult>;
|
|
336
|
+
/**
|
|
337
|
+
* Converge ONLY the tracked set to the boundary `entryId`: for each tracked file, target-version
|
|
338
|
+
* bytes ⇒ write via `env` (the guarded-write arm and the symlink/parent-chain/kind preflights are
|
|
339
|
+
* the design/381 slice-3 hardening — until then the write is plain and ADVISORY, stated, never
|
|
340
|
+
* overclaimed); deletion marker ⇒ remove; identical ⇒ untouched; tracked-later-than-target ⇒ fall
|
|
341
|
+
* back to its v1 (pristine) version, and a base-unavailable path (DV-14) is REFUSED with reason
|
|
342
|
+
* `base_unavailable` (left at its current state — never overwritten with a fake baseline).
|
|
343
|
+
* Per-file refusals SKIP that file and are counted, never failing the whole restore (CC form).
|
|
344
|
+
* Paths recorded relative to `root` re-join against the CURRENT root (worktree/moved-cwd safe).
|
|
345
|
+
*
|
|
346
|
+
* Partial outcome is FIRST-CLASS with a three-state discriminant and the per-file ledger always
|
|
347
|
+
* travels: applied / identicalSkipped / refused / failed are SEPARATE columns (a security refusal
|
|
348
|
+
* must never hide inside a benign skip count), so a caller can distinguish "nothing was mutated"
|
|
349
|
+
* from "the env died after k writes". Restore is per-file idempotent (identical files skip), so
|
|
350
|
+
* the documented recovery for a partial outcome is: re-run the same restore; it converges. There
|
|
351
|
+
* is NO all-or-nothing guarantee and none is claimed. Task-level policy on `"partial"` lives in
|
|
352
|
+
* the CALLER (prepare defaults to fail-loud; see DV-15).
|
|
353
|
+
*/
|
|
354
|
+
restore(scope: string, entryId: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryRestoreResult>;
|
|
355
|
+
/** Whether a boundary exists for (scope, entryId). */
|
|
356
|
+
canRestore(scope: string, entryId: string): Promise<boolean>;
|
|
357
|
+
/**
|
|
358
|
+
* Optional dry-run: per-file change stats for a prospective restore (CC diffStats; design/381
|
|
359
|
+
* slice 3). A-2 two-state law: `null` = preview UNAVAILABLE ≠ an empty stats object = a true
|
|
360
|
+
* zero-coverage answer — an absent preview must never be rendered as "nothing will change".
|
|
361
|
+
*
|
|
362
|
+
* It is an UPPER BOUND on what a restore will touch, and the reason is worth stating rather than
|
|
363
|
+
* discovering: the preview answers the CONTENT question (which tracked files differ from the
|
|
364
|
+
* boundary, and by how many lines), while the per-file security preflight — symlink/kind, parent
|
|
365
|
+
* chain — is decided by `restore` against the world as it stands THEN. A file listed here can
|
|
366
|
+
* therefore come back refused, and a preview that pre-ran those checks would be making promises
|
|
367
|
+
* about a world it does not own. Files whose target cannot be resolved at all (a missing version
|
|
368
|
+
* record or blob, an unsafe key) are left OUT: the restore reports them per file, and a preview
|
|
369
|
+
* cannot show the size of a change it cannot compute.
|
|
370
|
+
*/
|
|
371
|
+
diffStats?(scope: string, entryId: string, env: ExecutionEnv, root: string): Promise<FileHistoryDiffStats | null>;
|
|
372
|
+
/**
|
|
373
|
+
* GC: drop boundaries in `scope` not in `keepEntryIds`; then drop version records (and content
|
|
374
|
+
* blobs) referenced by NO surviving boundary — EXCEPT v1 records AND base-unavailable markers
|
|
375
|
+
* (DV-14), which are retained while the scope lives (the pristine fallback and its explicit
|
|
376
|
+
* refusal state are both part of the graph). Returns the number of boundaries dropped.
|
|
377
|
+
*
|
|
378
|
+
* WHO CALLS IT (design/381 §片2 — the bound is the backend's, not a chore left to the host): both
|
|
379
|
+
* bundled backends drive this verb THEMSELVES. Every committed boundary is followed by a
|
|
380
|
+
* retention pass that trims the scope to the newest
|
|
381
|
+
* {@link import("./file-history-retention.js").DEFAULT_FILE_HISTORY_BOUNDARY_KEEP} boundaries —
|
|
382
|
+
* an engine default a deployment overrides through the store's own `retention` option, including
|
|
383
|
+
* `keep: "unbounded"` to switch the self-trim off and take the schedule back. The pass is
|
|
384
|
+
* best-effort by construction (a GC failure never fails the turn that triggered it) and it only
|
|
385
|
+
* ever removes boundaries the caller could not name any more anyway.
|
|
386
|
+
*
|
|
387
|
+
* A backend outside this repo therefore owes ONE of two things, and silence is neither: its own
|
|
388
|
+
* bound (this verb, driven from its own commit path), or a documented statement that the host
|
|
389
|
+
* must schedule `reap`. An unbounded boundary graph is not a tuning choice — one boundary per
|
|
390
|
+
* turn, forever, is a disk that fills with no line anywhere admitting it would.
|
|
391
|
+
*
|
|
392
|
+
* WHAT THE SELF-TRIM DOES NOT COVER, said here rather than left to be discovered: it runs at a
|
|
393
|
+
* COMMIT, so a graph that arrives whole — `adoptScope`, `importHistory` — is published whole and
|
|
394
|
+
* comes under the bound at that scope's next commit, not before. That is deliberate rather than
|
|
395
|
+
* an oversight (round 2 raised it): both verbs promise all-or-nothing visibility of the graph the
|
|
396
|
+
* caller handed over, and trimming during publication would silently discard history the host
|
|
397
|
+
* explicitly asked to transfer. The overshoot is bounded by the envelope the host chose, which is
|
|
398
|
+
* a different thing from the per-turn source this bound exists to stop.
|
|
399
|
+
*
|
|
400
|
+
* A REAPED entryId IS SPENT (adversarial review — a law this verb acquired the day anything
|
|
401
|
+
* started calling it): dropping a boundary must not hand its key back. Until boundaries could be
|
|
402
|
+
* dropped, the committed mapping was itself the record that an entryId had been consumed, and
|
|
403
|
+
* `snapshot`'s idempotent short-circuit read it; a store that forgot a reaped id would let a
|
|
404
|
+
* later `snapshot` for that key take the CAPTURE path and publish post-turn bytes under a
|
|
405
|
+
* pre-turn key. Both bundled backends therefore tombstone what they reap and refuse a later
|
|
406
|
+
* capture of it, typed and with zero env reads. The ledger is IDS, not mappings, and it is
|
|
407
|
+
* unbounded — stated plainly because a bound's own residue growing forever would be a poor joke:
|
|
408
|
+
* one id string per boundary ever reaped, against the complete tracked-set mapping each of those
|
|
409
|
+
* boundaries used to carry. It is the cheap part of what the bound throws away, by two orders of
|
|
410
|
+
* magnitude, and it is what makes the bound safe rather than merely small.
|
|
411
|
+
*
|
|
412
|
+
* Concurrency is unchanged and stays the caller's to reason about: a pass that runs while another
|
|
413
|
+
* entryId's capture is parked at an env read may drop a version that capture staged, and that
|
|
414
|
+
* capture then refuses TYPED (its turn has no boundary) rather than publishing a dangling
|
|
415
|
+
* reference — the arm the commit paths already validate for.
|
|
416
|
+
*/
|
|
417
|
+
reap(scope: string, keepEntryIds: string[]): Promise<number>;
|
|
418
|
+
/**
|
|
419
|
+
* Cross-session carry (CC copyFileHistoryForResume): make `toScope` see `fromScope`'s boundaries
|
|
420
|
+
* + versions (content-addressed backends copy references, zero bytes). Same-scope call is a
|
|
421
|
+
* no-op. ATOMIC-VISIBILITY, same law as snapshot: the WHOLE adopted graph (tracked set, every
|
|
422
|
+
* version incl. base-availability state, every referenced blob) is staged and verified, then
|
|
423
|
+
* published in one step — `toScope` either sees the complete history or none of it. A
|
|
424
|
+
* failed/partial adopt publishes NOTHING; the resume still proceeds (same availability posture as
|
|
425
|
+
* the reference's tolerated partial copy, without the half-visible graph) and the failure is
|
|
426
|
+
* disclosed. EMPTY-TARGET ONLY: adoption is refused (loud, typed `conflict`) once `toScope` has
|
|
427
|
+
* ANY history of its own — adoption's seat is the fork/resume boundary BEFORE the new session's
|
|
428
|
+
* first turn; a retry after the target has begun writing would need a destructive merge
|
|
429
|
+
* (version-number collisions, overwritten target history) that this contract deliberately
|
|
430
|
+
* forbids. Retry while the target is still empty is idempotent and repairs abandoned staging.
|
|
431
|
+
*
|
|
432
|
+
* WHO CALLS IT — THE ENGINE DOES, since design/381 slice 3, and the seat is worth knowing because
|
|
433
|
+
* it decides WHEN this verb runs against your backend. `prepareTask` carries a forked session's
|
|
434
|
+
* history on the first turn it prepares for it, keyed on the LINEAGE FACT
|
|
435
|
+
* (`SessionMetadata.forkedFrom`) rather than on the code path that forked — so the Agent tool's
|
|
436
|
+
* fork route, `warmResume`, and a deployment that forks a session and then runs a task on it all
|
|
437
|
+
* reach the same seat. It runs BEFORE the restore seat, so the very first task on a fork may
|
|
438
|
+
* itself ask to restore files to a pre-fork boundary.
|
|
439
|
+
*
|
|
440
|
+
* The empty-target law is what makes that safe to attempt rather than schedule: a scope that
|
|
441
|
+
* already holds history answers the typed `conflict`, which at that seat MEANS "already carried,
|
|
442
|
+
* or grown its own" and is treated as a no-op. Only a non-conflict failure is disclosed
|
|
443
|
+
* (`onError` phase `"rewind"`), because that one is a carry the deployment expected and did not
|
|
444
|
+
* get. A failed carry never blocks the fork's run.
|
|
445
|
+
*
|
|
446
|
+
* A HOST MAY STILL CALL IT — the engine's seat covers sessions the engine prepares, and a
|
|
447
|
+
* deployment that copies history between scopes for its own reasons (a migration, a scope rename)
|
|
448
|
+
* is calling the same verb under the same law. What a MISSED carry gets is degraded, not broken,
|
|
449
|
+
* and it is worth naming precisely: the new scope starts EMPTY, so nothing the old one tracked is
|
|
450
|
+
* restorable from it. A rewind aimed at a pre-fork boundary answers "no boundary" (honest, typed)
|
|
451
|
+
* instead of converging those files, and the new lineage's own first touch of a path records THAT
|
|
452
|
+
* path's then-current bytes as its pristine v1 — a correct baseline for the new lineage, just not
|
|
453
|
+
* the old one's history. Nothing is silently mis-restored; the carry simply does not happen.
|
|
454
|
+
*/
|
|
455
|
+
adoptScope(fromScope: string, toScope: string): Promise<FileHistoryResult>;
|
|
456
|
+
/**
|
|
457
|
+
* Session-sync export/import (2c), versioned envelope — see {@link FileHistoryExport}. Optional.
|
|
458
|
+
* importHistory obeys the SAME atomic-visibility + staged-verify law as adoptScope: every
|
|
459
|
+
* referenced blob fetched and integrity-checked, then the scope's graph published in one step; a
|
|
460
|
+
* mid-import failure publishes nothing. NON-DESTRUCTIVE TARGET LAW (same family as adoptScope): a
|
|
461
|
+
* first import requires an EMPTY target graph; a retry is legal only while the target is still
|
|
462
|
+
* empty OR already holds a graph with the exact same import identity/digest (pure idempotent
|
|
463
|
+
* re-publish). A non-empty target with any other content ⇒ typed `conflict` refusal — atomic
|
|
464
|
+
* visibility prevents half-graphs, this law prevents publishing the WRONG complete graph over
|
|
465
|
+
* locally-grown history.
|
|
466
|
+
*/
|
|
467
|
+
/**
|
|
468
|
+
* NO `dispose` / lifecycle verb, and that is a decision rather than an omission (design/381 slice 3,
|
|
469
|
+
* the R-6 review candidate — recorded here because the next reader will otherwise wonder). The case
|
|
470
|
+
* for one: a directly-constructed file backend holds no store lock, so the single-writer premise is
|
|
471
|
+
* enforced only where a deployment assembles the backend (its data-root LOCK), and a `dispose`
|
|
472
|
+
* would let a store hold a lease and release it. The case against, which won:
|
|
473
|
+
* · every verb here is self-contained — a store holds no cross-call resource, so `dispose` would be
|
|
474
|
+
* an obligation on EVERY backend (a SQL twin included) in order to serve one backend's optional
|
|
475
|
+
* locking;
|
|
476
|
+
* · a lock released by a verb nobody is required to call fails WORSE than no lock: a crash leaves a
|
|
477
|
+
* stale lease that blocks a store which today simply works, and the recovery is manual;
|
|
478
|
+
* · the single-writer premise is the deployment's to make. The assembled backend is where it is
|
|
479
|
+
* enforced, and a directly-constructed store is the same "I know what I am doing" path every
|
|
480
|
+
* sibling file store in this family offers.
|
|
481
|
+
* A deployment that needs cross-process exclusion takes the assembled backend, or brings its own.
|
|
482
|
+
*/
|
|
483
|
+
exportHistory?(scope: string): Promise<FileHistoryExport | null>;
|
|
484
|
+
importHistory?(scope: string, data: FileHistoryExport, srcGetBlob: (hash: string) => Promise<Uint8Array | undefined>): Promise<FileHistoryResult>;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* design/381 §3 + §10 T-5 — the FENCING judge for a boundary-capture attempt, exposed as a
|
|
488
|
+
* direct-callable function (the G6b grey-box seat) and used by every in-tree backend as the single
|
|
489
|
+
* commit gate: an attempt whose signal fired — timeout or abort, no matter how close to completion
|
|
490
|
+
* the capture got — may NEVER publish its mapping. ABSOLUTE, no grace threshold (§10 T-5: "无宽限
|
|
491
|
+
* 期" — the verdict has no time axis at all): a late-completing capture may have read bytes AFTER
|
|
492
|
+
* the lease closed (post-edit bytes), and publishing them under the pre-turn key is exactly the
|
|
493
|
+
* poisoning the fence exists to stop.
|
|
494
|
+
*/
|
|
495
|
+
export declare function boundaryPublishVerdict(attempt: {
|
|
496
|
+
aborted: boolean;
|
|
497
|
+
}): {
|
|
498
|
+
publish: true;
|
|
499
|
+
} | {
|
|
500
|
+
publish: false;
|
|
501
|
+
reason: "aborted";
|
|
502
|
+
};
|
|
503
|
+
/** One captured state of one file, as read through the env (the shared capture half). */
|
|
504
|
+
export type FileBackupCapture =
|
|
505
|
+
/** `mode` = the env's POSIX permission bits when it HAS that face (design/381 DV-8); absent when
|
|
506
|
+
* the env cannot express them — never fabricated, so a restore can tell "this mode was recorded
|
|
507
|
+
* and could not be applied" from "there was never a mode to keep". */
|
|
508
|
+
{
|
|
509
|
+
state: "file";
|
|
510
|
+
bytes: Uint8Array;
|
|
511
|
+
hash: string;
|
|
512
|
+
mode?: number;
|
|
513
|
+
parentIdent?: string;
|
|
514
|
+
} | {
|
|
515
|
+
state: "absent";
|
|
516
|
+
parentIdent?: string;
|
|
517
|
+
} | {
|
|
518
|
+
state: "aborted";
|
|
519
|
+
}
|
|
520
|
+
/** Non-ENOENT read failure: the file may exist but its bytes are not capturable right now. */
|
|
521
|
+
| {
|
|
522
|
+
state: "unreadable";
|
|
523
|
+
cause: string;
|
|
524
|
+
};
|
|
525
|
+
/**
|
|
526
|
+
* Capture ONE file's current state through the ExecutionEnv — **the env-operating half every
|
|
527
|
+
* backend MUST reuse** (design/381 §3 分工, the E19 captureManifest condition carried forward:
|
|
528
|
+
* re-implementing it per backend is how symlink-escape/fail-closed bugs come back). Used by
|
|
529
|
+
* `trackEdit` (first-touch pristine) and `snapshot` (per-file re-read). `not_found` is the sole
|
|
530
|
+
* absent signal (the read itself is the authoritative existence probe — a separate stat would only
|
|
531
|
+
* re-open the same race one window later). The parent-identity guard rides the same frame:
|
|
532
|
+
* `canonicalPath(parent)` — root-relativized when under `root` (DV-6: a legal whole-root move must
|
|
533
|
+
* keep verifying; an in-root parent-chain swap must keep refusing) — recorded best-effort (an env
|
|
534
|
+
* that cannot canonicalize omits it; the slice-3 preflight then has nothing to compare, which is
|
|
535
|
+
* the honest degraded form, never a fabricated identity).
|
|
536
|
+
*/
|
|
537
|
+
export declare function captureFileBackupViaEnv(env: ExecutionEnv, absPath: string, root: string, signal?: AbortSignal): Promise<FileBackupCapture>;
|
|
538
|
+
/**
|
|
539
|
+
* The retraction PROOF READ (design/381 #491 + adversarial round 2), shared by every backend so the
|
|
540
|
+
* evidence rule cannot drift: does the path still look exactly as the first touch recorded it? An
|
|
541
|
+
* existed-not mint requires the path to still be absent; a captured mint requires the same content
|
|
542
|
+
* hash. Anything else — including an unreadable path — leaves the record standing, because the only
|
|
543
|
+
* failure this arm exists to distinguish is "the write never landed" from "the write may have
|
|
544
|
+
* changed the file". A base-unavailable marker is REFUSED here rather than waved through
|
|
545
|
+
* (adversarial round 3): it carries no bytes to compare, but the marker itself is the durable
|
|
546
|
+
* refusal state that stops a later touch from minting already-modified bytes as pristine — erasing
|
|
547
|
+
* it on an ambiguous failure re-opens exactly the DV-14 hole it exists to close. It stays
|
|
548
|
+
* retractable through the `"proven"` path, which needs no evidence read at all.
|
|
549
|
+
*/
|
|
550
|
+
export declare function worldStillMatchesMint(env: ExecutionEnv, absPath: string, root: string, baseAvailable: boolean, mintedBlobHash: string | null, signal?: AbortSignal): Promise<{
|
|
551
|
+
unchanged: true;
|
|
552
|
+
} | {
|
|
553
|
+
unchanged: false;
|
|
554
|
+
reason: string;
|
|
555
|
+
}>;
|
|
556
|
+
/**
|
|
557
|
+
* DV-13's metadata rule applied to the MODE axis (design/381 slice 3), and it is a rule about
|
|
558
|
+
* EVIDENCE rather than about equality: metadata may prove two states DIFFERENT, never the same. Two
|
|
559
|
+
* known-and-unequal modes are a proven difference, so the file is versioned again even though its
|
|
560
|
+
* bytes are identical (the permission bit is part of the state a rewind restores). An UNKNOWN mode
|
|
561
|
+
* on either side proves nothing and must not manufacture a version — which is also what keeps a
|
|
562
|
+
* graph captured before this field existed from re-versioning its whole tracked set on the first
|
|
563
|
+
* boundary after an upgrade.
|
|
564
|
+
*/
|
|
565
|
+
export declare function modeProvenDifferent(recorded: number | undefined, current: number | undefined): boolean;
|
|
566
|
+
/** The target one file must converge to (bytes+hash, or "must not exist"). */
|
|
567
|
+
export type FileRestoreTarget = {
|
|
568
|
+
bytes: Uint8Array;
|
|
569
|
+
hash: string;
|
|
570
|
+
} | {
|
|
571
|
+
absent: true;
|
|
572
|
+
};
|
|
573
|
+
/**
|
|
574
|
+
* The recorded facts the slice-3 preflight judges the current world against, plus the abort signal
|
|
575
|
+
* (design/381 §6 slice 3). Every member is OPTIONAL on purpose: a record captured by an env without
|
|
576
|
+
* the mode face, or without canonicalization, carries neither — and a guard with nothing recorded to
|
|
577
|
+
* compare must degrade to "no opinion", never to a fabricated expectation.
|
|
578
|
+
*/
|
|
579
|
+
export interface FileRestoreApplyOptions {
|
|
580
|
+
/** The task root the recorded key was re-joined against — the coordinate `parentIdent` lives in. */
|
|
581
|
+
root: string;
|
|
582
|
+
/** The parent identity recorded WITH the version being restored ({@link parentIdentityViaEnv}). */
|
|
583
|
+
parentIdent?: string;
|
|
584
|
+
/** The POSIX mode recorded with the version being restored (design/381 DV-8). */
|
|
585
|
+
mode?: number;
|
|
586
|
+
signal?: AbortSignal;
|
|
587
|
+
}
|
|
588
|
+
export type FileRestoreApplyOutcome =
|
|
589
|
+
/** `modeNotPreserved` rides the two NON-refusal arms: the bytes converged but the recorded mode
|
|
590
|
+
* could not be re-applied (no env mode face, or the env refused/failed the change). A disclosure,
|
|
591
|
+
* never a failure — the file's CONTENT is what the restore promised. */
|
|
592
|
+
{
|
|
593
|
+
outcome: "applied";
|
|
594
|
+
modeNotPreserved?: true;
|
|
595
|
+
} | {
|
|
596
|
+
outcome: "identical";
|
|
597
|
+
modeNotPreserved?: true;
|
|
598
|
+
} | {
|
|
599
|
+
outcome: "refused";
|
|
600
|
+
reason: string;
|
|
601
|
+
} | {
|
|
602
|
+
outcome: "failed";
|
|
603
|
+
reason: string;
|
|
604
|
+
};
|
|
605
|
+
/**
|
|
606
|
+
* Converge ONE file to `target` through the ExecutionEnv — **the env-operating + security half
|
|
607
|
+
* every backend MUST reuse** (design/381 §3 分工). skip-if-identical is content-hash proven (DV-13:
|
|
608
|
+
* metadata may only ever prove INEQUALITY, never equality; mtime is never consulted), so an
|
|
609
|
+
* identical file is untouched (no write, no mtime churn). Deletion markers remove the current file
|
|
610
|
+
* (ENOENT tolerated = already converged).
|
|
611
|
+
*
|
|
612
|
+
* THE PREFLIGHT FAMILY (design/381 slice 3, DV-6 — the env-level approximation of the reference's
|
|
613
|
+
* fd-level `O_NOFOLLOW` + dev/ino re-check, ordered cheapest-refusal-first). Every arm SKIPS the
|
|
614
|
+
* one file and is counted; none of them fails the whole restore:
|
|
615
|
+
* · KIND — the addressed object (no-follow `fileInfo`) must be a regular file or absent. A symlink
|
|
616
|
+
* at the leaf is refused rather than followed: the recorded bytes belong to the file the agent
|
|
617
|
+
* edited, not to whatever a link now points at. A directory/socket/FIFO in its place is the same
|
|
618
|
+
* refusal for the same reason.
|
|
619
|
+
* · PARENT CHAIN — the current canonical parent identity must equal the one recorded with the
|
|
620
|
+
* version ({@link parentIdentityViaEnv}, root-relative under the root). A legal whole-root move
|
|
621
|
+
* keeps it equal (E4 passes); an in-root ancestor swapped for a link out of the tree does not
|
|
622
|
+
* (E5 refuses, and nothing outside the root is written). BOTH must hold at once — relaxing to
|
|
623
|
+
* "moves also refuse" or "swaps also pass" is a mis-fix, not a tuning choice.
|
|
624
|
+
* · The guard is EVIDENCE-BASED, not assumption-based: a record with no parent identity (an env
|
|
625
|
+
* that cannot canonicalize) and a parent that cannot be canonicalized NOW (the ordinary "the
|
|
626
|
+
* directory was deleted and this restore recreates it" case) both mean "nothing to compare", and
|
|
627
|
+
* inventing a refusal there would break the recreate arm rather than close a hole.
|
|
628
|
+
*
|
|
629
|
+
* THE WRITE uses `env.writeFileGuarded` when the env has it — one atomic verify-and-write whose
|
|
630
|
+
* expectation names the addressed path and forbids following a leaf link, so the window between
|
|
631
|
+
* this preflight and the write is closed at the backend. An env WITHOUT it falls back to plain
|
|
632
|
+
* `env.writeFile` and the convergence is then ADVISORY on the TOCTOU axis: the preflight above is a
|
|
633
|
+
* check-then-act, and a sufficiently fast racer can still swap the leaf in between. Stated here
|
|
634
|
+
* because the alternative — a two-step emulation behind an atomic-looking face — is exactly what
|
|
635
|
+
* that capability's own contract forbids.
|
|
636
|
+
*
|
|
637
|
+
* MODE (DV-8): a recorded mode is re-applied through the optional `setFileMode` face after the
|
|
638
|
+
* bytes land (and on an otherwise-identical file whose mode drifted, which IS a convergence). An env
|
|
639
|
+
* without the face — or one that refuses the change — yields `modeNotPreserved`, disclosed by the
|
|
640
|
+
* caller's ledger; it never turns a successful content restore into a failure.
|
|
641
|
+
*/
|
|
642
|
+
export declare function applyFileRestoreViaEnv(env: ExecutionEnv, absPath: string, target: FileRestoreTarget, opts: FileRestoreApplyOptions): Promise<FileRestoreApplyOutcome>;
|
|
643
|
+
/**
|
|
644
|
+
* ONE file's contribution to {@link FileHistoryStore.diffStats}, shared by both bundled backends:
|
|
645
|
+
* `undefined` = this file would NOT change (already at the target, or already absent for a deletion
|
|
646
|
+
* marker), otherwise the line arithmetic of the change. Content-hash proven, exactly like the
|
|
647
|
+
* restore's own identical-skip — a preview that used a cheaper test than the operation it previews
|
|
648
|
+
* would disagree with it.
|
|
649
|
+
*/
|
|
650
|
+
export declare function previewFileDelta(env: ExecutionEnv, absPath: string, targetHash: string | null, targetBytes: Uint8Array | undefined, targetMode?: number): Promise<{
|
|
651
|
+
insertions: number;
|
|
652
|
+
deletions: number;
|
|
653
|
+
} | undefined>;
|
|
654
|
+
interface FileHistoryVersionRecord {
|
|
655
|
+
version: number;
|
|
656
|
+
/** Content-addressed blob hash, or null = "did not exist at this point" (existed-not marker —
|
|
657
|
+
* serves both the pre-creation v1 arm and later deletion arms). */
|
|
658
|
+
blobHash: string | null;
|
|
659
|
+
mode?: number;
|
|
660
|
+
parentIdent?: string;
|
|
661
|
+
backupTime: string;
|
|
662
|
+
}
|
|
663
|
+
interface TrackedFileState {
|
|
664
|
+
/** DV-14: false ⇒ the pristine base could not be captured; a durable refusal state, not an
|
|
665
|
+
* absence — versions then start ABOVE v1 and v1 is never minted later. */
|
|
666
|
+
baseAvailable: boolean;
|
|
667
|
+
/** The key's self-description (§3 amendment): recorded at the mint so a graph that later crosses
|
|
668
|
+
* machines can be judged instead of guessed. */
|
|
669
|
+
keyDesc: FileHistoryTrackKeyDescriptor;
|
|
670
|
+
/** Set when a LATER trackEdit for this path was served (answered `minted:false`) — that lane is
|
|
671
|
+
* now relying on this record as its own pre-edit baseline, so the original minter may no longer
|
|
672
|
+
* retract it (adversarial round 1: two lanes racing one path, the first failing its write, would
|
|
673
|
+
* otherwise delete the baseline the SECOND lane's successful edit depends on). */
|
|
674
|
+
observedByLaterTouch?: boolean;
|
|
675
|
+
versions: FileHistoryVersionRecord[];
|
|
676
|
+
}
|
|
677
|
+
/** One staged per-file outcome of a capture attempt (commit assigns version numbers atomically). */
|
|
678
|
+
type StagedFileCapture = {
|
|
679
|
+
kind: "unchanged";
|
|
680
|
+
version: number;
|
|
681
|
+
} | {
|
|
682
|
+
kind: "changed";
|
|
683
|
+
blobHash: string | null;
|
|
684
|
+
mode?: number;
|
|
685
|
+
parentIdent?: string;
|
|
686
|
+
};
|
|
687
|
+
/**
|
|
688
|
+
* In-memory, content-addressed reference {@link FileHistoryStore}. Blobs are deduplicated by sha256
|
|
689
|
+
* ACROSS all scopes and versions (a file unchanged between boundaries stores zero new bytes); each
|
|
690
|
+
* tracked file keeps a version chain, each boundary a complete `trackKey → version` mapping over
|
|
691
|
+
* the tracked set as of its mint. Single-process (the default-deps reference; the durable
|
|
692
|
+
* file-backed impl is design/381 slice 2).
|
|
693
|
+
*
|
|
694
|
+
* Grey-box fault seams (design/381 §7 C7 / §10 T-4): `persistFirstTouch` and `publishBoundary` are
|
|
695
|
+
* `protected` COMMIT points — a test subclass overrides one to throw and the public verb answers
|
|
696
|
+
* the typed `{ok:false}` the contract promises (the store itself still never throws).
|
|
697
|
+
*
|
|
698
|
+
* RETENTION is built in (design/381 §片2): each committed boundary is followed by a trim back to
|
|
699
|
+
* {@link import("./file-history-retention.js").DEFAULT_FILE_HISTORY_BOUNDARY_KEEP} newest boundaries per scope, overridable through
|
|
700
|
+
* {@link InMemoryFileHistoryStoreOptions.retention}. Even a single-process reference store needs
|
|
701
|
+
* it — a long-lived process IS the unbounded case.
|
|
702
|
+
*/
|
|
703
|
+
export interface InMemoryFileHistoryStoreOptions {
|
|
704
|
+
/** design/381 §片2 — the built-in boundary bound and its override. Omitted ⇒ the engine default
|
|
705
|
+
* ({@link import("./file-history-retention.js").DEFAULT_FILE_HISTORY_BOUNDARY_KEEP}); a malformed policy is REFUSED here, at the wiring
|
|
706
|
+
* site, rather than silently becoming a different policy inside a GC pass nobody watches. */
|
|
707
|
+
retention?: FileHistoryRetentionPolicy;
|
|
708
|
+
}
|
|
709
|
+
export declare class InMemoryFileHistoryStore implements FileHistoryStore {
|
|
710
|
+
private readonly scopes;
|
|
711
|
+
/** Resolved boundary cap, or `null` for the explicit `"unbounded"` opt-out (host owns the GC). */
|
|
712
|
+
private readonly boundaryKeep;
|
|
713
|
+
private readonly blobs;
|
|
714
|
+
/** Blob hashes written by an in-flight capture BEFORE its commit — a racing reap must treat them
|
|
715
|
+
* as live (same MAJOR-1 posture as the retired whole-tree store). REFCOUNTED, not a set
|
|
716
|
+
* (adversarial round): two concurrent captures can lease the same new hash, and the first
|
|
717
|
+
* attempt's release must not strip the second attempt's liveness. */
|
|
718
|
+
private readonly inFlightBlobs;
|
|
719
|
+
constructor(opts?: InMemoryFileHistoryStoreOptions);
|
|
720
|
+
/**
|
|
721
|
+
* The RETENTION pass, run after a boundary commits (design/381 §片2 — see {@link FileHistoryStore.reap}
|
|
722
|
+
* for why the backend owns this rather than the host). Boundaries are an insertion-ordered map
|
|
723
|
+
* here, so commit order is exact and the selection needs no clock.
|
|
724
|
+
*
|
|
725
|
+
* Never throws and never fails the commit that triggered it: the boundary is already published and
|
|
726
|
+
* a GC that could not run is a graph that is merely larger than its bound — the strictly safer of
|
|
727
|
+
* the two failure directions, and the same posture the reference implementation's own eviction has.
|
|
728
|
+
*/
|
|
729
|
+
private applyBoundaryRetention;
|
|
730
|
+
private leaseBlob;
|
|
731
|
+
private releaseBlob;
|
|
732
|
+
private scope;
|
|
733
|
+
/** PROTECTED COMMIT SEAM (fault-injectable): durably record one path's first-touch state.
|
|
734
|
+
* Synchronous single assignment = the "ONE atomic durable record" of DV-14 for this backend. */
|
|
735
|
+
protected persistFirstTouch(scope: string, key: string, state: TrackedFileState): void;
|
|
736
|
+
/** PROTECTED COMMIT SEAM (fault-injectable): publish a captured boundary in ONE synchronous step
|
|
737
|
+
* — version records + mapping become visible together or not at all (atomic visibility). */
|
|
738
|
+
protected publishBoundary(scope: string, entryId: string, staged: Map<string, StagedFileCapture>): void;
|
|
739
|
+
trackEdit(scope: string, absPath: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryTrackResult>;
|
|
740
|
+
annulTrack(scope: string, absPath: string, root: string, verify?: {
|
|
741
|
+
env: ExecutionEnv;
|
|
742
|
+
signal?: AbortSignal;
|
|
743
|
+
}): Promise<FileHistoryResult>;
|
|
744
|
+
snapshot(scope: string, entryId: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryResult>;
|
|
745
|
+
restore(scope: string, entryId: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryRestoreResult>;
|
|
746
|
+
canRestore(scope: string, entryId: string): Promise<boolean>;
|
|
747
|
+
diffStats(scope: string, entryId: string, env: ExecutionEnv, root: string): Promise<FileHistoryDiffStats | null>;
|
|
748
|
+
reap(scope: string, keepEntryIds: string[]): Promise<number>;
|
|
749
|
+
adoptScope(fromScope: string, toScope: string): Promise<FileHistoryResult>;
|
|
750
|
+
exportHistory(scope: string): Promise<FileHistoryExport | null>;
|
|
751
|
+
/** The export projection as a SYNCHRONOUS function — the import's final claim needs to compare a
|
|
752
|
+
* target's identity without yielding (see the publish step). */
|
|
753
|
+
private exportOf;
|
|
754
|
+
importHistory(scope: string, data: FileHistoryExport, srcGetBlob: (hash: string) => Promise<Uint8Array | undefined>): Promise<FileHistoryResult>;
|
|
755
|
+
/** The import body inside the blob lease (see {@link importHistory}). */
|
|
756
|
+
private importStaged;
|
|
757
|
+
}
|
|
758
|
+
/** Test-only window (family-aware path-coordinate pins + the DV-6 track-key coordinate): pure
|
|
759
|
+
* helpers whose behavior carries a fence/restore invariant. Not re-exported from src/index.ts. */
|
|
760
|
+
export declare const __testables: {
|
|
761
|
+
joinUnder: typeof joinUnder;
|
|
762
|
+
relUnder: typeof relUnder;
|
|
763
|
+
stripTrailSep: (p: string) => string;
|
|
764
|
+
trackKeyOf: typeof trackKeyOf;
|
|
765
|
+
resolveTrackKey: typeof resolveTrackKey;
|
|
766
|
+
parentDirOf: typeof parentDirOf;
|
|
767
|
+
};
|
|
768
|
+
export {};
|