@sentropic/h2a-runtime 0.98.7-rc.1 → 0.98.7-rc.3
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/dist/agent-launch-args.d.ts.map +1 -1
- package/dist/agent-launch-args.js +5 -5
- package/dist/agent-launch-args.js.map +1 -1
- package/dist/agy-claude.d.ts +2 -0
- package/dist/agy-claude.d.ts.map +1 -0
- package/dist/agy-claude.js +3 -0
- package/dist/agy-claude.js.map +1 -0
- package/dist/agy-headless.d.ts +11 -0
- package/dist/agy-headless.d.ts.map +1 -0
- package/dist/agy-headless.js +104 -0
- package/dist/agy-headless.js.map +1 -0
- package/dist/claude-code-cli-isolation.d.ts +53 -0
- package/dist/claude-code-cli-isolation.d.ts.map +1 -0
- package/dist/claude-code-cli-isolation.js +277 -0
- package/dist/claude-code-cli-isolation.js.map +1 -0
- package/dist/claude-code-cli-lifecycle.d.ts +80 -0
- package/dist/claude-code-cli-lifecycle.d.ts.map +1 -0
- package/dist/claude-code-cli-lifecycle.js +402 -0
- package/dist/claude-code-cli-lifecycle.js.map +1 -0
- package/dist/claude-code-cli-runner.d.ts +89 -0
- package/dist/claude-code-cli-runner.d.ts.map +1 -0
- package/dist/claude-code-cli-runner.js +659 -0
- package/dist/claude-code-cli-runner.js.map +1 -0
- package/dist/claude-seat-input.d.ts +47 -0
- package/dist/claude-seat-input.d.ts.map +1 -0
- package/dist/claude-seat-input.js +272 -0
- package/dist/claude-seat-input.js.map +1 -0
- package/dist/claude-seat-mesh-contract.d.ts +88 -0
- package/dist/claude-seat-mesh-contract.d.ts.map +1 -0
- package/dist/claude-seat-mesh-contract.js +13 -0
- package/dist/claude-seat-mesh-contract.js.map +1 -0
- package/dist/delegate.d.ts +10 -0
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +29 -0
- package/dist/delegate.js.map +1 -1
- package/dist/gateway-host/claude-seat-client.d.ts +32 -0
- package/dist/gateway-host/claude-seat-client.d.ts.map +1 -0
- package/dist/gateway-host/claude-seat-client.js +38 -0
- package/dist/gateway-host/claude-seat-client.js.map +1 -0
- package/dist/gateway-host/host.d.ts +6 -0
- package/dist/gateway-host/host.d.ts.map +1 -1
- package/dist/gateway-host/host.js +24 -3
- package/dist/gateway-host/host.js.map +1 -1
- package/dist/gateway-host/live-auth.d.ts +62 -0
- package/dist/gateway-host/live-auth.d.ts.map +1 -0
- package/dist/gateway-host/live-auth.js +87 -0
- package/dist/gateway-host/live-auth.js.map +1 -0
- package/dist/gateway-host/live-declarations.d.ts.map +1 -1
- package/dist/gateway-host/live-declarations.js +13 -7
- package/dist/gateway-host/live-declarations.js.map +1 -1
- package/dist/gateway-host/live-disclosure.d.ts +124 -0
- package/dist/gateway-host/live-disclosure.d.ts.map +1 -0
- package/dist/gateway-host/live-disclosure.js +347 -0
- package/dist/gateway-host/live-disclosure.js.map +1 -0
- package/dist/gateway-host/live-github.d.ts +11 -0
- package/dist/gateway-host/live-github.d.ts.map +1 -0
- package/dist/gateway-host/live-github.js +374 -0
- package/dist/gateway-host/live-github.js.map +1 -0
- package/dist/gateway-host/live-host.d.ts +20 -0
- package/dist/gateway-host/live-host.d.ts.map +1 -0
- package/dist/gateway-host/live-host.js +14 -0
- package/dist/gateway-host/live-host.js.map +1 -0
- package/dist/gateway-host/live-object-store.d.ts +35 -0
- package/dist/gateway-host/live-object-store.d.ts.map +1 -0
- package/dist/gateway-host/live-object-store.js +447 -0
- package/dist/gateway-host/live-object-store.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +211 -22
- package/dist/index.js.map +1 -1
- package/dist/llm-mesh-accounts.d.ts +70 -4
- package/dist/llm-mesh-accounts.d.ts.map +1 -1
- package/dist/llm-mesh-accounts.js +296 -9
- package/dist/llm-mesh-accounts.js.map +1 -1
- package/dist/native-host.d.ts +1 -1
- package/dist/native-host.d.ts.map +1 -1
- package/dist/native-host.js +3 -12
- package/dist/native-host.js.map +1 -1
- package/dist/native-terminal/host.d.ts +6 -1
- package/dist/native-terminal/host.d.ts.map +1 -1
- package/dist/native-terminal/host.js +6 -25
- package/dist/native-terminal/host.js.map +1 -1
- package/dist/proc-identity.d.ts +31 -0
- package/dist/proc-identity.d.ts.map +1 -1
- package/dist/proc-identity.js +54 -0
- package/dist/proc-identity.js.map +1 -1
- package/dist/prompt-delivery.d.ts +3 -0
- package/dist/prompt-delivery.d.ts.map +1 -1
- package/dist/prompt-delivery.js +8 -3
- package/dist/prompt-delivery.js.map +1 -1
- package/dist/tmux.d.ts +3 -1
- package/dist/tmux.d.ts.map +1 -1
- package/dist/tmux.js +13 -9
- package/dist/tmux.js.map +1 -1
- package/dist/vendor/engram-live/contracts.d.ts +619 -2
- package/dist/vendor/engram-live/contracts.js +1201 -25
- package/dist/vendor/engram-live/integration.d.ts +44 -4
- package/dist/vendor/engram-live/integration.js +65 -5
- package/dist/vendor/engram-live/pr-sync.d.ts +144 -0
- package/dist/vendor/engram-live/pr-sync.js +411 -0
- package/dist/vendor/engram-live/registration.d.ts +2 -2
- package/dist/vendor/engram-live/registration.js +2 -4
- package/dist/vendor/engram-live/retention.d.ts +56 -0
- package/dist/vendor/engram-live/retention.js +179 -0
- package/package.json +1 -1
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// @sentropic/engram-live/pr-sync — P1 neutral PR/object sync engine
|
|
2
|
+
// (spec/SPEC_LIVE_GIT_INDEXING.md §§4–5; plan/09 P1).
|
|
3
|
+
//
|
|
4
|
+
// Pagination/overlap/full-sweep reconciliation plus neutral PR/object jobs
|
|
5
|
+
// over INJECTED ports only: the GitHub page transport and the durable
|
|
6
|
+
// persistence are host-supplied fakes here (H/S implement production sides).
|
|
7
|
+
// Engram performs no network call, persists no token, invokes no
|
|
8
|
+
// repository-selected credential helper, and applies no count or age limit:
|
|
9
|
+
// every page is consumed, every state indexed, nothing erased.
|
|
10
|
+
import { PR_END_PROOF, validatePrPage, validatePrSyncCursor, validatePullRequestFact, } from "./contracts.js";
|
|
11
|
+
/** Neutral PR sync extractor version (checkpoints are per version). */
|
|
12
|
+
export const PR_SYNC_EXTRACTOR_VERSION = "engram-live/pr-sync/1";
|
|
13
|
+
/**
|
|
14
|
+
* Overlap re-ask on incremental retries (milliseconds): the resume window
|
|
15
|
+
* re-reads recently updated PRs so a page that moved during enumeration is
|
|
16
|
+
* deduplicated instead of lost. Pure constant, consumed by hosts.
|
|
17
|
+
*/
|
|
18
|
+
export const PR_SYNC_OVERLAP_MS = 5 * 60 * 1000;
|
|
19
|
+
export class PrSyncError extends Error {
|
|
20
|
+
kind;
|
|
21
|
+
constructor(kind, message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "PrSyncError";
|
|
24
|
+
this.kind = kind;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Identity of one persisted observation: the complete observed fact. Two
|
|
29
|
+
* rows sharing one head OID but differing by state, draft flag, refs, base,
|
|
30
|
+
* merge evidence or timestamp are different observations and all persist;
|
|
31
|
+
* only a byte-identical repeat collapses. The (PR, head OID) key is reserved
|
|
32
|
+
* for object-job deduplication (objectJobIdentityKey), never for dropping
|
|
33
|
+
* observations.
|
|
34
|
+
*/
|
|
35
|
+
export function observationIdentityKey(fact) {
|
|
36
|
+
const mergeOid = fact.mergeOid ?? null;
|
|
37
|
+
return JSON.stringify([
|
|
38
|
+
fact.repositoryOwner,
|
|
39
|
+
fact.repositoryRepo,
|
|
40
|
+
fact.number,
|
|
41
|
+
fact.state,
|
|
42
|
+
fact.isDraft,
|
|
43
|
+
fact.updatedAt,
|
|
44
|
+
fact.headOid.algorithm,
|
|
45
|
+
fact.headOid.oid,
|
|
46
|
+
fact.headRef,
|
|
47
|
+
fact.baseOid.algorithm,
|
|
48
|
+
fact.baseOid.oid,
|
|
49
|
+
fact.baseRef,
|
|
50
|
+
mergeOid === null ? null : [mergeOid.algorithm, mergeOid.oid],
|
|
51
|
+
fact.mergeStrategy ?? null,
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Identity of one object-acquisition job: the PR plus the observed head OID.
|
|
56
|
+
* Two jobs for one PR with different heads are different rows; recording
|
|
57
|
+
* head B never replaces head A's job, failures or receipts.
|
|
58
|
+
*/
|
|
59
|
+
export function objectJobIdentityKey(job) {
|
|
60
|
+
return `${job.repositoryOwner}/${job.repositoryRepo}#${job.prNumber}@${job.headOid.algorithm}:${job.headOid.oid}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Deduplicate exact observation repeats by complete observation identity,
|
|
64
|
+
* keeping one row per distinct fact. Observations whose metadata differs
|
|
65
|
+
* (state, base, merge evidence, timestamp, …) are all kept: this is the
|
|
66
|
+
* persistence rule (nothing erased), not the current view.
|
|
67
|
+
*/
|
|
68
|
+
export function deduplicatePrObservations(items) {
|
|
69
|
+
const kept = new Map();
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
const checked = validatePullRequestFact(item);
|
|
72
|
+
if (!checked.ok) {
|
|
73
|
+
throw new PrSyncError("protocol", `invalid PR item #${item.number}: ${checked.errors.join("; ")}`);
|
|
74
|
+
}
|
|
75
|
+
const key = observationIdentityKey(item);
|
|
76
|
+
if (!kept.has(key)) {
|
|
77
|
+
kept.set(key, item);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return [...kept.values()].sort((left, right) => left.number - right.number || left.updatedAt - right.updatedAt);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Current view over observations: one row per PR identity
|
|
84
|
+
* (owner/repo/number), keeping the newest updatedAt record. Computed
|
|
85
|
+
* separately AFTER every distinct observation is retained, so the live head
|
|
86
|
+
* never erases the observed history.
|
|
87
|
+
*/
|
|
88
|
+
export function deduplicatePrPageItems(items) {
|
|
89
|
+
const newest = new Map();
|
|
90
|
+
for (const item of items) {
|
|
91
|
+
const checked = validatePullRequestFact(item);
|
|
92
|
+
if (!checked.ok) {
|
|
93
|
+
throw new PrSyncError("protocol", `invalid PR item #${item.number}: ${checked.errors.join("; ")}`);
|
|
94
|
+
}
|
|
95
|
+
const key = `${item.repositoryOwner}/${item.repositoryRepo}#${item.number}`;
|
|
96
|
+
const previous = newest.get(key);
|
|
97
|
+
if (previous === undefined || item.updatedAt >= previous.updatedAt) {
|
|
98
|
+
newest.set(key, item);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return [...newest.values()].sort((left, right) => left.number - right.number);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Refuse any row that does not belong to the requested repository: the
|
|
105
|
+
* stored cursor and every received observation must carry the canonical
|
|
106
|
+
* requested identity before any persistence or completion. A foreign row is
|
|
107
|
+
* a protocol failure, never a silent adoption.
|
|
108
|
+
*/
|
|
109
|
+
export function assertSameRepository(expected, owner, repo, what) {
|
|
110
|
+
if (owner !== expected.owner || repo !== expected.repo) {
|
|
111
|
+
throw new PrSyncError("protocol", `${what} targets ${owner}/${repo} but the requested repository is ${expected.owner}/${expected.repo} (foreign rows never persist, foreign sweeps never complete)`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* ETag position key: one validator per (filter, cursor) position inside the
|
|
116
|
+
* repository pass, so page N's ETag is never replayed for page M.
|
|
117
|
+
*/
|
|
118
|
+
export function etagPositionKey(cursor, updatedAtSince) {
|
|
119
|
+
return `${updatedAtSince === null ? "-" : String(updatedAtSince)}|${cursor === null ? "<start>" : cursor}`;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Require the explicit end-of-list proof: hasMore false without endProof is
|
|
123
|
+
* a protocol failure, never a silent completion. hasMore true without a
|
|
124
|
+
* continuation cursor is the same failure class.
|
|
125
|
+
*/
|
|
126
|
+
export function requireEndOfListProof(page) {
|
|
127
|
+
const checked = validatePrPage(page);
|
|
128
|
+
if (!checked.ok) {
|
|
129
|
+
throw new PrSyncError("protocol", `invalid PR page: ${checked.errors.join("; ")}`);
|
|
130
|
+
}
|
|
131
|
+
if (page.hasMore === false && page.endProof !== PR_END_PROOF) {
|
|
132
|
+
throw new PrSyncError("protocol", `PR sweep ends without end-of-list proof (hasMore false without endProof "${PR_END_PROOF}")`);
|
|
133
|
+
}
|
|
134
|
+
if (page.hasMore === true && (page.nextCursor === null || page.nextCursor.length === 0)) {
|
|
135
|
+
throw new PrSyncError("protocol", "PR sweep continues without a continuation cursor (hasMore true without nextCursor)");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* S mapping rule: metadata receipts never acknowledge a failed private
|
|
140
|
+
* object acquisition. True only when every job is acquired AND verified:
|
|
141
|
+
* the acquired OID equals the observed head OID, no residual error stands,
|
|
142
|
+
* and known counters agree exactly (a partial count never acknowledges).
|
|
143
|
+
*/
|
|
144
|
+
export function canAcknowledgePrObjects(jobs) {
|
|
145
|
+
if (jobs.length === 0)
|
|
146
|
+
return false;
|
|
147
|
+
return jobs.every((job) => {
|
|
148
|
+
if (job.state !== "acquired")
|
|
149
|
+
return false;
|
|
150
|
+
if (job.acquiredOid === undefined || job.acquiredOid === null)
|
|
151
|
+
return false;
|
|
152
|
+
if (job.acquiredOid.algorithm !== job.headOid.algorithm || job.acquiredOid.oid !== job.headOid.oid)
|
|
153
|
+
return false;
|
|
154
|
+
if ((job.lastError ?? null) !== null)
|
|
155
|
+
return false;
|
|
156
|
+
const total = job.totalCommits ?? null;
|
|
157
|
+
const acquired = job.acquiredCommits ?? null;
|
|
158
|
+
if (total !== null && total <= 0)
|
|
159
|
+
return false;
|
|
160
|
+
if (total !== null && (acquired === null || acquired !== total))
|
|
161
|
+
return false;
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function staleCoverage(kind, lastSuccessAt, message) {
|
|
166
|
+
const state = kind === "auth_required" || kind === "unavailable" ? "unavailable" : "stale";
|
|
167
|
+
return [
|
|
168
|
+
{
|
|
169
|
+
axis: "pr-metadata",
|
|
170
|
+
state,
|
|
171
|
+
reason: `${kind}: ${message}; last success preserved, resume automatically with stale coverage`,
|
|
172
|
+
...(lastSuccessAt !== null ? { lastSuccessAt } : {}),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
axis: "object-availability",
|
|
176
|
+
state: "unavailable",
|
|
177
|
+
reason: `object jobs untouched by the metadata ${kind}; failed jobs stay failed until acquired`,
|
|
178
|
+
...(lastSuccessAt !== null ? { lastSuccessAt } : {}),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
axis: "pr-attribution",
|
|
182
|
+
state: "partial",
|
|
183
|
+
reason: "attribution stays pinned to the last observed bounds while the transport is down",
|
|
184
|
+
...(lastSuccessAt !== null ? { lastSuccessAt } : {}),
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Recreate the pending object job of every persisted observation that has
|
|
190
|
+
* none: after an interruption between saveObservation and recordObjectJob,
|
|
191
|
+
* the observation is durable but its acquisition is not. The resume — this
|
|
192
|
+
* pass's start and its proven end — heals that gap for retained observations,
|
|
193
|
+
* even ones this sweep does not re-observe.
|
|
194
|
+
*/
|
|
195
|
+
async function ensureObjectJobsForObservations(persistence, repository) {
|
|
196
|
+
const observations = await persistence.listObservations(repository);
|
|
197
|
+
if (observations.length === 0)
|
|
198
|
+
return;
|
|
199
|
+
const jobs = await persistence.listObjectJobs(repository);
|
|
200
|
+
const known = new Set(jobs.map((job) => objectJobIdentityKey(job)));
|
|
201
|
+
for (const item of observations) {
|
|
202
|
+
const key = objectJobIdentityKey({
|
|
203
|
+
repositoryOwner: item.repositoryOwner,
|
|
204
|
+
repositoryRepo: item.repositoryRepo,
|
|
205
|
+
prNumber: item.number,
|
|
206
|
+
headOid: item.headOid,
|
|
207
|
+
});
|
|
208
|
+
if (known.has(key))
|
|
209
|
+
continue;
|
|
210
|
+
await persistence.recordObjectJob({
|
|
211
|
+
repositoryOwner: item.repositoryOwner,
|
|
212
|
+
repositoryRepo: item.repositoryRepo,
|
|
213
|
+
prNumber: item.number,
|
|
214
|
+
headOid: item.headOid,
|
|
215
|
+
state: "pending",
|
|
216
|
+
attempts: 0,
|
|
217
|
+
lastError: null,
|
|
218
|
+
acquiredOid: null,
|
|
219
|
+
totalCommits: null,
|
|
220
|
+
acquiredCommits: null,
|
|
221
|
+
});
|
|
222
|
+
known.add(key);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Run one neutral PR sync pass: full pagination with overlap/dedup, durable
|
|
227
|
+
* cursor advance only after proven completion, object jobs recorded as
|
|
228
|
+
* pending independently of metadata, failures preserving last success with
|
|
229
|
+
* stale coverage and durable retry state for automatic resume.
|
|
230
|
+
*/
|
|
231
|
+
export async function runPrSyncPass(input, ports) {
|
|
232
|
+
const previous = await ports.persistence.loadCursor(input.repository);
|
|
233
|
+
if (previous !== null) {
|
|
234
|
+
const checked = validatePrSyncCursor(previous);
|
|
235
|
+
if (!checked.ok) {
|
|
236
|
+
throw new PrSyncError("protocol", `invalid stored PR cursor: ${checked.errors.join("; ")}`);
|
|
237
|
+
}
|
|
238
|
+
assertSameRepository(input.repository, previous.repositoryOwner, previous.repositoryRepo, "stored PR cursor");
|
|
239
|
+
}
|
|
240
|
+
const updatedAtSince = input.sweepKind === "incremental" && previous?.updatedAtCursor !== null && previous?.updatedAtCursor !== undefined
|
|
241
|
+
? Math.max(0, previous.updatedAtCursor - PR_SYNC_OVERLAP_MS)
|
|
242
|
+
: null;
|
|
243
|
+
// Validators stay bound to their position: one ETag per (filter, cursor).
|
|
244
|
+
// A scalar lastEtag from an older cursor is never replayed: without a
|
|
245
|
+
// recorded position it could validate the wrong page, so the request goes
|
|
246
|
+
// out unconditionally instead.
|
|
247
|
+
// Resume heals a prior interruption between saveObservation and
|
|
248
|
+
// recordObjectJob before any new page lands.
|
|
249
|
+
await ensureObjectJobsForObservations(ports.persistence, input.repository);
|
|
250
|
+
const etags = { ...(previous?.etagsByCursor ?? {}) };
|
|
251
|
+
const seen = [];
|
|
252
|
+
const seenKeys = new Set();
|
|
253
|
+
let exactDuplicates = 0;
|
|
254
|
+
let cursor = null;
|
|
255
|
+
let pages = 0;
|
|
256
|
+
let lastEtag = previous?.lastEtag ?? null;
|
|
257
|
+
let maxUpdatedAt = previous?.updatedAtCursor ?? null;
|
|
258
|
+
const keepFailureCursor = async (kind, attempts, nextAfter) => {
|
|
259
|
+
const kept = {
|
|
260
|
+
repositoryOwner: input.repository.owner,
|
|
261
|
+
repositoryRepo: input.repository.repo,
|
|
262
|
+
updatedAtCursor: previous?.updatedAtCursor ?? null,
|
|
263
|
+
lastSuccessAt: previous?.lastSuccessAt ?? null,
|
|
264
|
+
lastEtag,
|
|
265
|
+
etagsByCursor: { ...etags },
|
|
266
|
+
sweepKind: input.sweepKind,
|
|
267
|
+
pendingRetry: { kind, attempts, nextAfter },
|
|
268
|
+
};
|
|
269
|
+
await ports.persistence.saveCursor(kept);
|
|
270
|
+
return kept;
|
|
271
|
+
};
|
|
272
|
+
for (;;) {
|
|
273
|
+
const position = etagPositionKey(cursor, updatedAtSince);
|
|
274
|
+
let result;
|
|
275
|
+
try {
|
|
276
|
+
result = await ports.transport.fetchPage({
|
|
277
|
+
repository: input.repository,
|
|
278
|
+
cursor,
|
|
279
|
+
updatedAtSince,
|
|
280
|
+
etag: etags[position] ?? null,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
const kind = "offline";
|
|
285
|
+
const kept = await keepFailureCursor(kind, (previous?.pendingRetry?.attempts ?? 0) + 1, input.now + 60_000);
|
|
286
|
+
const observations = await ports.persistence.listObservations(input.repository);
|
|
287
|
+
return {
|
|
288
|
+
state: "stale",
|
|
289
|
+
observations,
|
|
290
|
+
cursor: kept,
|
|
291
|
+
coverage: staleCoverage(kind, kept.lastSuccessAt, error instanceof Error ? error.message : String(error)),
|
|
292
|
+
stats: { pages, itemsSeen: seen.length, unique: 0, duplicates: exactDuplicates, observations: observations.length, objectJobsPending: 0 },
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (result.ok === false) {
|
|
296
|
+
const kept = await keepFailureCursor(result.kind, (previous?.pendingRetry?.attempts ?? 0) + 1, input.now + (result.retryAfterMs ?? 60_000));
|
|
297
|
+
const observations = await ports.persistence.listObservations(input.repository);
|
|
298
|
+
return {
|
|
299
|
+
state: "stale",
|
|
300
|
+
observations,
|
|
301
|
+
cursor: kept,
|
|
302
|
+
coverage: staleCoverage(result.kind, kept.lastSuccessAt, result.message),
|
|
303
|
+
stats: { pages, itemsSeen: seen.length, unique: 0, duplicates: exactDuplicates, observations: observations.length, objectJobsPending: 0 },
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// Proven page first: every page is validated, and the terminator proof
|
|
307
|
+
// is enforced per page so a truncated sweep never completes silently.
|
|
308
|
+
const pageChecked = validatePrPage(result.page);
|
|
309
|
+
if (!pageChecked.ok) {
|
|
310
|
+
throw new PrSyncError("protocol", `invalid PR page: ${pageChecked.errors.join("; ")}`);
|
|
311
|
+
}
|
|
312
|
+
// Repository identity precedes any persistence: foreign rows never land.
|
|
313
|
+
for (const item of result.page.items) {
|
|
314
|
+
assertSameRepository(input.repository, item.repositoryOwner, item.repositoryRepo, `PR #${item.number} observation`);
|
|
315
|
+
}
|
|
316
|
+
// Per-page checkpoint: every distinct observation and its object job are
|
|
317
|
+
// persisted now, so a later failure keeps the received pages. Only the
|
|
318
|
+
// global success cursor waits for the proven end of list.
|
|
319
|
+
for (const item of result.page.items) {
|
|
320
|
+
seen.push(item);
|
|
321
|
+
const key = observationIdentityKey(item);
|
|
322
|
+
if (seenKeys.has(key)) {
|
|
323
|
+
exactDuplicates += 1;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
seenKeys.add(key);
|
|
327
|
+
await ports.persistence.saveObservation(item);
|
|
328
|
+
if (maxUpdatedAt === null || item.updatedAt > maxUpdatedAt)
|
|
329
|
+
maxUpdatedAt = item.updatedAt;
|
|
330
|
+
// Object jobs advance independently and per OID: a new head OID adds a
|
|
331
|
+
// pending job while the previous head's job — its failures and receipts
|
|
332
|
+
// included — stays untouched. Metadata success never marks a job
|
|
333
|
+
// acquired or failed.
|
|
334
|
+
const jobs = await ports.persistence.listObjectJobs(input.repository);
|
|
335
|
+
const existing = jobs.find((job) => job.prNumber === item.number
|
|
336
|
+
&& job.headOid.algorithm === item.headOid.algorithm
|
|
337
|
+
&& job.headOid.oid === item.headOid.oid);
|
|
338
|
+
if (existing === undefined) {
|
|
339
|
+
await ports.persistence.recordObjectJob({
|
|
340
|
+
repositoryOwner: item.repositoryOwner,
|
|
341
|
+
repositoryRepo: item.repositoryRepo,
|
|
342
|
+
prNumber: item.number,
|
|
343
|
+
headOid: item.headOid,
|
|
344
|
+
state: "pending",
|
|
345
|
+
attempts: 0,
|
|
346
|
+
lastError: null,
|
|
347
|
+
acquiredOid: null,
|
|
348
|
+
totalCommits: null,
|
|
349
|
+
acquiredCommits: null,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
pages += 1;
|
|
354
|
+
if (result.page.etag !== undefined && result.page.etag !== null) {
|
|
355
|
+
lastEtag = result.page.etag;
|
|
356
|
+
etags[position] = result.page.etag;
|
|
357
|
+
}
|
|
358
|
+
if (result.page.hasMore === false) {
|
|
359
|
+
requireEndOfListProof(result.page);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
requireEndOfListProof({ ...result.page, hasMore: true, nextCursor: result.page.nextCursor });
|
|
363
|
+
cursor = result.page.nextCursor;
|
|
364
|
+
}
|
|
365
|
+
// The current view (newest per PR) is computed separately after every
|
|
366
|
+
// distinct observation has been retained.
|
|
367
|
+
const current = deduplicatePrPageItems(seen);
|
|
368
|
+
const cursorOut = {
|
|
369
|
+
repositoryOwner: input.repository.owner,
|
|
370
|
+
repositoryRepo: input.repository.repo,
|
|
371
|
+
updatedAtCursor: maxUpdatedAt,
|
|
372
|
+
lastSuccessAt: input.now,
|
|
373
|
+
lastEtag,
|
|
374
|
+
etagsByCursor: { ...etags },
|
|
375
|
+
sweepKind: input.sweepKind,
|
|
376
|
+
pendingRetry: null,
|
|
377
|
+
};
|
|
378
|
+
// Proven end heals an interruption inside this sweep: every persisted
|
|
379
|
+
// observation leaves with its acquisition job before completion.
|
|
380
|
+
await ensureObjectJobsForObservations(ports.persistence, input.repository);
|
|
381
|
+
await ports.persistence.saveCursor(cursorOut);
|
|
382
|
+
const observations = await ports.persistence.listObservations(input.repository);
|
|
383
|
+
const jobs = await ports.persistence.listObjectJobs(input.repository);
|
|
384
|
+
const pending = jobs.filter((job) => job.state !== "acquired").length;
|
|
385
|
+
const objectsAcknowledged = canAcknowledgePrObjects(jobs);
|
|
386
|
+
return {
|
|
387
|
+
state: "complete",
|
|
388
|
+
observations,
|
|
389
|
+
cursor: cursorOut,
|
|
390
|
+
coverage: [
|
|
391
|
+
{ axis: "pr-metadata", state: "current", reason: `full sweep proven with ${PR_END_PROOF} over ${pages} pages`, lastSuccessAt: input.now },
|
|
392
|
+
{
|
|
393
|
+
axis: "object-availability",
|
|
394
|
+
state: objectsAcknowledged ? "current" : "unavailable",
|
|
395
|
+
reason: objectsAcknowledged
|
|
396
|
+
? "every object job acquired and verified against its observed head"
|
|
397
|
+
: `${jobs.length} object job(s), ${pending} pending or failed or unverified: metadata success completes no object job`,
|
|
398
|
+
lastSuccessAt: input.now,
|
|
399
|
+
},
|
|
400
|
+
{ axis: "pr-attribution", state: "partial", reason: "attribution pins per-PR bounds separately (see pr-attribution)", lastSuccessAt: input.now },
|
|
401
|
+
],
|
|
402
|
+
stats: {
|
|
403
|
+
pages,
|
|
404
|
+
itemsSeen: seen.length,
|
|
405
|
+
unique: current.length,
|
|
406
|
+
duplicates: exactDuplicates,
|
|
407
|
+
observations: observations.length,
|
|
408
|
+
objectJobsPending: pending,
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Vendored
|
|
1
|
+
// Vendored from rhanka/engram@545a57b8911ab108e84fa1778e0f268fe11bf1de: engram-live/registration.ts.
|
|
2
|
+
// Compiled with TypeScript 5.9.3 (ES2022/NodeNext, declarations); do not hand-edit.
|
|
2
3
|
import { type CloneIdentityDto, type DeclarationRevocationDto, type IdentityUncertaintyState, type NonActivatingLifecycleKind, type OidAlgorithm, type RevocationReason, type WorkspaceDeclarationDto, type WorktreeIdentityDto } from "./contracts.js";
|
|
3
4
|
/** Input for an explicit live declaration (revision starts at INITIAL_DECLARATION_REVISION). */
|
|
4
5
|
export interface CreateLiveDeclarationInput {
|
|
@@ -135,4 +136,3 @@ export declare function isSameClone(a: Pick<CloneIdentityDto, "cloneId">, b: Pic
|
|
|
135
136
|
* are refused.
|
|
136
137
|
*/
|
|
137
138
|
export declare function classifyLifecycleEvent(kind: NonActivatingLifecycleKind): "no-activation-change";
|
|
138
|
-
//# sourceMappingURL=registration.d.ts.map
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
// Vendored from
|
|
2
|
-
//
|
|
3
|
-
// tarball sha512 in the GUARD test). Portable: node:crypto plus the sibling
|
|
4
|
-
// contracts.js only. Do not hand-edit.
|
|
1
|
+
// Vendored from rhanka/engram@545a57b8911ab108e84fa1778e0f268fe11bf1de: engram-live/registration.ts.
|
|
2
|
+
// Compiled with TypeScript 5.9.3 (ES2022/NodeNext, declarations); do not hand-edit.
|
|
5
3
|
// @sentropic/engram-live/registration — pure neutral registration helpers
|
|
6
4
|
// (spec/SPEC_LIVE_GIT_INDEXING.md §§2–3, 10, 12; plan/09 L6a; owner Q4 A).
|
|
7
5
|
//
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Vendored from rhanka/engram@545a57b8911ab108e84fa1778e0f268fe11bf1de: engram-live/retention.ts.
|
|
2
|
+
// Compiled with TypeScript 5.9.3 (ES2022/NodeNext, declarations); do not hand-edit.
|
|
3
|
+
import { type LiveScope, type RetentionCoverageReportDto, type RetentionReceiptDto, type RetentionReceiptQuery, type RetentionRequestDto, type SelectedRootDto } from "./contracts.js";
|
|
4
|
+
export declare class RetentionError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* THE injected retention port (plan/09 R1: a port, never a base). Engram
|
|
9
|
+
* defines the contract only; h2a implements durable acquisition and the
|
|
10
|
+
* private store behind it. Receipts are scoped by tenant + clone and
|
|
11
|
+
* durable across passes; the port never carries file content.
|
|
12
|
+
*/
|
|
13
|
+
export interface RetentionPort {
|
|
14
|
+
recordRequest(request: RetentionRequestDto): Promise<{
|
|
15
|
+
received: true;
|
|
16
|
+
}>;
|
|
17
|
+
recordReceipt(receipt: RetentionReceiptDto): Promise<{
|
|
18
|
+
received: true;
|
|
19
|
+
}>;
|
|
20
|
+
loadReceipts(query: RetentionReceiptQuery): Promise<{
|
|
21
|
+
receipts: readonly RetentionReceiptDto[];
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface RetentionPassInput {
|
|
25
|
+
readonly roots: readonly SelectedRootDto[];
|
|
26
|
+
readonly scope: LiveScope;
|
|
27
|
+
/** Injected evaluation time for requestedAt (ms); the engine reads no clock. */
|
|
28
|
+
readonly now: number;
|
|
29
|
+
}
|
|
30
|
+
export interface RetentionPassStats {
|
|
31
|
+
readonly requests: number;
|
|
32
|
+
readonly receipts: number;
|
|
33
|
+
readonly missing: number;
|
|
34
|
+
}
|
|
35
|
+
export interface RetentionPassOutcome {
|
|
36
|
+
/** "complete": every request has its scoped durable receipt. "gaps": otherwise. */
|
|
37
|
+
readonly state: "complete" | "gaps";
|
|
38
|
+
readonly requests: readonly RetentionRequestDto[];
|
|
39
|
+
readonly coverage: readonly RetentionCoverageReportDto[];
|
|
40
|
+
readonly stats: RetentionPassStats;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Derive one retention request per distinct root OID, sorted by OID.
|
|
44
|
+
* Pure and total over valid inputs: no count cap, no age filter, no
|
|
45
|
+
* pruning — an old default branch, an expired window branch kept as a
|
|
46
|
+
* previously-ingested root, a PR head of any state and a detached HEAD
|
|
47
|
+
* each yield exactly one request.
|
|
48
|
+
*/
|
|
49
|
+
export declare function deriveRetentionRequests(roots: readonly SelectedRootDto[], scope: LiveScope, now: number): RetentionRequestDto[];
|
|
50
|
+
/**
|
|
51
|
+
* Run one retention pass: record every request, then require every scoped
|
|
52
|
+
* durable receipt. Deterministic over the injected inputs; the port is the
|
|
53
|
+
* only effect. A receipt satisfies a request only under the exact same
|
|
54
|
+
* tenant, clone, algorithm and OID; cross-scope receipts never satisfy.
|
|
55
|
+
*/
|
|
56
|
+
export declare function runRetentionPass(input: RetentionPassInput, port: RetentionPort): Promise<RetentionPassOutcome>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Vendored from rhanka/engram@545a57b8911ab108e84fa1778e0f268fe11bf1de: engram-live/retention.ts.
|
|
2
|
+
// Compiled with TypeScript 5.9.3 (ES2022/NodeNext, declarations); do not hand-edit.
|
|
3
|
+
// @sentropic/engram-live/retention — R1 private retention of every ingested root
|
|
4
|
+
// (spec/SPEC_LIVE_GIT_INDEXING.md §4, A02/A04; plan/09 R1).
|
|
5
|
+
//
|
|
6
|
+
// The neutral retention engine. Engram derives one scoped retention request
|
|
7
|
+
// per distinct ingested root OID and requires one scoped durable receipt
|
|
8
|
+
// before any reconstructible-coverage claim:
|
|
9
|
+
//
|
|
10
|
+
// 1. derive the requests from the L2b selected roots (./refs.js): default
|
|
11
|
+
// branch even when old, active local/remote branches, PR heads of all
|
|
12
|
+
// states, detached worktree HEADs, previously ingested roots — with no
|
|
13
|
+
// commit-count and no commit-age limit, and nothing ever erased here;
|
|
14
|
+
// 2. record every request through the INJECTED retention port (a port,
|
|
15
|
+
// never a base; engram owns no database and no object store);
|
|
16
|
+
// 3. load the scoped durable receipts from that same port and classify
|
|
17
|
+
// coverage: every request needs its tenant + clone receipt; a missing
|
|
18
|
+
// receipt is an explicit object-availability gap, never a silent
|
|
19
|
+
// current claim.
|
|
20
|
+
//
|
|
21
|
+
// Neutrality: this module imports ONLY ./contracts.js (data-pure DTOs and
|
|
22
|
+
// validators). No h2a, no cluster-mesh, no sentropic, no storage driver,
|
|
23
|
+
// no network, no filesystem, no clock read — the evaluation time `now` is
|
|
24
|
+
// injected, and durability comes from the host port alone.
|
|
25
|
+
//
|
|
26
|
+
// Host contract (H: live-object-store.ts, delegate.ts — NOT implemented
|
|
27
|
+
// here): h2a consumes RetentionRequestDto through RetentionPort, acquires
|
|
28
|
+
// the pinned committed objects into its protected private Git store under
|
|
29
|
+
// private refs (no writes into the observed clone, no borrowed alternates
|
|
30
|
+
// store, no repository-controlled programs), then records a
|
|
31
|
+
// RetentionReceiptDto with an opaque coordinated-backup backupRef for L8.
|
|
32
|
+
// If removal wins the acquisition race, H records no receipt and the engine
|
|
33
|
+
// reports the gap; quotas pause further work without deleting protected
|
|
34
|
+
// roots. P1 reuses this facility for PR roots. Uncommitted plaintext never
|
|
35
|
+
// enters a request or a receipt: requests carry OIDs only, by construction.
|
|
36
|
+
import { RETENTION_COVERAGE_AXIS, validateLiveScope, validateQualifiedOid, validateRetentionReceipt, validateRetentionRequest, } from "./contracts.js";
|
|
37
|
+
export class RetentionError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "RetentionError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function checkScope(scope) {
|
|
44
|
+
const checked = validateLiveScope(scope);
|
|
45
|
+
if (!checked.ok) {
|
|
46
|
+
throw new RetentionError(`invalid retention scope: ${checked.errors.join("; ")}`);
|
|
47
|
+
}
|
|
48
|
+
if (scope.worktree !== undefined) {
|
|
49
|
+
throw new RetentionError("retention scope must be clone-scoped: worktree must be absent (retention is per tenant + clone)");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function checkNow(now) {
|
|
53
|
+
if (typeof now !== "number" || !Number.isSafeInteger(now) || now <= 0) {
|
|
54
|
+
throw new RetentionError("retention pass requires a positive safe-integer now (milliseconds)");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Derive one retention request per distinct root OID, sorted by OID.
|
|
59
|
+
* Pure and total over valid inputs: no count cap, no age filter, no
|
|
60
|
+
* pruning — an old default branch, an expired window branch kept as a
|
|
61
|
+
* previously-ingested root, a PR head of any state and a detached HEAD
|
|
62
|
+
* each yield exactly one request.
|
|
63
|
+
*/
|
|
64
|
+
export function deriveRetentionRequests(roots, scope, now) {
|
|
65
|
+
checkScope(scope);
|
|
66
|
+
checkNow(now);
|
|
67
|
+
// Immutable snapshot: the caller-owned scope object may be shared and
|
|
68
|
+
// mutated while the pass awaits the port; every derived request carries
|
|
69
|
+
// the pass-entry scope, never a later mutation.
|
|
70
|
+
const frozen = { tenant: scope.tenant, clone: scope.clone };
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
const requests = [];
|
|
73
|
+
for (const root of roots) {
|
|
74
|
+
const checked = validateQualifiedOid(root.qualifiedOid);
|
|
75
|
+
if (!checked.ok) {
|
|
76
|
+
throw new RetentionError(`invalid root OID ${root.qualifiedOid.oid}: ${checked.errors.join("; ")}`);
|
|
77
|
+
}
|
|
78
|
+
if (seen.has(root.qualifiedOid.oid))
|
|
79
|
+
continue;
|
|
80
|
+
seen.add(root.qualifiedOid.oid);
|
|
81
|
+
if (root.sources.length === 0) {
|
|
82
|
+
throw new RetentionError(`root ${root.qualifiedOid.oid} carries no provenance source`);
|
|
83
|
+
}
|
|
84
|
+
const first = root.sources[0];
|
|
85
|
+
const refBacked = root.sources.find((source) => source.refName !== null);
|
|
86
|
+
const request = {
|
|
87
|
+
scope: { tenant: frozen.tenant, clone: frozen.clone },
|
|
88
|
+
algorithm: root.qualifiedOid.algorithm,
|
|
89
|
+
oid: root.qualifiedOid.oid,
|
|
90
|
+
firstSignal: first.signal,
|
|
91
|
+
refName: refBacked !== undefined ? refBacked.refName : null,
|
|
92
|
+
requestedAt: now,
|
|
93
|
+
};
|
|
94
|
+
const valid = validateRetentionRequest(request);
|
|
95
|
+
if (!valid.ok) {
|
|
96
|
+
throw new RetentionError(`derived invalid retention request for ${request.oid}: ${valid.errors.join("; ")}`);
|
|
97
|
+
}
|
|
98
|
+
requests.push(request);
|
|
99
|
+
}
|
|
100
|
+
requests.sort((left, right) => left.oid.localeCompare(right.oid));
|
|
101
|
+
return requests;
|
|
102
|
+
}
|
|
103
|
+
function receiptKey(receipt) {
|
|
104
|
+
return `${receipt.scope.tenant}:${receipt.scope.clone}:${receipt.algorithm}:${receipt.oid}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Run one retention pass: record every request, then require every scoped
|
|
108
|
+
* durable receipt. Deterministic over the injected inputs; the port is the
|
|
109
|
+
* only effect. A receipt satisfies a request only under the exact same
|
|
110
|
+
* tenant, clone, algorithm and OID; cross-scope receipts never satisfy.
|
|
111
|
+
*/
|
|
112
|
+
export async function runRetentionPass(input, port) {
|
|
113
|
+
checkScope(input.scope);
|
|
114
|
+
checkNow(input.now);
|
|
115
|
+
// Immutable snapshot taken before the first await: the caller-owned
|
|
116
|
+
// input.scope object may be shared and mutated while the pass awaits the
|
|
117
|
+
// port. Every query, receipt check and missing-key below uses this
|
|
118
|
+
// pass-entry scope; re-reading input.scope after an await would let a
|
|
119
|
+
// foreign-scope receipt falsely satisfy a request (false acquittement).
|
|
120
|
+
const scope = { tenant: input.scope.tenant, clone: input.scope.clone };
|
|
121
|
+
const requests = deriveRetentionRequests(input.roots, scope, input.now);
|
|
122
|
+
for (const request of requests) {
|
|
123
|
+
await port.recordRequest(request);
|
|
124
|
+
}
|
|
125
|
+
const algorithms = new Set();
|
|
126
|
+
for (const request of requests)
|
|
127
|
+
algorithms.add(request.algorithm);
|
|
128
|
+
const receipts = [];
|
|
129
|
+
if (algorithms.size === 0) {
|
|
130
|
+
return { state: "complete", requests, coverage: [], stats: { requests: 0, receipts: 0, missing: 0 } };
|
|
131
|
+
}
|
|
132
|
+
for (const algorithm of algorithms) {
|
|
133
|
+
const loaded = await port.loadReceipts({ scope, algorithm });
|
|
134
|
+
for (const receipt of loaded.receipts) {
|
|
135
|
+
const checked = validateRetentionReceipt(receipt);
|
|
136
|
+
if (!checked.ok) {
|
|
137
|
+
throw new RetentionError(`invalid retention receipt for ${receipt.oid}: ${checked.errors.join("; ")}`);
|
|
138
|
+
}
|
|
139
|
+
// The port is scoped by the query, but the engine re-checks scope:
|
|
140
|
+
// a foreign receipt that slipped through never satisfies a request.
|
|
141
|
+
if (receipt.scope.tenant !== scope.tenant || receipt.scope.clone !== scope.clone)
|
|
142
|
+
continue;
|
|
143
|
+
if (receipt.algorithm !== algorithm)
|
|
144
|
+
continue;
|
|
145
|
+
receipts.push(receipt);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const byKey = new Map();
|
|
149
|
+
for (const receipt of receipts)
|
|
150
|
+
byKey.set(receiptKey(receipt), receipt);
|
|
151
|
+
const missing = [];
|
|
152
|
+
for (const request of requests) {
|
|
153
|
+
const key = `${scope.tenant}:${scope.clone}:${request.algorithm}:${request.oid}`;
|
|
154
|
+
if (!byKey.has(key))
|
|
155
|
+
missing.push({ algorithm: request.algorithm, oid: request.oid });
|
|
156
|
+
}
|
|
157
|
+
missing.sort((left, right) => left.oid.localeCompare(right.oid));
|
|
158
|
+
if (missing.length === 0) {
|
|
159
|
+
return {
|
|
160
|
+
state: "complete",
|
|
161
|
+
requests,
|
|
162
|
+
coverage: [],
|
|
163
|
+
stats: { requests: requests.length, receipts: byKey.size, missing: 0 },
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const preview = missing.slice(0, 3).map((entry) => entry.oid.slice(0, 12)).join(", ");
|
|
167
|
+
return {
|
|
168
|
+
state: "gaps",
|
|
169
|
+
requests,
|
|
170
|
+
coverage: [{
|
|
171
|
+
axis: RETENTION_COVERAGE_AXIS,
|
|
172
|
+
state: "unavailable",
|
|
173
|
+
reason: `private retention incomplete: ${missing.length} of ${requests.length} ingested root(s) without a scoped durable retention receipt (${preview}…); no reconstructible coverage is claimed and nothing was pruned`,
|
|
174
|
+
cause: "missing-receipt",
|
|
175
|
+
missing,
|
|
176
|
+
}],
|
|
177
|
+
stats: { requests: requests.length, receipts: byKey.size, missing: missing.length },
|
|
178
|
+
};
|
|
179
|
+
}
|