opencode-swarm 7.153.1 → 7.154.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/background/pending-delegations.d.ts +29 -0
  2. package/dist/background/pr-subscriptions.d.ts +258 -27
  3. package/dist/cli/{config-doctor-ze21qs6b.js → config-doctor-8p9y543t.js} +2 -2
  4. package/dist/cli/{core-qdmhh1kx.js → core-1h6x6k59.js} +1 -1
  5. package/dist/cli/{curation-policy-twa6agf8.js → curation-policy-9r902jw4.js} +6 -6
  6. package/dist/cli/{curator-llm-factory-s8atmc2n.js → curator-llm-factory-ypwf3a90.js} +29 -29
  7. package/dist/cli/{curator-hxb3wsxr.js → curator-yj00z1qz.js} +29 -29
  8. package/dist/cli/{evidence-summary-service-wzzym9r7.js → evidence-summary-service-npq04240.js} +12 -12
  9. package/dist/cli/{gate-evidence-f71dn9vv.js → gate-evidence-4b6t0ycn.js} +6 -6
  10. package/dist/cli/{guardrail-explain-5kzeem4m.js → guardrail-explain-dyxbhrdj.js} +30 -30
  11. package/dist/cli/{guardrail-log-38jnajpa.js → guardrail-log-eyecsekk.js} +5 -5
  12. package/dist/cli/{guardrail-reset-wzvtmpn2.js → guardrail-reset-269jdwhd.js} +29 -29
  13. package/dist/cli/{hive-promoter-55qcqgfx.js → hive-promoter-5936am48.js} +29 -29
  14. package/dist/cli/{index-0tyc8kdg.js → index-10zvxqra.js} +1 -1
  15. package/dist/cli/{index-6ybjrqf7.js → index-1tdsddab.js} +284 -107
  16. package/dist/cli/{index-23rneydq.js → index-2kzxkx50.js} +31 -31
  17. package/dist/cli/{index-tdzbm974.js → index-2mbs5q26.js} +108 -8
  18. package/dist/cli/{index-x3y6zhgj.js → index-3db7xza0.js} +1 -1
  19. package/dist/cli/{index-gn26q90p.js → index-3hdnfmhp.js} +2 -2
  20. package/dist/cli/{index-0tzv5zg2.js → index-3mhgv8d6.js} +2 -2
  21. package/dist/cli/{index-6vgb5fyb.js → index-48m4841x.js} +1 -1
  22. package/dist/cli/{index-mb7b1gye.js → index-4g00jhbs.js} +3 -3
  23. package/dist/cli/{index-1akn19mp.js → index-4y7g04py.js} +1 -1
  24. package/dist/cli/{index-n5pxkera.js → index-68w5jxv5.js} +4 -4
  25. package/dist/cli/{index-4pjkdcgc.js → index-6yc06dx9.js} +1 -1
  26. package/dist/cli/{index-2mqmvxac.js → index-7jnh1sgb.js} +7 -1
  27. package/dist/cli/{index-pk1gbqmq.js → index-c6dfyzdh.js} +1 -1
  28. package/dist/cli/{index-td0jz8xe.js → index-c809a69b.js} +2 -2
  29. package/dist/cli/{index-af0cqe6n.js → index-dwh02jmv.js} +7 -7
  30. package/dist/cli/{index-3cegckkq.js → index-garsang6.js} +1 -1
  31. package/dist/cli/{index-k10qtq71.js → index-hk4wa0v1.js} +6 -6
  32. package/dist/cli/{index-8v9sj4aq.js → index-mddgkqg9.js} +2 -2
  33. package/dist/cli/{index-4g0g3dqz.js → index-nkr634n6.js} +1 -1
  34. package/dist/cli/{index-ajkxx2k6.js → index-p4y7jefb.js} +2 -2
  35. package/dist/cli/{index-xkjrg9k4.js → index-pcj7jk4t.js} +3 -3
  36. package/dist/cli/{index-v689bmr2.js → index-qcz4xprd.js} +4 -4
  37. package/dist/cli/{index-zppxpc3y.js → index-s0a4ewkb.js} +2 -2
  38. package/dist/cli/{index-1dtshrzg.js → index-s70tmqn3.js} +5 -5
  39. package/dist/cli/{index-smqsp8hs.js → index-snnps1vm.js} +5 -5
  40. package/dist/cli/{index-n823qsae.js → index-tnm9bb8b.js} +11 -3
  41. package/dist/cli/{index-x8vw6ap1.js → index-ttqnhxxq.js} +59 -42
  42. package/dist/cli/{index-891g7hbf.js → index-vtdwkfbv.js} +1 -1
  43. package/dist/cli/index-wfxg3s7n.js +1158 -0
  44. package/dist/cli/{index-9v8d3c0k.js → index-wpstzk85.js} +4 -4
  45. package/dist/cli/{index-1gzdnjg9.js → index-y64jrbch.js} +2 -2
  46. package/dist/cli/{index-3ttktt56.js → index-yve4e8fx.js} +1 -1
  47. package/dist/cli/index.js +29 -29
  48. package/dist/cli/{knowledge-escalator-w05ztgvk.js → knowledge-escalator-bxpzeth0.js} +11 -11
  49. package/dist/cli/{knowledge-events-pwm9jc0m.js → knowledge-events-jxdpg2hj.js} +9 -9
  50. package/dist/cli/{knowledge-link-ge0rddxw.js → knowledge-link-nwxykjya.js} +5 -5
  51. package/dist/cli/{knowledge-store-4xv65tj2.js → knowledge-store-336hyzs8.js} +6 -6
  52. package/dist/cli/{knowledge-validator-0ttfsca7.js → knowledge-validator-9wkvnsv0.js} +8 -8
  53. package/dist/cli/{pending-delegations-nk78wx7h.js → pending-delegations-0fmj1qy6.js} +3 -3
  54. package/dist/cli/{pr-subscriptions-dq1f61ye.js → pr-subscriptions-1ndwgtbw.js} +15 -4
  55. package/dist/cli/{runner-n50w1y4w.js → runner-06dvm53x.js} +6 -6
  56. package/dist/cli/{scan-cursor-b6n15w8d.js → scan-cursor-3xxqtxwx.js} +7 -7
  57. package/dist/cli/{schema-jkbfwqr2.js → schema-jdx83m4m.js} +1 -1
  58. package/dist/cli/{scope-persistence-5xt2m5ep.js → scope-persistence-wgxn2kba.js} +8 -8
  59. package/dist/cli/{skill-generator-3keadpf7.js → skill-generator-vmxsc2fm.js} +13 -13
  60. package/dist/cli/{telemetry-hh1j99za.js → telemetry-q2km2zrz.js} +1 -1
  61. package/dist/cli/{worktree-collision-ownership-4drcjymy.js → worktree-collision-ownership-gsay5210.js} +5 -5
  62. package/dist/cli/{worktree-isolation-1ymaxn4h.js → worktree-isolation-peb4bkg3.js} +29 -29
  63. package/dist/commands/pr-monitor-status.d.ts +8 -1
  64. package/dist/commands/registry.d.ts +1 -1
  65. package/dist/commands/reset-session.d.ts +8 -2
  66. package/dist/config/schema.d.ts +8 -0
  67. package/dist/hooks/knowledge-application-gate.d.ts +10 -1
  68. package/dist/hooks/knowledge-receipt-ledger.d.ts +1 -1
  69. package/dist/hooks/pr-review-resilience-circuit.d.ts +232 -0
  70. package/dist/hooks/pr-workflow-gate.d.ts +10 -11
  71. package/dist/index.js +249 -240
  72. package/dist/observability/catalog.d.ts +6 -3
  73. package/dist/state.d.ts +6 -4
  74. package/dist/telemetry.d.ts +21 -1
  75. package/package.json +7 -1
  76. package/dist/cli/index-meg33yyv.js +0 -247
