opencode-swarm 7.153.2 → 7.154.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/background/pending-delegations.d.ts +29 -0
  2. package/dist/background/pr-subscriptions.d.ts +265 -27
  3. package/dist/cli/{config-doctor-tv0ar88m.js → config-doctor-keasv3r8.js} +2 -2
  4. package/dist/cli/{core-kmsm50zn.js → core-7aj5g3f4.js} +1 -1
  5. package/dist/cli/{curation-policy-twa6agf8.js → curation-policy-a6tve11y.js} +6 -6
  6. package/dist/cli/{curator-wxe863k9.js → curator-anz136hr.js} +29 -29
  7. package/dist/cli/{curator-llm-factory-dxkhdhwg.js → curator-llm-factory-cr5ckwbd.js} +29 -29
  8. package/dist/cli/{evidence-summary-service-9akrdg9t.js → evidence-summary-service-6tgme7p3.js} +12 -12
  9. package/dist/cli/{gate-evidence-bdzqbg2y.js → gate-evidence-36nfx4vb.js} +6 -6
  10. package/dist/cli/{guardrail-explain-kx9rem51.js → guardrail-explain-frb5xd5h.js} +30 -30
  11. package/dist/cli/{guardrail-log-zxzyx1de.js → guardrail-log-fvma65xm.js} +5 -5
  12. package/dist/cli/{guardrail-reset-1mc3st34.js → guardrail-reset-3qk6k334.js} +29 -29
  13. package/dist/cli/{hive-promoter-gncwtq0z.js → hive-promoter-v3dhtzfa.js} +29 -29
  14. package/dist/cli/{index-ebshrg4x.js → index-0vss7vdg.js} +3 -3
  15. package/dist/cli/{index-zppxpc3y.js → index-107cv0dr.js} +2 -2
  16. package/dist/cli/{index-hhv2pvab.js → index-155pbsan.js} +1 -1
  17. package/dist/cli/{index-21pt4jwf.js → index-1sjnqsc8.js} +1 -1
  18. package/dist/cli/{index-3ttktt56.js → index-2ppbt5cf.js} +1 -1
  19. package/dist/cli/{index-4pjkdcgc.js → index-3md256qr.js} +1 -1
  20. package/dist/cli/{index-yvepnbyd.js → index-4r46q7tk.js} +5 -5
  21. package/dist/cli/{index-3bh4h6ve.js → index-50cy1a3p.js} +7 -7
  22. package/dist/cli/{index-84m4per1.js → index-97q41ds3.js} +2 -2
  23. package/dist/cli/{index-x3y6zhgj.js → index-9wzgztj9.js} +1 -1
  24. package/dist/cli/{index-2mqmvxac.js → index-9xev69y4.js} +7 -1
  25. package/dist/cli/{index-02rbfrab.js → index-9znypky4.js} +4 -4
  26. package/dist/cli/{index-vhbds89e.js → index-akm1dbr9.js} +4 -4
  27. package/dist/cli/index-bmkxc6sa.js +1231 -0
  28. package/dist/cli/{index-td0jz8xe.js → index-c809a69b.js} +2 -2
  29. package/dist/cli/{index-a58nmnmz.js → index-fz9xcq6z.js} +5 -3
  30. package/dist/cli/{index-3cegckkq.js → index-garsang6.js} +1 -1
  31. package/dist/cli/{index-x8vw6ap1.js → index-h1g8w4v0.js} +59 -42
  32. package/dist/cli/{index-56ejcj07.js → index-h7pamys0.js} +4 -4
  33. package/dist/cli/{index-3fpqpyp6.js → index-p17hs70t.js} +2 -2
  34. package/dist/cli/{index-h06112sh.js → index-qm8b3aaj.js} +5 -5
  35. package/dist/cli/{index-j4f8sz67.js → index-r0yavfvm.js} +193 -102
  36. package/dist/cli/{index-kvk65qzz.js → index-sx5f2fq9.js} +3 -3
  37. package/dist/cli/{index-1gzdnjg9.js → index-tmgbwgsm.js} +2 -2
  38. package/dist/cli/{index-bw55r5bz.js → index-wmxke5se.js} +6 -6
  39. package/dist/cli/{index-0azn976d.js → index-wqp8kk0w.js} +7 -7
  40. package/dist/cli/{index-kenz5bja.js → index-wxpemcdj.js} +31 -31
  41. package/dist/cli/{index-891g7hbf.js → index-xagq35tf.js} +1 -1
  42. package/dist/cli/{index-pk1gbqmq.js → index-xdy28ngt.js} +1 -1
  43. package/dist/cli/{index-5zp4c36c.js → index-xsbf2mmf.js} +1 -1
  44. package/dist/cli/{index-h3zhb18p.js → index-y7gyhe13.js} +2 -2
  45. package/dist/cli/{index-5ad8xcv3.js → index-yy4yps8w.js} +2 -2
  46. package/dist/cli/{index-s6b8jgtn.js → index-zak84bqr.js} +1 -1
  47. package/dist/cli/index.js +29 -29
  48. package/dist/cli/{knowledge-escalator-gp3mpbqe.js → knowledge-escalator-1gjv97hj.js} +11 -11
  49. package/dist/cli/{knowledge-events-nvkhtqvn.js → knowledge-events-94hfvp85.js} +9 -9
  50. package/dist/cli/{knowledge-link-ge0rddxw.js → knowledge-link-fs8hgh4h.js} +5 -5
  51. package/dist/cli/{knowledge-store-2ngh8xvv.js → knowledge-store-g7grbt7h.js} +6 -6
  52. package/dist/cli/{knowledge-validator-4bphyrec.js → knowledge-validator-ep0ym1e4.js} +8 -8
  53. package/dist/cli/{pending-delegations-nk78wx7h.js → pending-delegations-0pf2dtwx.js} +3 -3
  54. package/dist/cli/{pr-subscriptions-dq1f61ye.js → pr-subscriptions-e70phj71.js} +15 -4
  55. package/dist/cli/{runner-n50w1y4w.js → runner-b7pskcxb.js} +6 -6
  56. package/dist/cli/{scan-cursor-c16xtygz.js → scan-cursor-8zps8v46.js} +7 -7
  57. package/dist/cli/{schema-83tbte2g.js → schema-8sx9jp7v.js} +1 -1
  58. package/dist/cli/{scope-persistence-gdbw8xh8.js → scope-persistence-zacyjnx9.js} +8 -8
  59. package/dist/cli/{skill-generator-gr13xeha.js → skill-generator-xs5n5j4k.js} +13 -13
  60. package/dist/cli/{telemetry-hh1j99za.js → telemetry-r8e8zey7.js} +1 -1
  61. package/dist/cli/{worktree-collision-ownership-4drcjymy.js → worktree-collision-ownership-yjk5ya71.js} +5 -5
  62. package/dist/cli/{worktree-isolation-jjkfg2pg.js → worktree-isolation-cpf6b8ns.js} +29 -29
  63. package/dist/commands/pr-monitor-status.d.ts +8 -1
  64. package/dist/config/schema.d.ts +8 -0
  65. package/dist/hooks/pr-review-resilience-circuit.d.ts +232 -0
  66. package/dist/hooks/pr-workflow-gate.d.ts +10 -11
  67. package/dist/index.js +298 -289
  68. package/dist/observability/catalog.d.ts +6 -3
  69. package/dist/telemetry.d.ts +22 -1
  70. package/package.json +1 -1
  71. package/dist/cli/index-meg33yyv.js +0 -247
