@sentropic/h2a-runtime 0.98.6 → 0.98.7-rc.2

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.
Files changed (130) hide show
  1. package/dist/agent-launch-args.d.ts.map +1 -1
  2. package/dist/agent-launch-args.js +5 -5
  3. package/dist/agent-launch-args.js.map +1 -1
  4. package/dist/agy-claude.d.ts +2 -0
  5. package/dist/agy-claude.d.ts.map +1 -0
  6. package/dist/agy-claude.js +3 -0
  7. package/dist/agy-claude.js.map +1 -0
  8. package/dist/agy-headless.d.ts +11 -0
  9. package/dist/agy-headless.d.ts.map +1 -0
  10. package/dist/agy-headless.js +104 -0
  11. package/dist/agy-headless.js.map +1 -0
  12. package/dist/attach.d.ts +2 -0
  13. package/dist/attach.d.ts.map +1 -1
  14. package/dist/attach.js +73 -6
  15. package/dist/attach.js.map +1 -1
  16. package/dist/auth-login.d.ts +24 -1
  17. package/dist/auth-login.d.ts.map +1 -1
  18. package/dist/auth-login.js +132 -4
  19. package/dist/auth-login.js.map +1 -1
  20. package/dist/central-mcp.d.ts +2 -0
  21. package/dist/central-mcp.d.ts.map +1 -1
  22. package/dist/central-mcp.js +90 -3
  23. package/dist/central-mcp.js.map +1 -1
  24. package/dist/claude-code-cli-isolation.d.ts +53 -0
  25. package/dist/claude-code-cli-isolation.d.ts.map +1 -0
  26. package/dist/claude-code-cli-isolation.js +277 -0
  27. package/dist/claude-code-cli-isolation.js.map +1 -0
  28. package/dist/claude-code-cli-lifecycle.d.ts +80 -0
  29. package/dist/claude-code-cli-lifecycle.d.ts.map +1 -0
  30. package/dist/claude-code-cli-lifecycle.js +402 -0
  31. package/dist/claude-code-cli-lifecycle.js.map +1 -0
  32. package/dist/claude-code-cli-runner.d.ts +89 -0
  33. package/dist/claude-code-cli-runner.d.ts.map +1 -0
  34. package/dist/claude-code-cli-runner.js +659 -0
  35. package/dist/claude-code-cli-runner.js.map +1 -0
  36. package/dist/claude-seat-input.d.ts +47 -0
  37. package/dist/claude-seat-input.d.ts.map +1 -0
  38. package/dist/claude-seat-input.js +272 -0
  39. package/dist/claude-seat-input.js.map +1 -0
  40. package/dist/claude-seat-mesh-contract.d.ts +88 -0
  41. package/dist/claude-seat-mesh-contract.d.ts.map +1 -0
  42. package/dist/claude-seat-mesh-contract.js +13 -0
  43. package/dist/claude-seat-mesh-contract.js.map +1 -0
  44. package/dist/cli-help-groups.js +1 -1
  45. package/dist/cli-help-groups.js.map +1 -1
  46. package/dist/crash-log.d.ts +112 -0
  47. package/dist/crash-log.d.ts.map +1 -0
  48. package/dist/crash-log.js +560 -0
  49. package/dist/crash-log.js.map +1 -0
  50. package/dist/delegate.d.ts +10 -0
  51. package/dist/delegate.d.ts.map +1 -1
  52. package/dist/delegate.js +29 -0
  53. package/dist/delegate.js.map +1 -1
  54. package/dist/gateway-host/claude-seat-client.d.ts +32 -0
  55. package/dist/gateway-host/claude-seat-client.d.ts.map +1 -0
  56. package/dist/gateway-host/claude-seat-client.js +38 -0
  57. package/dist/gateway-host/claude-seat-client.js.map +1 -0
  58. package/dist/gateway-host/host.d.ts +6 -0
  59. package/dist/gateway-host/host.d.ts.map +1 -1
  60. package/dist/gateway-host/host.js +24 -3
  61. package/dist/gateway-host/host.js.map +1 -1
  62. package/dist/gateway-host/live-declarations.d.ts +58 -0
  63. package/dist/gateway-host/live-declarations.d.ts.map +1 -0
  64. package/dist/gateway-host/live-declarations.js +429 -0
  65. package/dist/gateway-host/live-declarations.js.map +1 -0
  66. package/dist/gateway-host/live-github.d.ts +11 -0
  67. package/dist/gateway-host/live-github.d.ts.map +1 -0
  68. package/dist/gateway-host/live-github.js +374 -0
  69. package/dist/gateway-host/live-github.js.map +1 -0
  70. package/dist/gateway-host/live-object-store.d.ts +35 -0
  71. package/dist/gateway-host/live-object-store.d.ts.map +1 -0
  72. package/dist/gateway-host/live-object-store.js +447 -0
  73. package/dist/gateway-host/live-object-store.js.map +1 -0
  74. package/dist/gateway-host/live-registration.d.ts +162 -0
  75. package/dist/gateway-host/live-registration.d.ts.map +1 -0
  76. package/dist/gateway-host/live-registration.js +455 -0
  77. package/dist/gateway-host/live-registration.js.map +1 -0
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +290 -23
  80. package/dist/index.js.map +1 -1
  81. package/dist/llm-mesh-accounts.d.ts +70 -4
  82. package/dist/llm-mesh-accounts.d.ts.map +1 -1
  83. package/dist/llm-mesh-accounts.js +296 -9
  84. package/dist/llm-mesh-accounts.js.map +1 -1
  85. package/dist/native-host.d.ts +1 -1
  86. package/dist/native-host.d.ts.map +1 -1
  87. package/dist/native-host.js +30 -17
  88. package/dist/native-host.js.map +1 -1
  89. package/dist/native-terminal/host.d.ts +6 -1
  90. package/dist/native-terminal/host.d.ts.map +1 -1
  91. package/dist/native-terminal/host.js +6 -25
  92. package/dist/native-terminal/host.js.map +1 -1
  93. package/dist/native-terminal/journal.d.ts +5 -0
  94. package/dist/native-terminal/journal.d.ts.map +1 -1
  95. package/dist/native-terminal/journal.js +28 -16
  96. package/dist/native-terminal/journal.js.map +1 -1
  97. package/dist/native-terminal/op.d.ts +13 -0
  98. package/dist/native-terminal/op.d.ts.map +1 -1
  99. package/dist/native-terminal/op.js +64 -5
  100. package/dist/native-terminal/op.js.map +1 -1
  101. package/dist/proc-identity.d.ts +31 -0
  102. package/dist/proc-identity.d.ts.map +1 -1
  103. package/dist/proc-identity.js +54 -0
  104. package/dist/proc-identity.js.map +1 -1
  105. package/dist/prompt-delivery.d.ts +3 -0
  106. package/dist/prompt-delivery.d.ts.map +1 -1
  107. package/dist/prompt-delivery.js +8 -3
  108. package/dist/prompt-delivery.js.map +1 -1
  109. package/dist/run.d.ts.map +1 -1
  110. package/dist/run.js +79 -16
  111. package/dist/run.js.map +1 -1
  112. package/dist/terminal-reset.d.ts +104 -0
  113. package/dist/terminal-reset.d.ts.map +1 -0
  114. package/dist/terminal-reset.js +229 -0
  115. package/dist/terminal-reset.js.map +1 -0
  116. package/dist/tmux.d.ts +26 -4
  117. package/dist/tmux.d.ts.map +1 -1
  118. package/dist/tmux.js +60 -25
  119. package/dist/tmux.js.map +1 -1
  120. package/dist/vendor/engram-live/contracts.d.ts +825 -0
  121. package/dist/vendor/engram-live/contracts.js +1734 -0
  122. package/dist/vendor/engram-live/integration.d.ts +113 -0
  123. package/dist/vendor/engram-live/integration.js +254 -0
  124. package/dist/vendor/engram-live/pr-sync.d.ts +144 -0
  125. package/dist/vendor/engram-live/pr-sync.js +411 -0
  126. package/dist/vendor/engram-live/registration.d.ts +138 -0
  127. package/dist/vendor/engram-live/registration.js +358 -0
  128. package/dist/vendor/engram-live/retention.d.ts +56 -0
  129. package/dist/vendor/engram-live/retention.js +179 -0
  130. package/package.json +2 -2