@@ -0,0 +1,1158 @@
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-vtdwkfbv.js";
11
+ import {
12
+ telemetry
13
+ } from "./index-ttqnhxxq.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 { folded, nextByte: startByte, eof: true, corruptLines };
179
+ }
180
+ const processLineBytes = (line) => {
181
+ if (line.length === 0)
182
+ return;
183
+ const text = line.toString("utf-8");
184
+ const trimmed = text.trim();
185
+ if (trimmed.length === 0)
186
+ return;
187
+ let parsed;
188
+ try {
189
+ parsed = JSON.parse(trimmed);
190
+ } catch {
191
+ corruptLines += 1;
192
+ return;
193
+ }
194
+ const result = RecordSchema.safeParse(parsed);
195
+ if (!result.success) {
196
+ corruptLines += 1;
197
+ return;
198
+ }
199
+ const rec = result.data;
200
+ if (rec.correlationId !== buildCorrelationId(rec.sessionID, rec.repoFullName, rec.prNumber)) {
201
+ corruptLines += 1;
202
+ return;
203
+ }
204
+ folded.set(rec.correlationId, rec);
205
+ };
206
+ try {
207
+ const size = fs.fstatSync(fd).size;
208
+ if (startByte >= size) {
209
+ return { folded, nextByte: startByte, eof: true, corruptLines };
210
+ }
211
+ const chunk = Buffer.alloc(PR_SUBSCRIPTION_LIMITS.readChunkBytes);
212
+ let pending = [];
213
+ let pendingLen = 0;
214
+ let oversize = false;
215
+ let pos = startByte;
216
+ const flushPendingAtEof = () => {
217
+ if (oversize) {
218
+ corruptLines += 1;
219
+ } else if (pendingLen > 0) {
220
+ processLineBytes(Buffer.concat(pending));
221
+ }
222
+ consumedTo = Math.max(consumedTo, pos);
223
+ pending = [];
224
+ pendingLen = 0;
225
+ oversize = false;
226
+ return { folded, nextByte: consumedTo, eof: true, corruptLines };
227
+ };
228
+ while (true) {
229
+ const want = Math.min(chunk.length, size - pos);
230
+ if (want <= 0)
231
+ return flushPendingAtEof();
232
+ const n = fs.readSync(fd, chunk, 0, want, pos);
233
+ if (n <= 0)
234
+ return flushPendingAtEof();
235
+ const data = chunk.subarray(0, n);
236
+ const base = pos;
237
+ pos += n;
238
+ let scan = 0;
239
+ while (scan < data.length) {
240
+ const nl = data.indexOf(10, scan);
241
+ if (nl === -1) {
242
+ pending.push(Buffer.from(data.subarray(scan)));
243
+ pendingLen += data.length - scan;
244
+ break;
245
+ }
246
+ const lineLen = pendingLen + (nl - scan);
247
+ if (oversize || lineLen > PR_SUBSCRIPTION_LIMITS.maxRecordBytes) {
248
+ corruptLines += 1;
249
+ } else {
250
+ const pieces = pending.length > 0 ? [...pending, data.subarray(scan, nl)] : [data.subarray(scan, nl)];
251
+ processLineBytes(Buffer.concat(pieces));
252
+ }
253
+ pending = [];
254
+ pendingLen = 0;
255
+ oversize = false;
256
+ consumedTo = base + nl + 1;
257
+ scan = nl + 1;
258
+ }
259
+ if (!oversize && pendingLen > PR_SUBSCRIPTION_LIMITS.maxRecordBytes) {
260
+ oversize = true;
261
+ pending = [];
262
+ pendingLen = 0;
263
+ }
264
+ if (consumedTo - startByte >= maxBytes && pendingLen === 0 && !oversize) {
265
+ const eof = consumedTo >= size;
266
+ return { folded, nextByte: consumedTo, eof, corruptLines };
267
+ }
268
+ }
269
+ } catch (err) {
270
+ log(`[pr-monitor] foldLegacyRegion failed: ${err instanceof Error ? err.message : String(err)}`);
271
+ return { folded, nextByte: consumedTo, eof: true, corruptLines };
272
+ } finally {
273
+ try {
274
+ fs.closeSync(fd);
275
+ } catch {}
276
+ }
277
+ }
278
+ function mergeFoldedRecords(view, overlay) {
279
+ let replaced = 0;
280
+ for (const [key, rec] of overlay) {
281
+ const base = view[key];
282
+ if (!base || rec.updatedAt >= base.updatedAt) {
283
+ view[key] = rec;
284
+ replaced += 1;
285
+ }
286
+ }
287
+ return replaced;
288
+ }
289
+ function readCheckpoint(directory) {
290
+ let stat;
291
+ try {
292
+ stat = fs.statSync(checkpointPath(directory));
293
+ } catch (err) {
294
+ if (err.code === "ENOENT") {
295
+ return { kind: "absent" };
296
+ }
297
+ return {
298
+ kind: "invalid",
299
+ reason: `unreadable (${err.code ?? "unknown"})`
300
+ };
301
+ }
302
+ if (stat.size > PR_SUBSCRIPTION_LIMITS.checkpointHardReadBytes) {
303
+ return {
304
+ kind: "invalid",
305
+ reason: `checkpoint exceeds hard read ceiling (${stat.size} > ${PR_SUBSCRIPTION_LIMITS.checkpointHardReadBytes} bytes)`
306
+ };
307
+ }
308
+ let raw;
309
+ try {
310
+ raw = fs.readFileSync(checkpointPath(directory), "utf-8");
311
+ } catch (err) {
312
+ return {
313
+ kind: "invalid",
314
+ reason: `unreadable (${err.code ?? "unknown"})`
315
+ };
316
+ }
317
+ let parsed;
318
+ try {
319
+ parsed = JSON.parse(raw);
320
+ } catch (err) {
321
+ return {
322
+ kind: "invalid",
323
+ reason: `malformed JSON (${err instanceof Error ? err.message : String(err)})`
324
+ };
325
+ }
326
+ const result = CheckpointSchema.safeParse(parsed);
327
+ if (!result.success) {
328
+ return { kind: "invalid", reason: result.error.message };
329
+ }
330
+ const checkpoint = result.data;
331
+ const recordCount = Object.keys(checkpoint.records).length;
332
+ if (recordCount > PR_SUBSCRIPTION_LIMITS.maxCheckpointRecords) {
333
+ return {
334
+ kind: "invalid",
335
+ reason: `records count ${recordCount} exceeds guard ${PR_SUBSCRIPTION_LIMITS.maxCheckpointRecords}`
336
+ };
337
+ }
338
+ for (const [key, rec] of Object.entries(checkpoint.records)) {
339
+ if (key !== rec.correlationId || rec.correlationId !== buildCorrelationId(rec.sessionID, rec.repoFullName, rec.prNumber)) {
340
+ return {
341
+ kind: "invalid",
342
+ reason: `record identity mismatch at key "${key}"`
343
+ };
344
+ }
345
+ }
346
+ return { kind: "ok", value: checkpoint };
347
+ }
348
+ function freshCheckpoint(directory) {
349
+ return {
350
+ schemaVersion: 1,
351
+ sequence: 0,
352
+ rootPath: path.resolve(directory),
353
+ updatedAt: Date.now(),
354
+ records: {},
355
+ terminalSummary: { removed: 0, expired: 0, lastTerminalAt: null },
356
+ migration: null,
357
+ maintenance: {
358
+ compactions: 0,
359
+ droppedAuditTransitions: 0,
360
+ corruptLegacyRecords: 0,
361
+ lastCompactedAt: null,
362
+ resets: 0
363
+ }
364
+ };
365
+ }
366
+ function cloneCheckpoint(cp) {
367
+ return JSON.parse(JSON.stringify(cp));
368
+ }
369
+ function validateRecord(record) {
370
+ const result = RecordSchema.safeParse(record);
371
+ if (!result.success) {
372
+ throw new Error(`Invalid subscription record: ${result.error.message}`);
373
+ }
374
+ }
375
+ function writeCheckpointFile(directory, checkpoint) {
376
+ const recordCount = Object.keys(checkpoint.records).length;
377
+ if (recordCount > PR_SUBSCRIPTION_LIMITS.maxCheckpointRecords) {
378
+ 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.`);
379
+ }
380
+ const content = `${JSON.stringify(checkpoint)}
381
+ `;
382
+ if (content.length > PR_SUBSCRIPTION_LIMITS.checkpointHardReadBytes) {
383
+ throw new Error(`PR subscription store over checkpoint capacity: ${content.length} 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.`);
384
+ }
385
+ if (content.length > PR_SUBSCRIPTION_LIMITS.maxCheckpointBytes) {
386
+ log(`[pr-monitor] checkpoint pressure: ${content.length}/${PR_SUBSCRIPTION_LIMITS.maxCheckpointBytes} bytes`);
387
+ }
388
+ ensureSwarmDir(directory);
389
+ atomicWriteSwarmFileSync(checkpointPath(directory), content);
390
+ }
391
+ function quarantineCheckpoint(directory, slot) {
392
+ const from = checkpointPath(directory);
393
+ const to = slot === "foreign" ? foreignSlotPath(directory) : corruptSlotPath(directory);
394
+ try {
395
+ try {
396
+ fs.unlinkSync(to);
397
+ } catch {}
398
+ renameWithRetry(from, to);
399
+ return true;
400
+ } catch (err) {
401
+ log(`[pr-monitor] checkpoint quarantine (${slot}) failed: ${err instanceof Error ? err.message : String(err)} \u2014 the next checkpoint write replaces it`);
402
+ return false;
403
+ }
404
+ }
405
+ function flushAuditEvents(directory, events, sequence, maintenance) {
406
+ if (events.length === 0)
407
+ return;
408
+ ensureSwarmDir(directory);
409
+ const lines = events.map((event) => JSON.stringify({
410
+ ts: Date.now(),
411
+ seq: sequence,
412
+ kind: event.kind,
413
+ ...event.correlationId ? { correlationId: event.correlationId } : {}
414
+ }));
415
+ try {
416
+ fs.appendFileSync(auditPath(directory), `${lines.join(`
417
+ `)}
418
+ `, "utf-8");
419
+ } catch (err) {
420
+ log(`[pr-monitor] audit append failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
421
+ return;
422
+ }
423
+ compactAuditTail(directory, maintenance);
424
+ }
425
+ function compactAuditTail(directory, maintenance) {
426
+ let size;
427
+ try {
428
+ size = fs.statSync(auditPath(directory)).size;
429
+ } catch {
430
+ return;
431
+ }
432
+ if (size <= PR_SUBSCRIPTION_LIMITS.auditMaxBytesLow)
433
+ return;
434
+ const windowBytes = 2 * PR_SUBSCRIPTION_LIMITS.auditMaxBytesHigh;
435
+ let content;
436
+ try {
437
+ if (size > windowBytes) {
438
+ const fd = fs.openSync(auditPath(directory), "r");
439
+ try {
440
+ const tail = Buffer.alloc(windowBytes);
441
+ const n = fs.readSync(fd, tail, 0, windowBytes, size - windowBytes);
442
+ const text = tail.subarray(0, n).toString("utf-8");
443
+ const firstNl = text.indexOf(`
444
+ `);
445
+ content = firstNl === -1 ? "" : text.slice(firstNl + 1);
446
+ } finally {
447
+ fs.closeSync(fd);
448
+ }
449
+ } else {
450
+ content = fs.readFileSync(auditPath(directory), "utf-8");
451
+ }
452
+ } catch {
453
+ return;
454
+ }
455
+ const all = content.split(`
456
+ `).filter((line) => line.trim().length > 0);
457
+ if (all.length <= PR_SUBSCRIPTION_LIMITS.auditMaxLinesHigh && size <= PR_SUBSCRIPTION_LIMITS.auditMaxBytesHigh) {
458
+ return;
459
+ }
460
+ const kept = [];
461
+ let bytes = 0;
462
+ for (let i = all.length - 1;i >= 0; i--) {
463
+ const lineBytes = Buffer.byteLength(all[i], "utf-8") + 1;
464
+ if (kept.length >= PR_SUBSCRIPTION_LIMITS.auditMaxLinesLow || bytes + lineBytes > PR_SUBSCRIPTION_LIMITS.auditMaxBytesLow) {
465
+ break;
466
+ }
467
+ kept.unshift(all[i]);
468
+ bytes += lineBytes;
469
+ }
470
+ const dropped = all.length - kept.length;
471
+ try {
472
+ atomicWriteSwarmFileSync(auditPath(directory), kept.length > 0 ? `${kept.join(`
473
+ `)}
474
+ ` : "");
475
+ maintenance.droppedAuditTransitions += dropped;
476
+ } catch (err) {
477
+ log(`[pr-monitor] audit compaction failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
478
+ }
479
+ }
480
+ function auditStats(directory) {
481
+ try {
482
+ const stat = fs.statSync(auditPath(directory));
483
+ let content;
484
+ if (stat.size > 2 * PR_SUBSCRIPTION_LIMITS.auditMaxBytesHigh) {
485
+ const fd = fs.openSync(auditPath(directory), "r");
486
+ try {
487
+ const head = Buffer.alloc(2 * PR_SUBSCRIPTION_LIMITS.auditMaxBytesHigh);
488
+ const n = fs.readSync(fd, head, 0, head.length, 0);
489
+ content = head.subarray(0, n).toString("utf-8");
490
+ } finally {
491
+ fs.closeSync(fd);
492
+ }
493
+ } else {
494
+ content = fs.readFileSync(auditPath(directory), "utf-8");
495
+ }
496
+ return {
497
+ lines: content.split(`
498
+ `).filter((line) => line.trim().length > 0).length,
499
+ bytes: stat.size
500
+ };
501
+ } catch {
502
+ return { lines: 0, bytes: 0 };
503
+ }
504
+ }
505
+ function compactTerminalRecords(view, checkpoint) {
506
+ const now = Date.now();
507
+ const terminals = Object.entries(view).filter(([, rec]) => rec.status !== "active").sort((a, b) => b[1].updatedAt - a[1].updatedAt);
508
+ if (terminals.length === 0)
509
+ return null;
510
+ const hasAged = terminals.some(([, rec]) => now - rec.updatedAt > PR_SUBSCRIPTION_LIMITS.terminalMaxAgeMs);
511
+ if (!hasAged && terminals.length <= PR_SUBSCRIPTION_LIMITS.terminalRecordsHigh) {
512
+ return null;
513
+ }
514
+ let kept = 0;
515
+ for (const [key, rec] of terminals) {
516
+ const aged = now - rec.updatedAt > PR_SUBSCRIPTION_LIMITS.terminalMaxAgeMs;
517
+ if (aged || kept >= PR_SUBSCRIPTION_LIMITS.terminalRecordsLow) {
518
+ if (rec.status === "removed")
519
+ checkpoint.terminalSummary.removed += 1;
520
+ else
521
+ checkpoint.terminalSummary.expired += 1;
522
+ checkpoint.terminalSummary.lastTerminalAt = Math.max(checkpoint.terminalSummary.lastTerminalAt ?? 0, rec.updatedAt);
523
+ delete view[key];
524
+ } else {
525
+ kept += 1;
526
+ }
527
+ }
528
+ checkpoint.maintenance.compactions += 1;
529
+ checkpoint.maintenance.lastCompactedAt = now;
530
+ return { kind: "compact" };
531
+ }
532
+ function maybeArchiveLegacy(directory, checkpoint) {
533
+ const migration = checkpoint.migration;
534
+ if (!migration || !migration.done || migration.archived)
535
+ return null;
536
+ const legacy = storePath(directory);
537
+ const size = fileSizeOrNull(legacy);
538
+ if (size === null) {
539
+ migration.archived = true;
540
+ return null;
541
+ }
542
+ if (size !== migration.sourceBytes)
543
+ return null;
544
+ try {
545
+ try {
546
+ fs.unlinkSync(legacyArchivePath(directory));
547
+ } catch {}
548
+ renameWithRetry(legacy, legacyArchivePath(directory));
549
+ } catch (err) {
550
+ log(`[pr-monitor] legacy archive deferred: ${err instanceof Error ? err.message : String(err)}`);
551
+ return null;
552
+ }
553
+ migration.archived = true;
554
+ return { kind: "archive" };
555
+ }
556
+ function cleanupStaleLegacyArchive(directory) {
557
+ try {
558
+ const stat = fs.statSync(legacyArchivePath(directory));
559
+ if (Date.now() - stat.mtimeMs > PR_SUBSCRIPTION_LIMITS.legacyArchiveTtlMs) {
560
+ fs.unlinkSync(legacyArchivePath(directory));
561
+ }
562
+ } catch {}
563
+ }
564
+ function emitHealthTelemetry(trigger, checkpoint) {
565
+ const records = Object.values(checkpoint.records);
566
+ telemetry.prSubscriptionHealth({
567
+ trigger,
568
+ active_count: records.filter((r) => r.status === "active").length,
569
+ terminal_count: records.filter((r) => r.status !== "active").length,
570
+ compactions: checkpoint.maintenance.compactions,
571
+ corrupt_count: checkpoint.maintenance.corruptLegacyRecords,
572
+ dropped_audit_count: checkpoint.maintenance.droppedAuditTransitions,
573
+ checkpoint_bytes: JSON.stringify(checkpoint).length,
574
+ limit_bytes: PR_SUBSCRIPTION_LIMITS.maxCheckpointBytes
575
+ });
576
+ }
577
+ var legacyOverLimitWarned = new Map;
578
+ function warnLegacyOverLimit(directory, size) {
579
+ if (legacyOverLimitWarned.has(directory))
580
+ return;
581
+ if (legacyOverLimitWarned.size >= 32) {
582
+ legacyOverLimitWarned.delete(legacyOverLimitWarned.keys().next().value);
583
+ }
584
+ legacyOverLimitWarned.set(directory, true);
585
+ 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)`);
586
+ }
587
+ var migrationCapacityWarned = new Map;
588
+ function warnMigrationCapacity(directory, records) {
589
+ if (migrationCapacityWarned.has(directory))
590
+ return;
591
+ if (migrationCapacityWarned.size >= 32) {
592
+ migrationCapacityWarned.delete(migrationCapacityWarned.keys().next().value);
593
+ }
594
+ migrationCapacityWarned.set(directory, true);
595
+ 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`);
596
+ }
597
+ function overlayLegacy(directory, checkpoint) {
598
+ const view = { ...checkpoint.records };
599
+ const legacy = storePath(directory);
600
+ let stat = null;
601
+ try {
602
+ stat = fs.statSync(legacy);
603
+ } catch {}
604
+ if (stat === null) {
605
+ return {
606
+ view,
607
+ usedLegacy: false,
608
+ absorbed: 0,
609
+ corruptLines: 0,
610
+ overLimit: false
611
+ };
612
+ }
613
+ if (stat.size > PR_SUBSCRIPTION_LIMITS.legacySourceMaxBytes) {
614
+ warnLegacyOverLimit(directory, stat.size);
615
+ return {
616
+ view,
617
+ usedLegacy: false,
618
+ absorbed: 0,
619
+ corruptLines: 0,
620
+ overLimit: true
621
+ };
622
+ }
623
+ const migration = checkpoint.migration;
624
+ if (migration === null || !migration.done) {
625
+ let cursor = migration?.scannedBytes ?? 0;
626
+ if (cursor > stat.size)
627
+ cursor = 0;
628
+ const fold2 = foldLegacyRegion(legacy, cursor, Number.MAX_SAFE_INTEGER);
629
+ const absorbed2 = mergeFoldedRecords(view, fold2.folded);
630
+ return {
631
+ view,
632
+ usedLegacy: true,
633
+ absorbed: absorbed2,
634
+ corruptLines: fold2.corruptLines,
635
+ overLimit: false
636
+ };
637
+ }
638
+ if (stat.size === migration.sourceBytes && stat.mtimeMs === migration.sourceMtimeMs) {
639
+ return {
640
+ view,
641
+ usedLegacy: false,
642
+ absorbed: 0,
643
+ corruptLines: 0,
644
+ overLimit: false
645
+ };
646
+ }
647
+ const fold = foldLegacyRegion(legacy, 0, Number.MAX_SAFE_INTEGER);
648
+ const absorbed = mergeFoldedRecords(view, fold.folded);
649
+ return {
650
+ view,
651
+ usedLegacy: true,
652
+ absorbed,
653
+ corruptLines: fold.corruptLines,
654
+ overLimit: false
655
+ };
656
+ }
657
+ async function loadViewForRead(directory) {
658
+ const read = readCheckpoint(directory);
659
+ if (read.kind === "ok") {
660
+ if (!sameProjectRoot(read.value.rootPath, directory)) {
661
+ return { view: {}, recoverySource: "foreign" };
662
+ }
663
+ const overlaid = overlayLegacy(directory, read.value);
664
+ return {
665
+ view: overlaid.view,
666
+ recoverySource: overlaid.usedLegacy ? "checkpoint+legacy" : "checkpoint"
667
+ };
668
+ }
669
+ if (read.kind === "invalid") {
670
+ const legacy = storePath(directory);
671
+ const view2 = {};
672
+ const size = fileSizeOrNull(legacy);
673
+ if (size !== null && size <= PR_SUBSCRIPTION_LIMITS.legacySourceMaxBytes) {
674
+ const fold2 = foldLegacyRegion(legacy, 0, Number.MAX_SAFE_INTEGER);
675
+ mergeFoldedRecords(view2, fold2.folded);
676
+ } else if (size !== null) {
677
+ warnLegacyOverLimit(directory, size);
678
+ }
679
+ return { view: view2, recoverySource: "corrupt-recovered" };
680
+ }
681
+ const legacyPath = storePath(directory);
682
+ const legacySize = fileSizeOrNull(legacyPath);
683
+ if (legacySize === null) {
684
+ return { view: {}, recoverySource: "empty" };
685
+ }
686
+ if (legacySize > PR_SUBSCRIPTION_LIMITS.legacySourceMaxBytes) {
687
+ warnLegacyOverLimit(directory, legacySize);
688
+ return { view: {}, recoverySource: "legacy-log" };
689
+ }
690
+ const fold = foldLegacyRegion(legacyPath, 0, Number.MAX_SAFE_INTEGER);
691
+ const view = {};
692
+ mergeFoldedRecords(view, fold.folded);
693
+ await bootstrapCheckpointFromLegacy(directory, view, fold);
694
+ return { view, recoverySource: "legacy-log" };
695
+ }
696
+ async function bootstrapCheckpointFromLegacy(directory, foldedView, fold) {
697
+ try {
698
+ await withEvidenceLock(directory, PR_SUBSCRIPTIONS_FILE, STORE_LOCK_AGENT, STORE_LOCK_TASK, async () => {
699
+ if (readCheckpoint(directory).kind !== "absent")
700
+ return;
701
+ const checkpoint = freshCheckpoint(directory);
702
+ checkpoint.records = { ...foldedView };
703
+ const terminals = Object.values(foldedView).filter((rec) => rec.status !== "active");
704
+ checkpoint.terminalSummary.lastTerminalAt = terminals.length ? Math.max(...terminals.map((rec) => rec.updatedAt)) : null;
705
+ checkpoint.maintenance.corruptLegacyRecords = fold.corruptLines;
706
+ let bootstrapMtime = 0;
707
+ try {
708
+ bootstrapMtime = fs.statSync(storePath(directory)).mtimeMs;
709
+ } catch {}
710
+ checkpoint.migration = {
711
+ scannedBytes: fold.nextByte,
712
+ sourceBytes: fold.nextByte,
713
+ sourceMtimeMs: bootstrapMtime,
714
+ corruptLines: fold.corruptLines,
715
+ done: true,
716
+ archived: false,
717
+ startedAt: Date.now()
718
+ };
719
+ checkpoint.sequence = 1;
720
+ checkpoint.updatedAt = Date.now();
721
+ writeCheckpointFile(directory, checkpoint);
722
+ flushAuditEvents(directory, [{ kind: "migrate-complete" }], checkpoint.sequence, checkpoint.maintenance);
723
+ emitHealthTelemetry("migrate-complete", checkpoint);
724
+ }, PR_SUBSCRIPTION_LIMITS.bootstrapLockTimeoutMs);
725
+ } catch (err) {
726
+ log(`[pr-monitor] read-bootstrap skipped: ${err instanceof Error ? err.message : String(err)}`);
727
+ }
728
+ }
729
+ function loadViewForWrite(directory) {
730
+ const audit = [];
731
+ let dirty = false;
732
+ let checkpoint;
733
+ const read = readCheckpoint(directory);
734
+ if (read.kind === "ok" && sameProjectRoot(read.value.rootPath, directory)) {
735
+ checkpoint = cloneCheckpoint(read.value);
736
+ } else if (read.kind === "ok") {
737
+ quarantineCheckpoint(directory, "foreign");
738
+ checkpoint = freshCheckpoint(directory);
739
+ checkpoint.maintenance.resets += 1;
740
+ audit.push({ kind: "foreign-rebind" });
741
+ dirty = true;
742
+ } else if (read.kind === "invalid") {
743
+ quarantineCheckpoint(directory, "corrupt");
744
+ checkpoint = freshCheckpoint(directory);
745
+ checkpoint.maintenance.resets += 1;
746
+ audit.push({ kind: "corrupt-quarantine" });
747
+ dirty = true;
748
+ } else {
749
+ checkpoint = freshCheckpoint(directory);
750
+ }
751
+ const legacy = storePath(directory);
752
+ let legacyStat = null;
753
+ try {
754
+ legacyStat = fs.statSync(legacy);
755
+ } catch {}
756
+ let view;
757
+ if (legacyStat === null) {
758
+ view = { ...checkpoint.records };
759
+ if (checkpoint.migration?.done && !checkpoint.migration.archived) {
760
+ checkpoint.migration.archived = true;
761
+ dirty = true;
762
+ }
763
+ return { checkpoint, view, audit, dirty };
764
+ }
765
+ if (legacyStat.size > PR_SUBSCRIPTION_LIMITS.legacySourceMaxBytes) {
766
+ warnLegacyOverLimit(directory, legacyStat.size);
767
+ return { checkpoint, view: { ...checkpoint.records }, audit, dirty };
768
+ }
769
+ const legacySize = legacyStat.size;
770
+ const legacyMtime = legacyStat.mtimeMs;
771
+ const migration = checkpoint.migration;
772
+ if (migration === null || !migration.done) {
773
+ let cursor = migration?.scannedBytes ?? 0;
774
+ if (cursor > legacySize)
775
+ cursor = 0;
776
+ const startedAt = migration?.startedAt ?? Date.now();
777
+ let corruptTotal = migration?.corruptLines ?? 0;
778
+ view = { ...checkpoint.records };
779
+ let capacityRefused = false;
780
+ while (true) {
781
+ const fold = foldLegacyRegion(legacy, cursor, PR_SUBSCRIPTION_LIMITS.migrationChunkBytes);
782
+ mergeFoldedRecords(view, fold.folded);
783
+ checkpoint.maintenance.corruptLegacyRecords += fold.corruptLines;
784
+ corruptTotal += fold.corruptLines;
785
+ cursor = fold.nextByte;
786
+ const compactEvent = compactTerminalRecords(view, checkpoint);
787
+ if (compactEvent)
788
+ audit.push(compactEvent);
789
+ if (Object.keys(view).length > PR_SUBSCRIPTION_LIMITS.maxCheckpointRecords) {
790
+ capacityRefused = true;
791
+ warnMigrationCapacity(directory, Object.keys(view).length);
792
+ break;
793
+ }
794
+ if (!fold.eof) {
795
+ const progress = cloneCheckpoint(checkpoint);
796
+ progress.records = { ...view };
797
+ progress.migration = {
798
+ scannedBytes: cursor,
799
+ sourceBytes: legacySize,
800
+ sourceMtimeMs: legacyMtime,
801
+ corruptLines: corruptTotal,
802
+ done: false,
803
+ archived: false,
804
+ startedAt
805
+ };
806
+ progress.updatedAt = Date.now();
807
+ progress.sequence += 1;
808
+ writeCheckpointFile(directory, progress);
809
+ checkpoint.sequence = progress.sequence;
810
+ dirty = true;
811
+ continue;
812
+ }
813
+ checkpoint.migration = {
814
+ scannedBytes: cursor,
815
+ sourceBytes: legacySize,
816
+ sourceMtimeMs: legacyMtime,
817
+ corruptLines: corruptTotal,
818
+ done: true,
819
+ archived: false,
820
+ startedAt
821
+ };
822
+ audit.push({ kind: "migrate-complete" });
823
+ dirty = true;
824
+ break;
825
+ }
826
+ if (capacityRefused) {
827
+ return { checkpoint, view, audit, dirty: false };
828
+ }
829
+ return { checkpoint, view, audit, dirty };
830
+ }
831
+ const overlaid = overlayLegacy(directory, checkpoint);
832
+ view = overlaid.view;
833
+ if (overlaid.usedLegacy) {
834
+ migration.sourceBytes = legacySize;
835
+ migration.sourceMtimeMs = legacyMtime;
836
+ migration.scannedBytes = legacySize;
837
+ migration.archived = false;
838
+ checkpoint.maintenance.corruptLegacyRecords += overlaid.corruptLines;
839
+ dirty = true;
840
+ }
841
+ return { checkpoint, view, audit, dirty };
842
+ }
843
+ function finalizeWrite(directory, loaded, opAudit) {
844
+ const { checkpoint } = loaded;
845
+ const events = [...loaded.audit, ...opAudit];
846
+ const compactEvent = compactTerminalRecords(loaded.view, checkpoint);
847
+ if (compactEvent)
848
+ events.push(compactEvent);
849
+ if (!loaded.dirty && events.length === 0)
850
+ return;
851
+ checkpoint.records = loaded.view;
852
+ checkpoint.updatedAt = Date.now();
853
+ checkpoint.rootPath = path.resolve(directory);
854
+ checkpoint.sequence += 1;
855
+ flushAuditEvents(directory, events, checkpoint.sequence, checkpoint.maintenance);
856
+ writeCheckpointFile(directory, checkpoint);
857
+ const archiveEvent = maybeArchiveLegacy(directory, checkpoint);
858
+ if (archiveEvent) {
859
+ events.push(archiveEvent);
860
+ checkpoint.sequence += 1;
861
+ flushAuditEvents(directory, [archiveEvent], checkpoint.sequence, checkpoint.maintenance);
862
+ writeCheckpointFile(directory, checkpoint);
863
+ }
864
+ cleanupStaleLegacyArchive(directory);
865
+ for (const kind of [
866
+ "compact",
867
+ "migrate-complete",
868
+ "archive",
869
+ "foreign-rebind",
870
+ "corrupt-quarantine"
871
+ ]) {
872
+ if (events.some((event) => event.kind === kind)) {
873
+ emitHealthTelemetry(kind, checkpoint);
874
+ }
875
+ }
876
+ }
877
+ async function subscribe(directory, input) {
878
+ if (!directory || directory.trim() === "") {
879
+ throw new Error("directory is required");
880
+ }
881
+ if (!input.sessionID || input.sessionID.trim() === "") {
882
+ throw new Error("sessionID is required and must be non-empty");
883
+ }
884
+ if (!input.repoFullName || input.repoFullName.trim() === "") {
885
+ throw new Error("repoFullName is required and must be non-empty");
886
+ }
887
+ if (!input.prUrl || input.prUrl.trim() === "") {
888
+ throw new Error("prUrl is required and must be non-empty");
889
+ }
890
+ if (!input.prNumber || !Number.isInteger(input.prNumber) || input.prNumber <= 0) {
891
+ throw new Error("prNumber is required and must be a positive integer");
892
+ }
893
+ const correlationId = buildCorrelationId(input.sessionID, input.repoFullName, input.prNumber);
894
+ const now = Date.now();
895
+ return withEvidenceLock(directory, PR_SUBSCRIPTIONS_FILE, STORE_LOCK_AGENT, STORE_LOCK_TASK, async () => {
896
+ const loaded = loadViewForWrite(directory);
897
+ const match = Object.values(loaded.view).find((r) => r.correlationId === correlationId && r.status === "active");
898
+ if (match) {
899
+ onSubscriptionCreated?.(directory, match);
900
+ finalizeWrite(directory, loaded, []);
901
+ return match;
902
+ }
903
+ if (input.maxSubscriptions === undefined || input.maxSubscriptions > 0) {
904
+ const limit = input.maxSubscriptions !== undefined ? input.maxSubscriptions : PR_SUBSCRIPTION_LIMITS.defaultMaxActiveSubscriptions;
905
+ const activeCount = Object.values(loaded.view).filter((r) => r.status === "active").length;
906
+ if (activeCount >= limit) {
907
+ throw new Error(`PR subscription limit reached: ${activeCount}/${limit}`);
908
+ }
909
+ }
910
+ const record = {
911
+ correlationId,
912
+ sessionID: input.sessionID,
913
+ prNumber: input.prNumber,
914
+ repoFullName: input.repoFullName,
915
+ prUrl: input.prUrl,
916
+ lastCheckedAt: now,
917
+ isWatching: true,
918
+ hasUnaddressedEvents: false,
919
+ status: "active",
920
+ createdAt: now,
921
+ updatedAt: now,
922
+ errorCount: 0
923
+ };
924
+ validateRecord(record);
925
+ loaded.view[correlationId] = record;
926
+ loaded.dirty = true;
927
+ finalizeWrite(directory, loaded, [{ kind: "subscribe", correlationId }]);
928
+ onSubscriptionCreated?.(directory, record);
929
+ return record;
930
+ });
931
+ }
932
+ async function unsubscribe(directory, correlationId) {
933
+ if (!correlationId)
934
+ return null;
935
+ return withEvidenceLock(directory, PR_SUBSCRIPTIONS_FILE, STORE_LOCK_AGENT, STORE_LOCK_TASK, async () => {
936
+ const loaded = loadViewForWrite(directory);
937
+ const match = Object.values(loaded.view).find((r) => r.correlationId === correlationId && r.status === "active");
938
+ if (!match) {
939
+ finalizeWrite(directory, loaded, []);
940
+ return null;
941
+ }
942
+ const now = Date.now();
943
+ const removed = {
944
+ ...match,
945
+ status: "removed",
946
+ isWatching: false,
947
+ updatedAt: now
948
+ };
949
+ loaded.view[correlationId] = removed;
950
+ loaded.dirty = true;
951
+ finalizeWrite(directory, loaded, [
952
+ { kind: "unsubscribe", correlationId }
953
+ ]);
954
+ return removed;
955
+ });
956
+ }
957
+ async function listActive(directory) {
958
+ const { view } = await loadViewForRead(directory);
959
+ return Object.values(view).filter((r) => r.status === "active");
960
+ }
961
+ async function lookupByPr(directory, repoFullName, prNumber) {
962
+ const { view } = await loadViewForRead(directory);
963
+ for (const record of Object.values(view)) {
964
+ if (record.status === "active" && record.repoFullName === repoFullName && record.prNumber === prNumber) {
965
+ return record;
966
+ }
967
+ }
968
+ return null;
969
+ }
970
+ async function updateSnapshot(directory, correlationId, updates) {
971
+ if (!correlationId)
972
+ return null;
973
+ return withEvidenceLock(directory, PR_SUBSCRIPTIONS_FILE, STORE_LOCK_AGENT, STORE_LOCK_TASK, async () => {
974
+ const loaded = loadViewForWrite(directory);
975
+ const match = Object.values(loaded.view).find((r) => r.correlationId === correlationId && r.status === "active");
976
+ if (!match) {
977
+ finalizeWrite(directory, loaded, []);
978
+ return null;
979
+ }
980
+ const updated = {
981
+ ...match,
982
+ ...updates,
983
+ correlationId,
984
+ sessionID: match.sessionID,
985
+ repoFullName: match.repoFullName,
986
+ prNumber: match.prNumber,
987
+ prUrl: match.prUrl,
988
+ createdAt: match.createdAt,
989
+ updatedAt: Date.now()
990
+ };
991
+ loaded.view[correlationId] = updated;
992
+ loaded.dirty = true;
993
+ finalizeWrite(directory, loaded, []);
994
+ return updated;
995
+ });
996
+ }
997
+ async function sweepStale(directory, ttlDays, mergedPrs) {
998
+ if (!ttlDays || ttlDays <= 0)
999
+ return 0;
1000
+ const ttlMs = ttlDays * 86400000;
1001
+ const now = Date.now();
1002
+ try {
1003
+ return await withEvidenceLock(directory, PR_SUBSCRIPTIONS_FILE, STORE_LOCK_AGENT, STORE_LOCK_TASK, async () => {
1004
+ const loaded = loadViewForWrite(directory);
1005
+ const opAudit = [];
1006
+ let swept = 0;
1007
+ for (const record of Object.values(loaded.view)) {
1008
+ if (record.status !== "active")
1009
+ continue;
1010
+ const prKey = `${record.repoFullName}::${record.prNumber}`;
1011
+ const isMerged = mergedPrs?.has(prKey) ?? false;
1012
+ const isStale = now - record.updatedAt > ttlMs;
1013
+ if (isMerged) {
1014
+ loaded.view[record.correlationId] = {
1015
+ ...record,
1016
+ status: "expired",
1017
+ isWatching: false,
1018
+ updatedAt: now
1019
+ };
1020
+ swept += 1;
1021
+ opAudit.push({
1022
+ kind: "expired",
1023
+ correlationId: record.correlationId
1024
+ });
1025
+ loaded.dirty = true;
1026
+ log(`[pr-monitor] Swept subscription: merged/closed: ${prKey}`);
1027
+ continue;
1028
+ }
1029
+ if (isStale && !record.hasUnaddressedEvents) {
1030
+ loaded.view[record.correlationId] = {
1031
+ ...record,
1032
+ status: "expired",
1033
+ isWatching: false,
1034
+ updatedAt: now
1035
+ };
1036
+ swept += 1;
1037
+ opAudit.push({
1038
+ kind: "expired",
1039
+ correlationId: record.correlationId
1040
+ });
1041
+ loaded.dirty = true;
1042
+ log(`[pr-monitor] Swept subscription: stale (TTL ${ttlDays}d): ${prKey}`);
1043
+ }
1044
+ }
1045
+ finalizeWrite(directory, loaded, opAudit);
1046
+ return swept;
1047
+ });
1048
+ } catch (err) {
1049
+ log(`[pr-monitor] sweepStale failed: ${err instanceof Error ? err.message : String(err)}`);
1050
+ return 0;
1051
+ }
1052
+ }
1053
+ async function getPrSubscriptionHealth(directory) {
1054
+ try {
1055
+ const read = readCheckpoint(directory);
1056
+ const audit = auditStats(directory);
1057
+ const archiveBytes = fileSizeOrNull(legacyArchivePath(directory)) ?? 0;
1058
+ const checkpointBytes = fileSizeOrNull(checkpointPath(directory)) ?? 0;
1059
+ const legacySize = fileSizeOrNull(storePath(directory));
1060
+ const legacyOverLimit = legacySize !== null && legacySize > PR_SUBSCRIPTION_LIMITS.legacySourceMaxBytes;
1061
+ let health;
1062
+ if (read.kind === "ok" && !sameProjectRoot(read.value.rootPath, directory)) {
1063
+ health = emptyHealth();
1064
+ health.recoverySource = "foreign";
1065
+ } else if (read.kind === "invalid") {
1066
+ if (legacySize !== null && !legacyOverLimit) {
1067
+ const fold = foldLegacyRegion(storePath(directory), 0, Number.MAX_SAFE_INTEGER);
1068
+ const view = {};
1069
+ mergeFoldedRecords(view, fold.folded);
1070
+ health = healthFromView(view, freshCheckpoint(directory));
1071
+ health.corruptLegacyRecords = fold.corruptLines;
1072
+ } else {
1073
+ health = emptyHealth();
1074
+ }
1075
+ health.recoverySource = "corrupt-recovered";
1076
+ } else if (read.kind === "absent") {
1077
+ if (legacySize === null || legacyOverLimit) {
1078
+ health = emptyHealth();
1079
+ } else {
1080
+ const fold = foldLegacyRegion(storePath(directory), 0, Number.MAX_SAFE_INTEGER);
1081
+ const view = {};
1082
+ mergeFoldedRecords(view, fold.folded);
1083
+ health = healthFromView(view, freshCheckpoint(directory));
1084
+ health.corruptLegacyRecords = fold.corruptLines;
1085
+ }
1086
+ health.recoverySource = legacySize === null ? "empty" : "legacy-log";
1087
+ } else {
1088
+ const overlaid = overlayLegacy(directory, read.value);
1089
+ health = healthFromView(overlaid.view, read.value);
1090
+ health.recoverySource = overlaid.usedLegacy ? "checkpoint+legacy" : "checkpoint";
1091
+ }
1092
+ health.legacyOverLimit = legacyOverLimit;
1093
+ applyFileStats(health, checkpointBytes, audit, archiveBytes);
1094
+ return health;
1095
+ } catch (err) {
1096
+ log(`[pr-monitor] getPrSubscriptionHealth failed: ${err instanceof Error ? err.message : String(err)}`);
1097
+ return emptyHealth();
1098
+ }
1099
+ }
1100
+ function applyFileStats(health, checkpointBytes, audit, archiveBytes) {
1101
+ health.checkpointBytes = checkpointBytes;
1102
+ health.pressurePct = checkpointBytes === 0 ? 0 : Math.round(checkpointBytes / health.checkpointLimitBytes * 1000) / 10;
1103
+ health.auditLines = audit.lines;
1104
+ health.auditBytes = audit.bytes;
1105
+ health.legacyArchiveBytes = archiveBytes;
1106
+ }
1107
+ function emptyHealth() {
1108
+ return {
1109
+ schemaVersion: 1,
1110
+ sequence: 0,
1111
+ checkpointAgeMs: null,
1112
+ checkpointBytes: 0,
1113
+ checkpointLimitBytes: PR_SUBSCRIPTION_LIMITS.maxCheckpointBytes,
1114
+ pressurePct: 0,
1115
+ activeCount: 0,
1116
+ removedCount: 0,
1117
+ expiredCount: 0,
1118
+ terminalSummary: { removed: 0, expired: 0, lastTerminalAt: null },
1119
+ auditLines: 0,
1120
+ auditBytes: 0,
1121
+ auditLimitBytes: PR_SUBSCRIPTION_LIMITS.auditMaxBytesHigh,
1122
+ compactions: 0,
1123
+ corruptLegacyRecords: 0,
1124
+ droppedAuditTransitions: 0,
1125
+ resets: 0,
1126
+ recoverySource: "empty",
1127
+ migration: null,
1128
+ legacyArchiveBytes: 0,
1129
+ legacyOverLimit: false
1130
+ };
1131
+ }
1132
+ function healthFromView(view, checkpoint) {
1133
+ const records = Object.values(view);
1134
+ const health = emptyHealth();
1135
+ health.sequence = checkpoint.sequence;
1136
+ health.checkpointAgeMs = checkpoint.sequence > 0 ? Date.now() - checkpoint.updatedAt : null;
1137
+ health.activeCount = records.filter((r) => r.status === "active").length;
1138
+ health.removedCount = records.filter((r) => r.status === "removed").length;
1139
+ health.expiredCount = records.filter((r) => r.status === "expired").length;
1140
+ health.terminalSummary = { ...checkpoint.terminalSummary };
1141
+ health.compactions = checkpoint.maintenance.compactions;
1142
+ health.corruptLegacyRecords = checkpoint.maintenance.corruptLegacyRecords;
1143
+ health.droppedAuditTransitions = checkpoint.maintenance.droppedAuditTransitions;
1144
+ health.resets = checkpoint.maintenance.resets;
1145
+ health.migration = checkpoint.migration ? {
1146
+ done: checkpoint.migration.done,
1147
+ scannedBytes: checkpoint.migration.scannedBytes,
1148
+ sourceBytes: checkpoint.migration.sourceBytes
1149
+ } : null;
1150
+ return health;
1151
+ }
1152
+ var _internals = {
1153
+ foldLegacyRegion,
1154
+ mergeFoldedRecords,
1155
+ sameProjectRoot
1156
+ };
1157
+
1158
+ 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 };