@@ -0,0 +1,1231 @@
1
+ // @bun
2
+ import {
3
+ validateSwarmPath
4
+ } from "./index-4cgee5vh.js";
5
+ import {
6
+ atomicWriteSwarmFileSync
7
+ } from "./index-c809a69b.js";
8
+ import {
9
+ withEvidenceLock
10
+ } from "./index-xagq35tf.js";
11
+ import {
12
+ telemetry
13
+ } from "./index-h1g8w4v0.js";
14
+ import {
15
+ exports_external,
16
+ init_zod
17
+ } from "./index-bpmtbmy9.js";
18
+ import {
19
+ log
20
+ } from "./index-zgwm4ryv.js";
21
+
22
+ // src/background/pr-subscriptions.ts
23
+ init_zod();
24
+ import * as fs from "fs";
25
+ import * as path from "path";
26
+ var PR_SUBSCRIPTIONS_FILE = "pr-monitor/subscriptions.jsonl";
27
+ var PR_SUBSCRIPTIONS_CHECKPOINT_FILE = "pr-monitor/subscriptions.checkpoint.json";
28
+ var PR_SUBSCRIPTIONS_AUDIT_FILE = "pr-monitor/subscriptions.audit.jsonl";
29
+ var LEGACY_ARCHIVE_FILE = "pr-monitor/subscriptions.legacy.jsonl";
30
+ var FOREIGN_CHECKPOINT_FILE = "pr-monitor/subscriptions.checkpoint.foreign.json";
31
+ var CORRUPT_CHECKPOINT_FILE = "pr-monitor/subscriptions.checkpoint.corrupt.json";
32
+ var STORE_LOCK_AGENT = "pr-monitor";
33
+ var STORE_LOCK_TASK = "pr-subscriptions";
34
+ var PR_SUBSCRIPTION_LIMITS = {
35
+ readChunkBytes: 64 * 1024,
36
+ maxRecordBytes: 64 * 1024,
37
+ migrationChunkBytes: 1024 * 1024,
38
+ legacySourceMaxBytes: 64 * 1024 * 1024,
39
+ defaultMaxActiveSubscriptions: 20,
40
+ maxCheckpointRecords: 512,
41
+ maxCheckpointBytes: 256 * 1024,
42
+ checkpointHardReadBytes: 1024 * 1024,
43
+ terminalRecordsHigh: 60,
44
+ terminalRecordsLow: 30,
45
+ terminalMaxAgeMs: 30 * 86400000,
46
+ auditMaxLinesHigh: 500,
47
+ auditMaxLinesLow: 250,
48
+ auditMaxBytesHigh: 128 * 1024,
49
+ auditMaxBytesLow: 64 * 1024,
50
+ legacyArchiveTtlMs: 7 * 86400000,
51
+ bootstrapLockTimeoutMs: 5000
52
+ };
53
+ var onSubscriptionCreated = null;
54
+ function setOnSubscriptionCreated(callback) {
55
+ onSubscriptionCreated = callback;
56
+ }
57
+ var RecordSchema = exports_external.object({
58
+ correlationId: exports_external.string().min(1),
59
+ sessionID: exports_external.string().min(1),
60
+ prNumber: exports_external.number().int().positive(),
61
+ repoFullName: exports_external.string().regex(/^[^/]+\/[^/]+$/, "Must be owner/repo format"),
62
+ prUrl: exports_external.string().min(1).regex(/^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/, "Must be a valid GitHub PR URL"),
63
+ headRefOid: exports_external.string().optional(),
64
+ lastCheckedAt: exports_external.number(),
65
+ lastCommentId: exports_external.string().optional(),
66
+ lastCheckRunSet: exports_external.string().optional(),
67
+ mergeableState: exports_external.string().optional(),
68
+ mergeGroupRunStatus: exports_external.string().optional(),
69
+ mergeGroupRunConclusion: exports_external.string().optional(),
70
+ mergeGroupRunHtmlUrl: exports_external.string().optional(),
71
+ isWatching: exports_external.boolean(),
72
+ hasUnaddressedEvents: exports_external.boolean(),
73
+ status: exports_external.enum(["active", "removed", "expired"]),
74
+ createdAt: exports_external.number(),
75
+ updatedAt: exports_external.number(),
76
+ errorCount: exports_external.number().int().min(0),
77
+ customPollIntervalSeconds: exports_external.number().int().positive().optional(),
78
+ customFailureThreshold: exports_external.number().int().min(0).optional(),
79
+ customCooldownSeconds: exports_external.number().int().positive().optional()
80
+ }).strict();
81
+ var TerminalSummarySchema = exports_external.object({
82
+ removed: exports_external.number().int().min(0),
83
+ expired: exports_external.number().int().min(0),
84
+ lastTerminalAt: exports_external.number().int().nullable()
85
+ }).strict();
86
+ var MigrationSchema = exports_external.object({
87
+ scannedBytes: exports_external.number().int().nonnegative(),
88
+ sourceBytes: exports_external.number().int().nonnegative(),
89
+ sourceMtimeMs: exports_external.number().nonnegative(),
90
+ corruptLines: exports_external.number().int().nonnegative(),
91
+ done: exports_external.boolean(),
92
+ archived: exports_external.boolean(),
93
+ startedAt: exports_external.number().int().nonnegative()
94
+ }).strict();
95
+ var MaintenanceSchema = exports_external.object({
96
+ compactions: exports_external.number().int().nonnegative(),
97
+ droppedAuditTransitions: exports_external.number().int().nonnegative(),
98
+ corruptLegacyRecords: exports_external.number().int().nonnegative(),
99
+ lastCompactedAt: exports_external.number().int().nullable(),
100
+ resets: exports_external.number().int().nonnegative()
101
+ }).strict();
102
+ var CheckpointSchema = exports_external.object({
103
+ schemaVersion: exports_external.literal(1),
104
+ sequence: exports_external.number().int().positive(),
105
+ rootPath: exports_external.string().min(1).max(4096),
106
+ updatedAt: exports_external.number().int().nonnegative(),
107
+ records: exports_external.record(exports_external.string(), RecordSchema),
108
+ terminalSummary: TerminalSummarySchema,
109
+ migration: MigrationSchema.nullable(),
110
+ maintenance: MaintenanceSchema
111
+ }).strict();
112
+ function storePath(directory) {
113
+ return validateSwarmPath(directory, PR_SUBSCRIPTIONS_FILE);
114
+ }
115
+ function checkpointPath(directory) {
116
+ return validateSwarmPath(directory, PR_SUBSCRIPTIONS_CHECKPOINT_FILE);
117
+ }
118
+ function auditPath(directory) {
119
+ return validateSwarmPath(directory, PR_SUBSCRIPTIONS_AUDIT_FILE);
120
+ }
121
+ function legacyArchivePath(directory) {
122
+ return validateSwarmPath(directory, LEGACY_ARCHIVE_FILE);
123
+ }
124
+ function foreignSlotPath(directory) {
125
+ return validateSwarmPath(directory, FOREIGN_CHECKPOINT_FILE);
126
+ }
127
+ function corruptSlotPath(directory) {
128
+ return validateSwarmPath(directory, CORRUPT_CHECKPOINT_FILE);
129
+ }
130
+ function ensureSwarmDir(directory) {
131
+ fs.mkdirSync(path.resolve(directory, ".swarm", "pr-monitor"), {
132
+ recursive: true
133
+ });
134
+ }
135
+ function buildCorrelationId(sessionID, repoFullName, prNumber) {
136
+ return `${sessionID}::${repoFullName}::${prNumber}`;
137
+ }
138
+ function sameProjectRoot(recorded, current) {
139
+ const a = path.resolve(recorded);
140
+ const b = path.resolve(current);
141
+ if (process.platform === "win32")
142
+ return a.toLowerCase() === b.toLowerCase();
143
+ return a === b;
144
+ }
145
+ function fileSizeOrNull(filePath) {
146
+ try {
147
+ return fs.statSync(filePath).size;
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+ function renameWithRetry(from, to) {
153
+ let lastError;
154
+ for (let attempt = 0;attempt < 5; attempt++) {
155
+ try {
156
+ fs.renameSync(from, to);
157
+ return;
158
+ } catch (err) {
159
+ lastError = err;
160
+ const code = err.code;
161
+ if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY")
162
+ throw err;
163
+ try {
164
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 15);
165
+ } catch {}
166
+ }
167
+ }
168
+ throw lastError;
169
+ }
170
+ function foldLegacyRegion(filePath, startByte, maxBytes) {
171
+ const folded = new Map;
172
+ let corruptLines = 0;
173
+ let consumedTo = startByte;
174
+ let fd;
175
+ try {
176
+ fd = fs.openSync(filePath, "r");
177
+ } catch {
178
+ return {
179
+ folded,
180
+ nextByte: startByte,
181
+ eof: true,
182
+ aborted: true,
183
+ corruptLines
184
+ };
185
+ }
186
+ const processLineBytes = (line) => {
187
+ if (line.length === 0)
188
+ return;
189
+ const text = line.toString("utf-8");
190
+ const trimmed = text.trim();
191
+ if (trimmed.length === 0)
192
+ return;
193
+ let parsed;
194
+ try {
195
+ parsed = JSON.parse(trimmed);
196
+ } catch {
197
+ corruptLines += 1;
198
+ return;
199
+ }
200
+ const result = RecordSchema.safeParse(parsed);
201
+ if (!result.success) {
202
+ corruptLines += 1;
203
+ return;
204
+ }
205
+ const rec = result.data;
206
+ if (rec.correlationId !== buildCorrelationId(rec.sessionID, rec.repoFullName, rec.prNumber)) {
207
+ corruptLines += 1;
208
+ return;
209
+ }
210
+ folded.set(rec.correlationId, rec);
211
+ };
212
+ try {
213
+ const size = fs.fstatSync(fd).size;
214
+ if (startByte >= size) {
215
+ return {
216
+ folded,
217
+ nextByte: startByte,
218
+ eof: true,
219
+ aborted: false,
220
+ corruptLines
221
+ };
222
+ }
223
+ const chunk = Buffer.alloc(PR_SUBSCRIPTION_LIMITS.readChunkBytes);
224
+ let pending = [];
225
+ let pendingLen = 0;
226
+ let oversize = false;
227
+ let pos = startByte;
228
+ const flushPendingAtEof = () => {
229
+ if (oversize) {
230
+ corruptLines += 1;
231
+ } else if (pendingLen > 0) {
232
+ processLineBytes(Buffer.concat(pending));
233
+ }
234
+ consumedTo = Math.max(consumedTo, pos);
235
+ pending = [];
236
+ pendingLen = 0;
237
+ oversize = false;
238
+ return {
239
+ folded,
240
+ nextByte: consumedTo,
241
+ eof: true,
242
+ aborted: false,
243
+ corruptLines
244
+ };
245
+ };
246
+ while (true) {
247
+ const want = Math.min(chunk.length, size - pos);
248
+ if (want <= 0)
249
+ return flushPendingAtEof();
250
+ const n = fs.readSync(fd, chunk, 0, want, pos);
251
+ if (n <= 0)
252
+ return flushPendingAtEof();
253
+ const data = chunk.subarray(0, n);
254
+ const base = pos;
255
+ pos += n;
256
+ let scan = 0;
257
+ while (scan < data.length) {
258
+ const nl = data.indexOf(10, scan);
259
+ if (nl === -1) {
260
+ pending.push(Buffer.from(data.subarray(scan)));
261
+ pendingLen += data.length - scan;
262
+ break;
263
+ }
264
+ const lineLen = pendingLen + (nl - scan);
265
+ if (oversize || lineLen > PR_SUBSCRIPTION_LIMITS.maxRecordBytes) {
266
+ corruptLines += 1;
267
+ } else {
268
+ const pieces = pending.length > 0 ? [...pending, data.subarray(scan, nl)] : [data.subarray(scan, nl)];
269
+ processLineBytes(Buffer.concat(pieces));
270
+ }
271
+ pending = [];
272
+ pendingLen = 0;
273
+ oversize = false;
274
+ consumedTo = base + nl + 1;
275
+ scan = nl + 1;
276
+ }
277
+ if (!oversize && pendingLen > PR_SUBSCRIPTION_LIMITS.maxRecordBytes) {
278
+ oversize = true;
279
+ pending = [];
280
+ pendingLen = 0;
281
+ }
282
+ if (consumedTo - startByte >= maxBytes && pendingLen === 0 && !oversize) {
283
+ const eof = consumedTo >= size;
284
+ return {
285
+ folded,
286
+ nextByte: consumedTo,
287
+ eof,
288
+ aborted: false,
289
+ corruptLines
290
+ };
291
+ }
292
+ }
293
+ } catch (err) {
294
+ log(`[pr-monitor] foldLegacyRegion failed: ${err instanceof Error ? err.message : String(err)}`);
295
+ return {
296
+ folded,
297
+ nextByte: consumedTo,
298
+ eof: true,
299
+ aborted: true,
300
+ corruptLines
301
+ };
302
+ } finally {
303
+ try {
304
+ fs.closeSync(fd);
305
+ } catch {}
306
+ }
307
+ }
308
+ function mergeFoldedRecords(view, overlay) {
309
+ let replaced = 0;
310
+ for (const [key, rec] of overlay) {
311
+ const base = view[key];
312
+ if (!base || rec.updatedAt >= base.updatedAt) {
313
+ view[key] = rec;
314
+ replaced += 1;
315
+ }
316
+ }
317
+ return replaced;
318
+ }
319
+ function readCheckpoint(directory) {
320
+ let stat;
321
+ try {
322
+ stat = fs.statSync(checkpointPath(directory));
323
+ } catch (err) {
324
+ if (err.code === "ENOENT") {
325
+ return { kind: "absent" };
326
+ }
327
+ return {
328
+ kind: "invalid",
329
+ reason: `unreadable (${err.code ?? "unknown"})`
330
+ };
331
+ }
332
+ if (stat.size > PR_SUBSCRIPTION_LIMITS.checkpointHardReadBytes) {
333
+ return {
334
+ kind: "invalid",
335
+ reason: `checkpoint exceeds hard read ceiling (${stat.size} > ${PR_SUBSCRIPTION_LIMITS.checkpointHardReadBytes} bytes)`
336
+ };
337
+ }
338
+ let raw;
339
+ try {
340
+ raw = fs.readFileSync(checkpointPath(directory), "utf-8");
341
+ } catch (err) {
342
+ return {
343
+ kind: "invalid",
344
+ reason: `unreadable (${err.code ?? "unknown"})`
345
+ };
346
+ }
347
+ let parsed;
348
+ try {
349
+ parsed = JSON.parse(raw);
350
+ } catch (err) {
351
+ return {
352
+ kind: "invalid",
353
+ reason: `malformed JSON (${err instanceof Error ? err.message : String(err)})`
354
+ };
355
+ }
356
+ const result = CheckpointSchema.safeParse(parsed);
357
+ if (!result.success) {
358
+ return { kind: "invalid", reason: result.error.message };
359
+ }
360
+ const checkpoint = result.data;
361
+ const recordCount = Object.keys(checkpoint.records).length;
362
+ if (recordCount > PR_SUBSCRIPTION_LIMITS.maxCheckpointRecords) {
363
+ return {
364
+ kind: "invalid",
365
+ reason: `records count ${recordCount} exceeds guard ${PR_SUBSCRIPTION_LIMITS.maxCheckpointRecords}`
366
+ };
367
+ }
368
+ for (const [key, rec] of Object.entries(checkpoint.records)) {
369
+ if (key !== rec.correlationId || rec.correlationId !== buildCorrelationId(rec.sessionID, rec.repoFullName, rec.prNumber)) {
370
+ return {
371
+ kind: "invalid",
372
+ reason: `record identity mismatch at key "${key}"`
373
+ };
374
+ }
375
+ }
376
+ return { kind: "ok", value: checkpoint };
377
+ }
378
+ function freshCheckpoint(directory) {
379
+ return {
380
+ schemaVersion: 1,
381
+ sequence: 0,
382
+ rootPath: path.resolve(directory),
383
+ updatedAt: Date.now(),
384
+ records: {},
385
+ terminalSummary: { removed: 0, expired: 0, lastTerminalAt: null },
386
+ migration: null,
387
+ maintenance: {
388
+ compactions: 0,
389
+ droppedAuditTransitions: 0,
390
+ corruptLegacyRecords: 0,
391
+ lastCompactedAt: null,
392
+ resets: 0
393
+ }
394
+ };
395
+ }
396
+ function cloneCheckpoint(cp) {
397
+ return JSON.parse(JSON.stringify(cp));
398
+ }
399
+ function validateRecord(record) {
400
+ const result = RecordSchema.safeParse(record);
401
+ if (!result.success) {
402
+ throw new Error(`Invalid subscription record: ${result.error.message}`);
403
+ }
404
+ }
405
+ function writeCheckpointFile(directory, checkpoint) {
406
+ const recordCount = Object.keys(checkpoint.records).length;
407
+ if (recordCount > PR_SUBSCRIPTION_LIMITS.maxCheckpointRecords) {
408
+ throw new Error(`PR subscription store over checkpoint capacity: ${recordCount} records > ${PR_SUBSCRIPTION_LIMITS.maxCheckpointRecords}. The folded state exceeds the bounded checkpoint \u2014 archive or split .swarm/pr-monitor/subscriptions.jsonl (subscriptions can be re-created with /swarm pr subscribe), then remove it.`);
409
+ }
410
+ const content = `${JSON.stringify(checkpoint)}
411
+ `;
412
+ const contentBytes = Buffer.byteLength(content, "utf-8");
413
+ if (contentBytes > PR_SUBSCRIPTION_LIMITS.checkpointHardReadBytes) {
414
+ throw new Error(`PR subscription store over checkpoint capacity: ${contentBytes} bytes > ${PR_SUBSCRIPTION_LIMITS.checkpointHardReadBytes}. The folded state exceeds the bounded checkpoint \u2014 archive or split .swarm/pr-monitor/subscriptions.jsonl (subscriptions can be re-created with /swarm pr subscribe), then remove it.`);
415
+ }
416
+ if (contentBytes > PR_SUBSCRIPTION_LIMITS.maxCheckpointBytes) {
417
+ log(`[pr-monitor] checkpoint pressure: ${contentBytes}/${PR_SUBSCRIPTION_LIMITS.maxCheckpointBytes} bytes`);
418
+ }
419
+ ensureSwarmDir(directory);
420
+ atomicWriteSwarmFileSync(checkpointPath(directory), content);
421
+ }
422
+ function quarantineCheckpoint(directory, slot) {
423
+ const from = checkpointPath(directory);
424
+ const to = slot === "foreign" ? foreignSlotPath(directory) : corruptSlotPath(directory);
425
+ try {
426
+ try {
427
+ fs.renameSync(to, `${to}.prev`);
428
+ } catch {}
429
+ renameWithRetry(from, to);
430
+ return true;
431
+ } catch (err) {
432
+ log(`[pr-monitor] checkpoint quarantine (${slot}) failed: ${err instanceof Error ? err.message : String(err)} \u2014 the next checkpoint write replaces it`);
433
+ return false;
434
+ }
435
+ }
436
+ function flushAuditEvents(directory, events, sequence, maintenance) {
437
+ if (events.length === 0)
438
+ return;
439
+ ensureSwarmDir(directory);
440
+ const lines = events.map((event) => JSON.stringify({
441
+ ts: Date.now(),
442
+ seq: sequence,
443
+ kind: event.kind,
444
+ ...event.correlationId ? { correlationId: event.correlationId } : {}
445
+ }));
446
+ try {
447
+ fs.appendFileSync(auditPath(directory), `${lines.join(`
448
+ `)}
449
+ `, "utf-8");
450
+ } catch (err) {
451
+ log(`[pr-monitor] audit append failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
452
+ return;
453
+ }
454
+ compactAuditTail(directory, maintenance);
455
+ }
456
+ function compactAuditTail(directory, maintenance) {
457
+ let size;
458
+ try {
459
+ size = fs.statSync(auditPath(directory)).size;
460
+ } catch {
461
+ return;
462
+ }
463
+ if (size <= PR_SUBSCRIPTION_LIMITS.auditMaxBytesLow)
464
+ return;
465
+ const windowBytes = 2 * PR_SUBSCRIPTION_LIMITS.auditMaxBytesHigh;
466
+ let content;
467
+ try {
468
+ if (size > windowBytes) {
469
+ const fd = fs.openSync(auditPath(directory), "r");
470
+ try {
471
+ const tail = Buffer.alloc(windowBytes);
472
+ const n = fs.readSync(fd, tail, 0, windowBytes, size - windowBytes);
473
+ const text = tail.subarray(0, n).toString("utf-8");
474
+ const firstNl = text.indexOf(`
475
+ `);
476
+ content = firstNl === -1 ? "" : text.slice(firstNl + 1);
477
+ } finally {
478
+ fs.closeSync(fd);
479
+ }
480
+ } else {
481
+ content = fs.readFileSync(auditPath(directory), "utf-8");
482
+ }
483
+ } catch {
484
+ return;
485
+ }
486
+ const all = content.split(`
487
+ `).filter((line) => line.trim().length > 0);
488
+ if (all.length <= PR_SUBSCRIPTION_LIMITS.auditMaxLinesHigh && size <= PR_SUBSCRIPTION_LIMITS.auditMaxBytesHigh) {
489
+ return;
490
+ }
491
+ const kept = [];
492
+ let bytes = 0;
493
+ for (let i = all.length - 1;i >= 0; i--) {
494
+ const lineBytes = Buffer.byteLength(all[i], "utf-8") + 1;
495
+ if (kept.length >= PR_SUBSCRIPTION_LIMITS.auditMaxLinesLow || bytes + lineBytes > PR_SUBSCRIPTION_LIMITS.auditMaxBytesLow) {
496
+ break;
497
+ }
498
+ kept.unshift(all[i]);
499
+ bytes += lineBytes;
500
+ }
501
+ const dropped = all.length - kept.length;
502
+ try {
503
+ atomicWriteSwarmFileSync(auditPath(directory), kept.length > 0 ? `${kept.join(`
504
+ `)}
505
+ ` : "");
506
+ maintenance.droppedAuditTransitions += dropped;
507
+ } catch (err) {
508
+ log(`[pr-monitor] audit compaction failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
509
+ }
510
+ }
511
+ function auditStats(directory) {
512
+ try {
513
+ const stat = fs.statSync(auditPath(directory));
514
+ let content;
515
+ if (stat.size > 2 * PR_SUBSCRIPTION_LIMITS.auditMaxBytesHigh) {
516
+ const fd = fs.openSync(auditPath(directory), "r");
517
+ try {
518
+ const head = Buffer.alloc(2 * PR_SUBSCRIPTION_LIMITS.auditMaxBytesHigh);
519
+ const n = fs.readSync(fd, head, 0, head.length, 0);
520
+ content = head.subarray(0, n).toString("utf-8");
521
+ } finally {
522
+ fs.closeSync(fd);
523
+ }
524
+ } else {
525
+ content = fs.readFileSync(auditPath(directory), "utf-8");
526
+ }
527
+ return {
528
+ lines: content.split(`
529
+ `).filter((line) => line.trim().length > 0).length,
530
+ bytes: stat.size
531
+ };
532
+ } catch {
533
+ return { lines: 0, bytes: 0 };
534
+ }
535
+ }
536
+ function compactTerminalRecords(view, checkpoint) {
537
+ const now = Date.now();
538
+ const terminals = Object.entries(view).filter(([, rec]) => rec.status !== "active").sort((a, b) => b[1].updatedAt - a[1].updatedAt);
539
+ if (terminals.length === 0)
540
+ return null;
541
+ const hasAged = terminals.some(([, rec]) => now - rec.updatedAt > PR_SUBSCRIPTION_LIMITS.terminalMaxAgeMs);
542
+ if (!hasAged && terminals.length <= PR_SUBSCRIPTION_LIMITS.terminalRecordsHigh) {
543
+ return null;
544
+ }
545
+ let kept = 0;
546
+ for (const [key, rec] of terminals) {
547
+ const aged = now - rec.updatedAt > PR_SUBSCRIPTION_LIMITS.terminalMaxAgeMs;
548
+ if (aged || kept >= PR_SUBSCRIPTION_LIMITS.terminalRecordsLow) {
549
+ if (rec.status === "removed")
550
+ checkpoint.terminalSummary.removed += 1;
551
+ else
552
+ checkpoint.terminalSummary.expired += 1;
553
+ checkpoint.terminalSummary.lastTerminalAt = Math.max(checkpoint.terminalSummary.lastTerminalAt ?? 0, rec.updatedAt);
554
+ delete view[key];
555
+ } else {
556
+ kept += 1;
557
+ }
558
+ }
559
+ checkpoint.maintenance.compactions += 1;
560
+ checkpoint.maintenance.lastCompactedAt = now;
561
+ return { kind: "compact" };
562
+ }
563
+ function maybeArchiveLegacy(directory, checkpoint) {
564
+ const migration = checkpoint.migration;
565
+ if (!migration || !migration.done || migration.archived)
566
+ return null;
567
+ const legacy = storePath(directory);
568
+ const size = fileSizeOrNull(legacy);
569
+ if (size === null) {
570
+ migration.archived = true;
571
+ return null;
572
+ }
573
+ if (size !== migration.sourceBytes)
574
+ return null;
575
+ try {
576
+ try {
577
+ fs.unlinkSync(legacyArchivePath(directory));
578
+ } catch {}
579
+ renameWithRetry(legacy, legacyArchivePath(directory));
580
+ } catch (err) {
581
+ log(`[pr-monitor] legacy archive deferred: ${err instanceof Error ? err.message : String(err)}`);
582
+ return null;
583
+ }
584
+ try {
585
+ const now = Date.now() / 1000;
586
+ fs.utimesSync(legacyArchivePath(directory), now, now);
587
+ } catch {}
588
+ migration.archived = true;
589
+ return { kind: "archive" };
590
+ }
591
+ function cleanupStaleLegacyArchive(directory) {
592
+ try {
593
+ const stat = fs.statSync(legacyArchivePath(directory));
594
+ if (Date.now() - stat.mtimeMs > PR_SUBSCRIPTION_LIMITS.legacyArchiveTtlMs) {
595
+ fs.unlinkSync(legacyArchivePath(directory));
596
+ }
597
+ } catch {}
598
+ }
599
+ function emitHealthTelemetry(trigger, checkpoint) {
600
+ const records = Object.values(checkpoint.records);
601
+ telemetry.prSubscriptionHealth({
602
+ trigger,
603
+ active_count: records.filter((r) => r.status === "active").length,
604
+ terminal_count: records.filter((r) => r.status !== "active").length,
605
+ compactions: checkpoint.maintenance.compactions,
606
+ corrupt_count: checkpoint.maintenance.corruptLegacyRecords,
607
+ dropped_audit_count: checkpoint.maintenance.droppedAuditTransitions,
608
+ checkpoint_bytes: Buffer.byteLength(JSON.stringify(checkpoint), "utf-8"),
609
+ limit_bytes: PR_SUBSCRIPTION_LIMITS.maxCheckpointBytes,
610
+ recovery_resets: checkpoint.maintenance.resets
611
+ });
612
+ }
613
+ var legacyOverLimitWarned = new Map;
614
+ function warnLegacyOverLimit(directory, size) {
615
+ if (legacyOverLimitWarned.has(directory))
616
+ return;
617
+ if (legacyOverLimitWarned.size >= 32) {
618
+ legacyOverLimitWarned.delete(legacyOverLimitWarned.keys().next().value);
619
+ }
620
+ legacyOverLimitWarned.set(directory, true);
621
+ log(`[pr-monitor] legacy subscriptions.jsonl is ${size} bytes (>${PR_SUBSCRIPTION_LIMITS.legacySourceMaxBytes}); refusing to fold it \u2014 archive or split the file so its subscriptions can be re-subscribed, then remove it (reported via /swarm pr status)`);
622
+ }
623
+ var migrationCapacityWarned = new Map;
624
+ function warnMigrationCapacity(directory, records) {
625
+ if (migrationCapacityWarned.has(directory))
626
+ return;
627
+ if (migrationCapacityWarned.size >= 32) {
628
+ migrationCapacityWarned.delete(migrationCapacityWarned.keys().next().value);
629
+ }
630
+ migrationCapacityWarned.set(directory, true);
631
+ log(`[pr-monitor] legacy subscriptions.jsonl folds to ${records} records (>checkpoint capacity ${PR_SUBSCRIPTION_LIMITS.maxCheckpointRecords}); migration refused \u2014 archive or split the file so its subscriptions can be re-subscribed, then remove it`);
632
+ }
633
+ function overlayLegacy(directory, checkpoint) {
634
+ const view = { ...checkpoint.records };
635
+ const legacy = storePath(directory);
636
+ let stat = null;
637
+ try {
638
+ stat = fs.statSync(legacy);
639
+ } catch {}
640
+ if (stat === null) {
641
+ return {
642
+ view,
643
+ usedLegacy: false,
644
+ absorbed: 0,
645
+ corruptLines: 0,
646
+ overLimit: false,
647
+ aborted: false
648
+ };
649
+ }
650
+ if (stat.size > PR_SUBSCRIPTION_LIMITS.legacySourceMaxBytes) {
651
+ warnLegacyOverLimit(directory, stat.size);
652
+ return {
653
+ view,
654
+ usedLegacy: false,
655
+ absorbed: 0,
656
+ corruptLines: 0,
657
+ overLimit: true,
658
+ aborted: false
659
+ };
660
+ }
661
+ const migration = checkpoint.migration;
662
+ if (migration === null || !migration.done) {
663
+ let cursor = migration?.scannedBytes ?? 0;
664
+ if (cursor > stat.size)
665
+ cursor = 0;
666
+ const fold2 = foldLegacyRegion(legacy, cursor, Number.MAX_SAFE_INTEGER);
667
+ const absorbed2 = mergeFoldedRecords(view, fold2.folded);
668
+ return {
669
+ view,
670
+ usedLegacy: true,
671
+ absorbed: absorbed2,
672
+ corruptLines: fold2.corruptLines,
673
+ overLimit: false,
674
+ aborted: fold2.aborted
675
+ };
676
+ }
677
+ if (stat.size === migration.sourceBytes && stat.mtimeMs === migration.sourceMtimeMs) {
678
+ return {
679
+ view,
680
+ usedLegacy: false,
681
+ absorbed: 0,
682
+ corruptLines: 0,
683
+ overLimit: false,
684
+ aborted: false
685
+ };
686
+ }
687
+ const fold = foldLegacyRegion(legacy, 0, Number.MAX_SAFE_INTEGER);
688
+ const absorbed = mergeFoldedRecords(view, fold.folded);
689
+ return {
690
+ view,
691
+ usedLegacy: true,
692
+ absorbed,
693
+ corruptLines: fold.corruptLines,
694
+ overLimit: false,
695
+ aborted: fold.aborted
696
+ };
697
+ }
698
+ async function loadViewForRead(directory) {
699
+ const read = readCheckpoint(directory);
700
+ if (read.kind === "ok") {
701
+ if (!sameProjectRoot(read.value.rootPath, directory)) {
702
+ return { view: {}, recoverySource: "foreign" };
703
+ }
704
+ const overlaid = overlayLegacy(directory, read.value);
705
+ return {
706
+ view: overlaid.view,
707
+ recoverySource: overlaid.usedLegacy ? "checkpoint+legacy" : "checkpoint"
708
+ };
709
+ }
710
+ if (read.kind === "invalid") {
711
+ const legacy = storePath(directory);
712
+ const view2 = {};
713
+ const size = fileSizeOrNull(legacy);
714
+ if (size !== null && size <= PR_SUBSCRIPTION_LIMITS.legacySourceMaxBytes) {
715
+ const fold2 = foldLegacyRegion(legacy, 0, Number.MAX_SAFE_INTEGER);
716
+ mergeFoldedRecords(view2, fold2.folded);
717
+ } else if (size !== null) {
718
+ warnLegacyOverLimit(directory, size);
719
+ }
720
+ return { view: view2, recoverySource: "corrupt-recovered" };
721
+ }
722
+ const legacyPath = storePath(directory);
723
+ const legacySize = fileSizeOrNull(legacyPath);
724
+ if (legacySize === null) {
725
+ return { view: {}, recoverySource: "empty" };
726
+ }
727
+ if (legacySize > PR_SUBSCRIPTION_LIMITS.legacySourceMaxBytes) {
728
+ warnLegacyOverLimit(directory, legacySize);
729
+ return { view: {}, recoverySource: "legacy-log" };
730
+ }
731
+ const fold = foldLegacyRegion(legacyPath, 0, Number.MAX_SAFE_INTEGER);
732
+ const view = {};
733
+ mergeFoldedRecords(view, fold.folded);
734
+ if (!fold.aborted) {
735
+ await bootstrapCheckpointFromLegacy(directory, view, fold);
736
+ }
737
+ return { view, recoverySource: "legacy-log" };
738
+ }
739
+ var bootstrapAttempted = new Map;
740
+ async function bootstrapCheckpointFromLegacy(directory, foldedView, fold) {
741
+ if (bootstrapAttempted.has(directory))
742
+ return;
743
+ if (bootstrapAttempted.size >= 32) {
744
+ bootstrapAttempted.delete(bootstrapAttempted.keys().next().value);
745
+ }
746
+ bootstrapAttempted.set(directory, true);
747
+ try {
748
+ await withEvidenceLock(directory, PR_SUBSCRIPTIONS_FILE, STORE_LOCK_AGENT, STORE_LOCK_TASK, async () => {
749
+ if (readCheckpoint(directory).kind !== "absent")
750
+ return;
751
+ const checkpoint = freshCheckpoint(directory);
752
+ checkpoint.records = { ...foldedView };
753
+ const terminals = Object.values(foldedView).filter((rec) => rec.status !== "active");
754
+ checkpoint.terminalSummary.lastTerminalAt = terminals.length ? Math.max(...terminals.map((rec) => rec.updatedAt)) : null;
755
+ checkpoint.maintenance.corruptLegacyRecords = fold.corruptLines;
756
+ let bootstrapMtime = 0;
757
+ try {
758
+ bootstrapMtime = fs.statSync(storePath(directory)).mtimeMs;
759
+ } catch {}
760
+ checkpoint.migration = {
761
+ scannedBytes: fold.nextByte,
762
+ sourceBytes: fold.nextByte,
763
+ sourceMtimeMs: bootstrapMtime,
764
+ corruptLines: fold.corruptLines,
765
+ done: true,
766
+ archived: false,
767
+ startedAt: Date.now()
768
+ };
769
+ checkpoint.sequence = 1;
770
+ checkpoint.updatedAt = Date.now();
771
+ writeCheckpointFile(directory, checkpoint);
772
+ flushAuditEvents(directory, [{ kind: "migrate-complete" }], checkpoint.sequence, checkpoint.maintenance);
773
+ emitHealthTelemetry("migrate-complete", checkpoint);
774
+ }, PR_SUBSCRIPTION_LIMITS.bootstrapLockTimeoutMs);
775
+ } catch (err) {
776
+ log(`[pr-monitor] read-bootstrap skipped: ${err instanceof Error ? err.message : String(err)}`);
777
+ }
778
+ }
779
+ function loadViewForWrite(directory) {
780
+ const audit = [];
781
+ let dirty = false;
782
+ let checkpoint;
783
+ const read = readCheckpoint(directory);
784
+ if (read.kind === "ok" && sameProjectRoot(read.value.rootPath, directory)) {
785
+ checkpoint = cloneCheckpoint(read.value);
786
+ } else if (read.kind === "ok") {
787
+ const displaced = Object.keys(read.value.records).length;
788
+ quarantineCheckpoint(directory, "foreign");
789
+ checkpoint = freshCheckpoint(directory);
790
+ checkpoint.maintenance.resets += read.value.maintenance.resets + 1;
791
+ audit.push({ kind: "foreign-rebind" });
792
+ dirty = true;
793
+ log(`[pr-monitor] foreign checkpoint quarantined (${displaced} displaced records, recorded root ${read.value.rootPath}); store rebound to ${directory}`);
794
+ } else if (read.kind === "invalid") {
795
+ quarantineCheckpoint(directory, "corrupt");
796
+ checkpoint = freshCheckpoint(directory);
797
+ checkpoint.maintenance.resets += 1;
798
+ audit.push({ kind: "corrupt-quarantine" });
799
+ dirty = true;
800
+ log(`[pr-monitor] corrupt checkpoint quarantined; store rebound to ${directory}`);
801
+ } else {
802
+ checkpoint = freshCheckpoint(directory);
803
+ }
804
+ const legacy = storePath(directory);
805
+ let legacyStat = null;
806
+ try {
807
+ legacyStat = fs.statSync(legacy);
808
+ } catch {}
809
+ let view;
810
+ if (legacyStat === null) {
811
+ view = { ...checkpoint.records };
812
+ if (checkpoint.migration?.done && !checkpoint.migration.archived) {
813
+ checkpoint.migration.archived = true;
814
+ dirty = true;
815
+ }
816
+ return { checkpoint, view, audit, dirty };
817
+ }
818
+ if (legacyStat.size > PR_SUBSCRIPTION_LIMITS.legacySourceMaxBytes) {
819
+ warnLegacyOverLimit(directory, legacyStat.size);
820
+ return { checkpoint, view: { ...checkpoint.records }, audit, dirty };
821
+ }
822
+ const legacySize = legacyStat.size;
823
+ const legacyMtime = legacyStat.mtimeMs;
824
+ const migration = checkpoint.migration;
825
+ if (migration === null || !migration.done) {
826
+ let cursor = migration?.scannedBytes ?? 0;
827
+ if (cursor > legacySize)
828
+ cursor = 0;
829
+ const startedAt = migration?.startedAt ?? Date.now();
830
+ let corruptTotal = migration?.corruptLines ?? 0;
831
+ view = { ...checkpoint.records };
832
+ let capacityRefused = false;
833
+ while (true) {
834
+ const fold = foldLegacyRegion(legacy, cursor, PR_SUBSCRIPTION_LIMITS.migrationChunkBytes);
835
+ mergeFoldedRecords(view, fold.folded);
836
+ checkpoint.maintenance.corruptLegacyRecords += fold.corruptLines;
837
+ corruptTotal += fold.corruptLines;
838
+ cursor = fold.nextByte;
839
+ if (fold.aborted) {
840
+ const progress = cloneCheckpoint(checkpoint);
841
+ progress.records = { ...view };
842
+ progress.migration = {
843
+ scannedBytes: cursor,
844
+ sourceBytes: legacySize,
845
+ sourceMtimeMs: legacyMtime,
846
+ corruptLines: corruptTotal,
847
+ done: false,
848
+ archived: false,
849
+ startedAt
850
+ };
851
+ progress.updatedAt = Date.now();
852
+ progress.sequence += 1;
853
+ writeCheckpointFile(directory, progress);
854
+ checkpoint.sequence = progress.sequence;
855
+ log(`[pr-monitor] legacy scan aborted by an I/O error after ${cursor} bytes \u2014 migration stays incomplete and will retry`);
856
+ dirty = true;
857
+ return { checkpoint, view, audit, dirty };
858
+ }
859
+ const compactEvent = compactTerminalRecords(view, checkpoint);
860
+ if (compactEvent)
861
+ audit.push(compactEvent);
862
+ if (Object.keys(view).length > PR_SUBSCRIPTION_LIMITS.maxCheckpointRecords) {
863
+ capacityRefused = true;
864
+ warnMigrationCapacity(directory, Object.keys(view).length);
865
+ break;
866
+ }
867
+ if (!fold.eof) {
868
+ const progress = cloneCheckpoint(checkpoint);
869
+ progress.records = { ...view };
870
+ progress.migration = {
871
+ scannedBytes: cursor,
872
+ sourceBytes: legacySize,
873
+ sourceMtimeMs: legacyMtime,
874
+ corruptLines: corruptTotal,
875
+ done: false,
876
+ archived: false,
877
+ startedAt
878
+ };
879
+ progress.updatedAt = Date.now();
880
+ progress.sequence += 1;
881
+ writeCheckpointFile(directory, progress);
882
+ checkpoint.sequence = progress.sequence;
883
+ dirty = true;
884
+ continue;
885
+ }
886
+ checkpoint.migration = {
887
+ scannedBytes: cursor,
888
+ sourceBytes: legacySize,
889
+ sourceMtimeMs: legacyMtime,
890
+ corruptLines: corruptTotal,
891
+ done: true,
892
+ archived: false,
893
+ startedAt
894
+ };
895
+ audit.push({ kind: "migrate-complete" });
896
+ dirty = true;
897
+ break;
898
+ }
899
+ if (capacityRefused) {
900
+ return { checkpoint, view, audit, dirty: false };
901
+ }
902
+ return { checkpoint, view, audit, dirty };
903
+ }
904
+ const overlaid = overlayLegacy(directory, checkpoint);
905
+ view = overlaid.view;
906
+ if (overlaid.usedLegacy && !overlaid.aborted) {
907
+ migration.sourceBytes = legacySize;
908
+ migration.sourceMtimeMs = legacyMtime;
909
+ migration.scannedBytes = legacySize;
910
+ migration.archived = false;
911
+ checkpoint.maintenance.corruptLegacyRecords += overlaid.corruptLines;
912
+ dirty = true;
913
+ }
914
+ return { checkpoint, view, audit, dirty };
915
+ }
916
+ function finalizeWrite(directory, loaded, opAudit) {
917
+ const { checkpoint } = loaded;
918
+ const events = [...loaded.audit, ...opAudit];
919
+ const compactEvent = compactTerminalRecords(loaded.view, checkpoint);
920
+ if (compactEvent)
921
+ events.push(compactEvent);
922
+ if (!loaded.dirty && events.length === 0)
923
+ return;
924
+ checkpoint.records = loaded.view;
925
+ checkpoint.updatedAt = Date.now();
926
+ checkpoint.rootPath = path.resolve(directory);
927
+ checkpoint.sequence += 1;
928
+ flushAuditEvents(directory, events, checkpoint.sequence, checkpoint.maintenance);
929
+ writeCheckpointFile(directory, checkpoint);
930
+ const archiveEvent = maybeArchiveLegacy(directory, checkpoint);
931
+ if (archiveEvent) {
932
+ events.push(archiveEvent);
933
+ checkpoint.sequence += 1;
934
+ flushAuditEvents(directory, [archiveEvent], checkpoint.sequence, checkpoint.maintenance);
935
+ writeCheckpointFile(directory, checkpoint);
936
+ }
937
+ cleanupStaleLegacyArchive(directory);
938
+ for (const kind of [
939
+ "compact",
940
+ "migrate-complete",
941
+ "archive",
942
+ "foreign-rebind",
943
+ "corrupt-quarantine"
944
+ ]) {
945
+ if (events.some((event) => event.kind === kind)) {
946
+ emitHealthTelemetry(kind, checkpoint);
947
+ }
948
+ }
949
+ }
950
+ async function subscribe(directory, input) {
951
+ if (!directory || directory.trim() === "") {
952
+ throw new Error("directory is required");
953
+ }
954
+ if (!input.sessionID || input.sessionID.trim() === "") {
955
+ throw new Error("sessionID is required and must be non-empty");
956
+ }
957
+ if (!input.repoFullName || input.repoFullName.trim() === "") {
958
+ throw new Error("repoFullName is required and must be non-empty");
959
+ }
960
+ if (!input.prUrl || input.prUrl.trim() === "") {
961
+ throw new Error("prUrl is required and must be non-empty");
962
+ }
963
+ if (!input.prNumber || !Number.isInteger(input.prNumber) || input.prNumber <= 0) {
964
+ throw new Error("prNumber is required and must be a positive integer");
965
+ }
966
+ const correlationId = buildCorrelationId(input.sessionID, input.repoFullName, input.prNumber);
967
+ const now = Date.now();
968
+ return withEvidenceLock(directory, PR_SUBSCRIPTIONS_FILE, STORE_LOCK_AGENT, STORE_LOCK_TASK, async () => {
969
+ const loaded = loadViewForWrite(directory);
970
+ const match = Object.values(loaded.view).find((r) => r.correlationId === correlationId && r.status === "active");
971
+ if (match) {
972
+ onSubscriptionCreated?.(directory, match);
973
+ finalizeWrite(directory, loaded, []);
974
+ return match;
975
+ }
976
+ if (input.maxSubscriptions === undefined || input.maxSubscriptions > 0) {
977
+ const limit = input.maxSubscriptions !== undefined ? input.maxSubscriptions : PR_SUBSCRIPTION_LIMITS.defaultMaxActiveSubscriptions;
978
+ const activeCount = Object.values(loaded.view).filter((r) => r.status === "active").length;
979
+ if (activeCount >= limit) {
980
+ throw new Error(`PR subscription limit reached: ${activeCount}/${limit}`);
981
+ }
982
+ }
983
+ const record = {
984
+ correlationId,
985
+ sessionID: input.sessionID,
986
+ prNumber: input.prNumber,
987
+ repoFullName: input.repoFullName,
988
+ prUrl: input.prUrl,
989
+ lastCheckedAt: now,
990
+ isWatching: true,
991
+ hasUnaddressedEvents: false,
992
+ status: "active",
993
+ createdAt: now,
994
+ updatedAt: now,
995
+ errorCount: 0
996
+ };
997
+ validateRecord(record);
998
+ loaded.view[correlationId] = record;
999
+ loaded.dirty = true;
1000
+ finalizeWrite(directory, loaded, [{ kind: "subscribe", correlationId }]);
1001
+ onSubscriptionCreated?.(directory, record);
1002
+ return record;
1003
+ });
1004
+ }
1005
+ async function unsubscribe(directory, correlationId) {
1006
+ if (!correlationId)
1007
+ return null;
1008
+ return withEvidenceLock(directory, PR_SUBSCRIPTIONS_FILE, STORE_LOCK_AGENT, STORE_LOCK_TASK, async () => {
1009
+ const loaded = loadViewForWrite(directory);
1010
+ const match = Object.values(loaded.view).find((r) => r.correlationId === correlationId && r.status === "active");
1011
+ if (!match) {
1012
+ finalizeWrite(directory, loaded, []);
1013
+ return null;
1014
+ }
1015
+ const now = Date.now();
1016
+ const removed = {
1017
+ ...match,
1018
+ status: "removed",
1019
+ isWatching: false,
1020
+ updatedAt: now
1021
+ };
1022
+ loaded.view[correlationId] = removed;
1023
+ loaded.dirty = true;
1024
+ finalizeWrite(directory, loaded, [
1025
+ { kind: "unsubscribe", correlationId }
1026
+ ]);
1027
+ return removed;
1028
+ });
1029
+ }
1030
+ async function listActive(directory) {
1031
+ const { view } = await loadViewForRead(directory);
1032
+ return Object.values(view).filter((r) => r.status === "active");
1033
+ }
1034
+ async function lookupByPr(directory, repoFullName, prNumber) {
1035
+ const { view } = await loadViewForRead(directory);
1036
+ for (const record of Object.values(view)) {
1037
+ if (record.status === "active" && record.repoFullName === repoFullName && record.prNumber === prNumber) {
1038
+ return record;
1039
+ }
1040
+ }
1041
+ return null;
1042
+ }
1043
+ async function updateSnapshot(directory, correlationId, updates) {
1044
+ if (!correlationId)
1045
+ return null;
1046
+ return withEvidenceLock(directory, PR_SUBSCRIPTIONS_FILE, STORE_LOCK_AGENT, STORE_LOCK_TASK, async () => {
1047
+ const loaded = loadViewForWrite(directory);
1048
+ const match = Object.values(loaded.view).find((r) => r.correlationId === correlationId && r.status === "active");
1049
+ if (!match) {
1050
+ finalizeWrite(directory, loaded, []);
1051
+ return null;
1052
+ }
1053
+ const updated = {
1054
+ ...match,
1055
+ ...updates,
1056
+ correlationId,
1057
+ sessionID: match.sessionID,
1058
+ repoFullName: match.repoFullName,
1059
+ prNumber: match.prNumber,
1060
+ prUrl: match.prUrl,
1061
+ createdAt: match.createdAt,
1062
+ updatedAt: Date.now()
1063
+ };
1064
+ loaded.view[correlationId] = updated;
1065
+ loaded.dirty = true;
1066
+ finalizeWrite(directory, loaded, []);
1067
+ return updated;
1068
+ });
1069
+ }
1070
+ async function sweepStale(directory, ttlDays, mergedPrs) {
1071
+ if (!ttlDays || ttlDays <= 0)
1072
+ return 0;
1073
+ const ttlMs = ttlDays * 86400000;
1074
+ const now = Date.now();
1075
+ try {
1076
+ return await withEvidenceLock(directory, PR_SUBSCRIPTIONS_FILE, STORE_LOCK_AGENT, STORE_LOCK_TASK, async () => {
1077
+ const loaded = loadViewForWrite(directory);
1078
+ const opAudit = [];
1079
+ let swept = 0;
1080
+ for (const record of Object.values(loaded.view)) {
1081
+ if (record.status !== "active")
1082
+ continue;
1083
+ const prKey = `${record.repoFullName}::${record.prNumber}`;
1084
+ const isMerged = mergedPrs?.has(prKey) ?? false;
1085
+ const isStale = now - record.updatedAt > ttlMs;
1086
+ if (isMerged) {
1087
+ loaded.view[record.correlationId] = {
1088
+ ...record,
1089
+ status: "expired",
1090
+ isWatching: false,
1091
+ updatedAt: now
1092
+ };
1093
+ swept += 1;
1094
+ opAudit.push({
1095
+ kind: "expired",
1096
+ correlationId: record.correlationId
1097
+ });
1098
+ loaded.dirty = true;
1099
+ log(`[pr-monitor] Swept subscription: merged/closed: ${prKey}`);
1100
+ continue;
1101
+ }
1102
+ if (isStale && !record.hasUnaddressedEvents) {
1103
+ loaded.view[record.correlationId] = {
1104
+ ...record,
1105
+ status: "expired",
1106
+ isWatching: false,
1107
+ updatedAt: now
1108
+ };
1109
+ swept += 1;
1110
+ opAudit.push({
1111
+ kind: "expired",
1112
+ correlationId: record.correlationId
1113
+ });
1114
+ loaded.dirty = true;
1115
+ log(`[pr-monitor] Swept subscription: stale (TTL ${ttlDays}d): ${prKey}`);
1116
+ }
1117
+ }
1118
+ finalizeWrite(directory, loaded, opAudit);
1119
+ return swept;
1120
+ });
1121
+ } catch (err) {
1122
+ log(`[pr-monitor] sweepStale failed: ${err instanceof Error ? err.message : String(err)}`);
1123
+ return 0;
1124
+ }
1125
+ }
1126
+ async function getPrSubscriptionHealth(directory) {
1127
+ try {
1128
+ const read = readCheckpoint(directory);
1129
+ const audit = auditStats(directory);
1130
+ const archiveBytes = fileSizeOrNull(legacyArchivePath(directory)) ?? 0;
1131
+ const checkpointBytes = fileSizeOrNull(checkpointPath(directory)) ?? 0;
1132
+ const legacySize = fileSizeOrNull(storePath(directory));
1133
+ const legacyOverLimit = legacySize !== null && legacySize > PR_SUBSCRIPTION_LIMITS.legacySourceMaxBytes;
1134
+ let health;
1135
+ if (read.kind === "ok" && !sameProjectRoot(read.value.rootPath, directory)) {
1136
+ health = emptyHealth();
1137
+ health.recoverySource = "foreign";
1138
+ } else if (read.kind === "invalid") {
1139
+ if (legacySize !== null && !legacyOverLimit) {
1140
+ const fold = foldLegacyRegion(storePath(directory), 0, Number.MAX_SAFE_INTEGER);
1141
+ const view = {};
1142
+ mergeFoldedRecords(view, fold.folded);
1143
+ health = healthFromView(view, freshCheckpoint(directory));
1144
+ health.corruptLegacyRecords = fold.corruptLines;
1145
+ } else {
1146
+ health = emptyHealth();
1147
+ }
1148
+ health.recoverySource = "corrupt-recovered";
1149
+ } else if (read.kind === "absent") {
1150
+ if (legacySize === null || legacyOverLimit) {
1151
+ health = emptyHealth();
1152
+ } else {
1153
+ const fold = foldLegacyRegion(storePath(directory), 0, Number.MAX_SAFE_INTEGER);
1154
+ const view = {};
1155
+ mergeFoldedRecords(view, fold.folded);
1156
+ health = healthFromView(view, freshCheckpoint(directory));
1157
+ health.corruptLegacyRecords = fold.corruptLines;
1158
+ }
1159
+ health.recoverySource = legacySize === null ? "empty" : "legacy-log";
1160
+ } else {
1161
+ const overlaid = overlayLegacy(directory, read.value);
1162
+ health = healthFromView(overlaid.view, read.value);
1163
+ health.recoverySource = overlaid.usedLegacy ? "checkpoint+legacy" : "checkpoint";
1164
+ }
1165
+ health.legacyOverLimit = legacyOverLimit;
1166
+ applyFileStats(health, checkpointBytes, audit, archiveBytes);
1167
+ return health;
1168
+ } catch (err) {
1169
+ log(`[pr-monitor] getPrSubscriptionHealth failed: ${err instanceof Error ? err.message : String(err)}`);
1170
+ return emptyHealth();
1171
+ }
1172
+ }
1173
+ function applyFileStats(health, checkpointBytes, audit, archiveBytes) {
1174
+ health.checkpointBytes = checkpointBytes;
1175
+ health.pressurePct = checkpointBytes === 0 ? 0 : Math.round(checkpointBytes / health.checkpointLimitBytes * 1000) / 10;
1176
+ health.auditLines = audit.lines;
1177
+ health.auditBytes = audit.bytes;
1178
+ health.legacyArchiveBytes = archiveBytes;
1179
+ }
1180
+ function emptyHealth() {
1181
+ return {
1182
+ schemaVersion: 1,
1183
+ sequence: 0,
1184
+ checkpointAgeMs: null,
1185
+ checkpointBytes: 0,
1186
+ checkpointLimitBytes: PR_SUBSCRIPTION_LIMITS.maxCheckpointBytes,
1187
+ pressurePct: 0,
1188
+ activeCount: 0,
1189
+ removedCount: 0,
1190
+ expiredCount: 0,
1191
+ terminalSummary: { removed: 0, expired: 0, lastTerminalAt: null },
1192
+ auditLines: 0,
1193
+ auditBytes: 0,
1194
+ auditLimitBytes: PR_SUBSCRIPTION_LIMITS.auditMaxBytesHigh,
1195
+ compactions: 0,
1196
+ corruptLegacyRecords: 0,
1197
+ droppedAuditTransitions: 0,
1198
+ resets: 0,
1199
+ recoverySource: "empty",
1200
+ migration: null,
1201
+ legacyArchiveBytes: 0,
1202
+ legacyOverLimit: false
1203
+ };
1204
+ }
1205
+ function healthFromView(view, checkpoint) {
1206
+ const records = Object.values(view);
1207
+ const health = emptyHealth();
1208
+ health.sequence = checkpoint.sequence;
1209
+ health.checkpointAgeMs = checkpoint.sequence > 0 ? Date.now() - checkpoint.updatedAt : null;
1210
+ health.activeCount = records.filter((r) => r.status === "active").length;
1211
+ health.removedCount = records.filter((r) => r.status === "removed").length;
1212
+ health.expiredCount = records.filter((r) => r.status === "expired").length;
1213
+ health.terminalSummary = { ...checkpoint.terminalSummary };
1214
+ health.compactions = checkpoint.maintenance.compactions;
1215
+ health.corruptLegacyRecords = checkpoint.maintenance.corruptLegacyRecords;
1216
+ health.droppedAuditTransitions = checkpoint.maintenance.droppedAuditTransitions;
1217
+ health.resets = checkpoint.maintenance.resets;
1218
+ health.migration = checkpoint.migration ? {
1219
+ done: checkpoint.migration.done,
1220
+ scannedBytes: checkpoint.migration.scannedBytes,
1221
+ sourceBytes: checkpoint.migration.sourceBytes
1222
+ } : null;
1223
+ return health;
1224
+ }
1225
+ var _internals = {
1226
+ foldLegacyRegion,
1227
+ mergeFoldedRecords,
1228
+ sameProjectRoot
1229
+ };
1230
+
1231
+ export { PR_SUBSCRIPTIONS_FILE, PR_SUBSCRIPTIONS_CHECKPOINT_FILE, PR_SUBSCRIPTIONS_AUDIT_FILE, PR_SUBSCRIPTION_LIMITS, setOnSubscriptionCreated, buildCorrelationId, subscribe, unsubscribe, listActive, lookupByPr, updateSnapshot, sweepStale, getPrSubscriptionHealth, _internals };