@@ -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
+ }
@@ -0,0 +1,138 @@
1
+ // Vendored from rhanka/engram@545a57b8911ab108e84fa1778e0f268fe11bf1de: engram-live/registration.ts.
2
+ // Compiled with TypeScript 5.9.3 (ES2022/NodeNext, declarations); do not hand-edit.
3
+ import { type CloneIdentityDto, type DeclarationRevocationDto, type IdentityUncertaintyState, type NonActivatingLifecycleKind, type OidAlgorithm, type RevocationReason, type WorkspaceDeclarationDto, type WorktreeIdentityDto } from "./contracts.js";
4
+ /** Input for an explicit live declaration (revision starts at INITIAL_DECLARATION_REVISION). */
5
+ export interface CreateLiveDeclarationInput {
6
+ readonly tenant: string;
7
+ readonly principal: string;
8
+ readonly hostFingerprint?: string;
9
+ readonly identityUncertainty: readonly IdentityUncertaintyState[];
10
+ readonly declarationId: string;
11
+ readonly cloneId: string;
12
+ readonly worktreeId: string;
13
+ readonly authorizedRoots: readonly string[];
14
+ readonly liveCapability: "live";
15
+ readonly policyVersion: string;
16
+ readonly serverWorkspaceId?: string;
17
+ readonly serverIssuer?: string;
18
+ readonly bindingRevision?: number;
19
+ }
20
+ /** Patchable declaration fields. Identity (tenant, principal,
21
+ * hostFingerprint, the declarationId/cloneId/worktreeId triangle,
22
+ * liveCapability, declarationRevision) is immutable across revisions. */
23
+ export interface LiveDeclarationPatch {
24
+ readonly authorizedRoots?: readonly string[];
25
+ readonly policyVersion?: string;
26
+ readonly serverWorkspaceId?: string;
27
+ readonly serverIssuer?: string;
28
+ readonly bindingRevision?: number;
29
+ }
30
+ /** Input for deriving a clone identity from host-resolved components. */
31
+ export interface DeriveCloneIdentityInput {
32
+ readonly hostScope: string;
33
+ readonly commonDirRealpath: string;
34
+ readonly commonDirProjectFingerprint?: string;
35
+ readonly identityUncertainty?: readonly IdentityUncertaintyState[];
36
+ readonly oidAlgorithm: OidAlgorithm;
37
+ }
38
+ /** Input for deriving a worktree identity under an already derived clone. */
39
+ export interface DeriveWorktreeIdentityInput {
40
+ readonly hostScope: string;
41
+ readonly clone: Pick<CloneIdentityDto, "cloneId">;
42
+ readonly worktreePath: string;
43
+ readonly gitDir: string;
44
+ readonly worktreeFingerprint?: string;
45
+ readonly identityUncertainty?: readonly IdentityUncertaintyState[];
46
+ }
47
+ /** Input for deriving the identity of one declaration under a clone/worktree. */
48
+ export interface DeriveDeclarationIdentityInput {
49
+ readonly hostScope: string;
50
+ readonly clone: Pick<CloneIdentityDto, "cloneId">;
51
+ readonly worktree: Pick<WorktreeIdentityDto, "worktreeId">;
52
+ }
53
+ /** Opaque declaration identity: the declaration and its clone/worktree attachment. */
54
+ export interface DeclarationIdentityDto {
55
+ readonly declarationId: string;
56
+ readonly cloneId: string;
57
+ readonly worktreeId: string;
58
+ }
59
+ /**
60
+ * Create the initial live declaration for one workspace. Requires the
61
+ * explicit "live" capability and the full identity triangle
62
+ * (declarationId/cloneId/worktreeId, minted by the canonical derivation
63
+ * below): any other opt-in value (bind, upload, session kinds) is refused,
64
+ * so no lifecycle event can implicitly activate live. The declaration
65
+ * revision starts at the frozen INITIAL_DECLARATION_REVISION.
66
+ */
67
+ export declare function createLiveDeclaration(input: CreateLiveDeclarationInput): WorkspaceDeclarationDto;
68
+ /**
69
+ * Revise a declaration as a pure revision increment. The revision grows by
70
+ * exactly one; tenant, principal, hostFingerprint, the identity triangle
71
+ * and the live capability are immutable. The product binding is the
72
+ * complete (serverWorkspaceId, serverIssuer, bindingRevision) triplet.
73
+ * A revision outside the binding (roots, policy version) PRESERVES the
74
+ * binding: no renewal is imposed. Only a change of serverWorkspaceId or
75
+ * serverIssuer, or an explicit bindingRevision revalidation, renews the
76
+ * binding — and then strictly increasingly: the first binding carries
77
+ * exactly INITIAL_BINDING_REVISION, and every rebind/revalidation carries
78
+ * a strictly greater bindingRevision; binding revisions never go
79
+ * backwards. The host must additionally refuse to redirect in-flight
80
+ * collection/publication when a rebind lands.
81
+ */
82
+ export declare function reviseLiveDeclaration(previous: WorkspaceDeclarationDto, patch: LiveDeclarationPatch): WorkspaceDeclarationDto;
83
+ /**
84
+ * Revoke exactly one declaration as a pure private-purge request. The
85
+ * returned DTO names the single target by its opaque declarationId and
86
+ * carries the declaration's cloneId/worktreeId attachment and revision, so
87
+ * the recipient can never confuse two declarations of one tenant that
88
+ * happen to share public fingerprints or revisions. The host stops work,
89
+ * rejects delayed jobs and purges private snapshots, derived indexes and
90
+ * decryptability, while preserving authorized committed history.
91
+ * Revocation deletes no committed facts.
92
+ */
93
+ export declare function revokeLiveDeclaration(declaration: WorkspaceDeclarationDto, reason: RevocationReason): DeclarationRevocationDto;
94
+ /**
95
+ * Derive the clone identity for one common-dir — the ONE canonical clone
96
+ * derivation. The key components (hostScope, normalized common-dir
97
+ * realpath, common-dir project fingerprint) serialize through
98
+ * canonicalIdKey (unambiguous, NUL-refusing) and hash to the opaque
99
+ * cloneId: equal fingerprints under different realpaths derive distinct
100
+ * clones, while linked worktrees sharing one common-dir derive the same
101
+ * clone. The fingerprint may be absent only under explicit
102
+ * "missing_history" uncertainty; the hostScope + realpath components keep
103
+ * clones distinct even then (LIVE §12). The realpath stays host-local;
104
+ * only cloneId is shared. Deterministic and free of I/O.
105
+ */
106
+ export declare function deriveCloneIdentity(input: DeriveCloneIdentityInput): CloneIdentityDto;
107
+ /**
108
+ * Derive the identity of one worktree under an already derived clone —
109
+ * the ONE canonical worktree derivation. Worktrees of one clone share
110
+ * cloneId and keep an isolated host-scoped worktreeId plus per-worktree
111
+ * git-dir, fingerprint and path. The worktree fingerprint may be absent
112
+ * only under explicit "missing_history" uncertainty (LIVE §12).
113
+ */
114
+ export declare function deriveWorktreeIdentity(input: DeriveWorktreeIdentityInput): WorktreeIdentityDto;
115
+ /**
116
+ * Derive the identity of one declaration — the ONE canonical declaration
117
+ * derivation. One declaration per activated worktree: the declarationId
118
+ * binds the host scope, the clone and the worktree, so two worktrees of
119
+ * one clone (and two clones of one project) derive distinct declarationIds
120
+ * even without fingerprints. Stable across every revision of the same
121
+ * declaration. Deterministic and free of I/O.
122
+ */
123
+ export declare function deriveDeclarationIdentity(input: DeriveDeclarationIdentityInput): DeclarationIdentityDto;
124
+ /**
125
+ * Decide whether two clone references denote the same clone, by canonical
126
+ * identifier only: the cloneId minted by the shared canonical derivation is
127
+ * the clone identity, so the comparison is an equivalence (transitive and
128
+ * consistent between full DTOs and opaque references). Throws when either
129
+ * reference lacks a cloneId — equality is never guessed from components.
130
+ */
131
+ export declare function isSameClone(a: Pick<CloneIdentityDto, "cloneId">, b: Pick<CloneIdentityDto, "cloneId">): boolean;
132
+ /**
133
+ * Classify a host lifecycle event against live declarations. Bind, upload,
134
+ * code-workspace creation and session open/end never activate or remove a
135
+ * declaration: the only outcome is "no-activation-change". Unknown kinds
136
+ * are refused.
137
+ */
138
+ export declare function classifyLifecycleEvent(kind: NonActivatingLifecycleKind): "no-activation-change";