@a9n-shoji/rvw 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +6 -3
  3. package/dist/cli.mjs +188 -19
  4. package/dist/cli.mjs.map +4 -4
  5. package/dist/web/assets/DocumentViewer-B4LdSL1l.js +1560 -0
  6. package/dist/web/assets/{MarkdownTable-DVJ0_MIC.js → MarkdownTable-CdbKPft2.js} +1 -1
  7. package/dist/web/assets/WalkthroughViewer-D1LlSQw2.js +1 -0
  8. package/dist/web/assets/{abnfDiagram-N423BO3Z-6dQzEuPV.js → abnfDiagram-N423BO3Z-CE1nZSC9.js} +1 -1
  9. package/dist/web/assets/architecture-TIHT7OUA-edLV2Zq-.js +1 -0
  10. package/dist/web/assets/{architectureDiagram-T3A2C74G-DP7hHV7T.js → architectureDiagram-T3A2C74G-B88qYFUo.js} +1 -1
  11. package/dist/web/assets/{blockDiagram-VBNYF7ZC-ZbwzVKWP.js → blockDiagram-VBNYF7ZC-BaX0rhbb.js} +1 -1
  12. package/dist/web/assets/{c4Diagram-5PPSVZJV-HVD9_uV4.js → c4Diagram-5PPSVZJV-C1ppBaEC.js} +1 -1
  13. package/dist/web/assets/channel-J8FHSJDG.js +1 -0
  14. package/dist/web/assets/{chunk-2GRJ4B5K-Bkxs2d4J.js → chunk-2GRJ4B5K-D2TTocfh.js} +1 -1
  15. package/dist/web/assets/{chunk-4I5QYGJK-BJHE6zC_.js → chunk-4I5QYGJK-ikT25rPN.js} +1 -1
  16. package/dist/web/assets/{chunk-5RXB4S5H-B-52dfCQ.js → chunk-5RXB4S5H-CGlPaulj.js} +1 -1
  17. package/dist/web/assets/{chunk-6Q2QTUOP-BpD0vQ66.js → chunk-6Q2QTUOP-BuuMeCo2.js} +1 -1
  18. package/dist/web/assets/{chunk-7Z6QIM7H-BTI1qgAO.js → chunk-7Z6QIM7H-DT3BAZ4L.js} +1 -1
  19. package/dist/web/assets/{chunk-GF5L2VYU-B8BxoCCx.js → chunk-GF5L2VYU-mIdlxzfr.js} +1 -1
  20. package/dist/web/assets/{chunk-I66GZJ75-zwi-Xf01.js → chunk-I66GZJ75-CGl1hx4J.js} +1 -1
  21. package/dist/web/assets/{chunk-JQJVKLGR-D9mKBWUz.js → chunk-JQJVKLGR-BLG6gAfx.js} +1 -1
  22. package/dist/web/assets/{chunk-KBJHAD2P-Qmtir3qc.js → chunk-KBJHAD2P-D_MPzBJx.js} +1 -1
  23. package/dist/web/assets/{chunk-NSK5VX7P-SP2vZdAo.js → chunk-NSK5VX7P-CiYiU8wP.js} +1 -1
  24. package/dist/web/assets/{chunk-QR6OTTB3-uVDCHurs.js → chunk-QR6OTTB3-BK8Tnvcc.js} +1 -1
  25. package/dist/web/assets/{chunk-UBXNYLIW-CtNzBWOU.js → chunk-UBXNYLIW-BuzttTFk.js} +1 -1
  26. package/dist/web/assets/{chunk-W5SLKNZC-BPfDqbmK.js → chunk-W5SLKNZC-UidmFHe2.js} +1 -1
  27. package/dist/web/assets/{chunk-WRU74C26-BZkEOexq.js → chunk-WRU74C26-nox7WFnh.js} +1 -1
  28. package/dist/web/assets/classDiagram-JCYQIIEL-CmTWSnH0.js +1 -0
  29. package/dist/web/assets/classDiagram-v2-OCEON4UE-CmTWSnH0.js +1 -0
  30. package/dist/web/assets/{cynefin-VYW2F7L2-BRisCyqC.js → cynefin-VYW2F7L2-wDMibS5s.js} +1 -1
  31. package/dist/web/assets/{cynefinDiagram-MW4NZA55-ClIrmCkB.js → cynefinDiagram-MW4NZA55-CBTrdvOE.js} +1 -1
  32. package/dist/web/assets/{dagre-VZM6K2ZE-X31uC_JB.js → dagre-VZM6K2ZE-Bbgn4Rr7.js} +1 -1
  33. package/dist/web/assets/{diagram-7IWD3JNH-DBQFjl0i.js → diagram-7IWD3JNH-Bm9AuXb9.js} +1 -1
  34. package/dist/web/assets/{diagram-B4RE2ZJO-BZ7934Wg.js → diagram-B4RE2ZJO-D3lZ-r-H.js} +1 -1
  35. package/dist/web/assets/{diagram-LBJQPF4R-CcJqH9aY.js → diagram-LBJQPF4R-D-GaynX7.js} +1 -1
  36. package/dist/web/assets/{diagram-Q27KOJAE-DzM7eiil.js → diagram-Q27KOJAE-BeZRiu1d.js} +1 -1
  37. package/dist/web/assets/{diagram-UB23O5K3-CVCV-d3m.js → diagram-UB23O5K3-C3GP7iMe.js} +1 -1
  38. package/dist/web/assets/{ebnfDiagram-BXEA7PRR-D8xb9FBp.js → ebnfDiagram-BXEA7PRR-BJWg0NVU.js} +1 -1
  39. package/dist/web/assets/{erDiagram-JOGREHBK-1l-G_PMG.js → erDiagram-JOGREHBK-XPmhUeB_.js} +1 -1
  40. package/dist/web/assets/eventmodeling-45OFAUF4-C3c4qddw.js +1 -0
  41. package/dist/web/assets/flowDiagram-UKHOOZJN-DMA-a1zf.js +1 -0
  42. package/dist/web/assets/{ganttDiagram-PKOTCBZU-CDJ9a1tp.js → ganttDiagram-PKOTCBZU-CL-xEo-M.js} +1 -1
  43. package/dist/web/assets/{gitGraph-TEB2WS4Q-DOeTh5wB.js → gitGraph-TEB2WS4Q-BVRyuIx3.js} +1 -1
  44. package/dist/web/assets/{gitGraphDiagram-DS77QQ5N-DYLxc8Nz.js → gitGraphDiagram-DS77QQ5N-BVNIEupW.js} +1 -1
  45. package/dist/web/assets/{index-9cOaPesh.js → index-D8GJBw23.js} +98 -98
  46. package/dist/web/assets/{index-kK3hngnz.css → index-DMD2vvQW.css} +1 -1
  47. package/dist/web/assets/{info-DKCQHKI2-B5u97QuQ.js → info-DKCQHKI2-BKBUVpfc.js} +1 -1
  48. package/dist/web/assets/{infoDiagram-6WML65LV-zosKW2OD.js → infoDiagram-6WML65LV-_dTFnzSE.js} +1 -1
  49. package/dist/web/assets/{ishikawaDiagram-WSZJBQD7-VYcc_au8.js → ishikawaDiagram-WSZJBQD7-DIRgD4hO.js} +1 -1
  50. package/dist/web/assets/{journeyDiagram-NVQOT4AX-C_nU8kP4.js → journeyDiagram-NVQOT4AX-bp7nZrfR.js} +1 -1
  51. package/dist/web/assets/{kanban-definition-27J2QSJJ-DybKaWfx.js → kanban-definition-27J2QSJJ-C6mZIo8S.js} +1 -1
  52. package/dist/web/assets/{line-DWsGplGj.js → line-DWTym7eJ.js} +1 -1
  53. package/dist/web/assets/{mermaid-parser.core-D_T_3oIQ.js → mermaid-parser.core-CbecsNA1.js} +3 -3
  54. package/dist/web/assets/{mermaid.core-BoLGzxB5.js → mermaid.core-CdRuLyYI.js} +4 -4
  55. package/dist/web/assets/{mindmap-definition-FAOFIHXS-CsdpqPIa.js → mindmap-definition-FAOFIHXS-CU2P1z-9.js} +1 -1
  56. package/dist/web/assets/{packet-7NZHBO7P-CReAEF1h.js → packet-7NZHBO7P-CZsup6Od.js} +1 -1
  57. package/dist/web/assets/{pegDiagram-VL7TDLO6-CE9-NPMu.js → pegDiagram-VL7TDLO6-DTTyttpe.js} +1 -1
  58. package/dist/web/assets/{pie-RZYD4A2V-GYixXUka.js → pie-RZYD4A2V-DgrIiWZA.js} +1 -1
  59. package/dist/web/assets/{pieDiagram-7S7Q4E2Y-CqaAKNg2.js → pieDiagram-7S7Q4E2Y-BBNFyPT1.js} +1 -1
  60. package/dist/web/assets/{quadrantDiagram-CIZ2JOQS-eRMHJqfj.js → quadrantDiagram-CIZ2JOQS-ejp-Aa2l.js} +1 -1
  61. package/dist/web/assets/{radar-I7S5WNFK-BwHxP0iK.js → radar-I7S5WNFK-Cp_hfjrI.js} +1 -1
  62. package/dist/web/assets/{railroad-3IZDKUUU-AHc3O4Oz.js → railroad-3IZDKUUU-Dw-oMal2.js} +1 -1
  63. package/dist/web/assets/railroad-abnf-AHOZXSZD-BN-ZBhE0.js +1 -0
  64. package/dist/web/assets/railroad-ebnf-EBAXGLYW-CG97zyI2.js +1 -0
  65. package/dist/web/assets/railroad-peg-LSFZ7HO6-DHLQ1evQ.js +1 -0
  66. package/dist/web/assets/{railroadDiagram-AXF67PYL-BhAxCokG.js → railroadDiagram-AXF67PYL-DkBUaj6Z.js} +1 -1
  67. package/dist/web/assets/{requirementDiagram-LRYGKXZP-BRW-kYhg.js → requirementDiagram-LRYGKXZP-Dff1ex_h.js} +1 -1
  68. package/dist/web/assets/{sankeyDiagram-W5VNT64P-DfnYLbDJ.js → sankeyDiagram-W5VNT64P-DptSL9Bc.js} +1 -1
  69. package/dist/web/assets/{sequenceDiagram-SI44F4Z6-WEkIPSg8.js → sequenceDiagram-SI44F4Z6-DEnPcs3U.js} +1 -1
  70. package/dist/web/assets/{stateDiagram-OKZ733FA-CdN7veIN.js → stateDiagram-OKZ733FA-BwJO1MKA.js} +1 -1
  71. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-LaSNqvWy.js +1 -0
  72. package/dist/web/assets/{swimlanes-SLNWSIFB-g9wWLf5Y.js → swimlanes-SLNWSIFB-BosKiwZA.js} +1 -1
  73. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-C5OBtBlv.js +8 -0
  74. package/dist/web/assets/{timeline-definition-Z64GVDOM-DjkKZIPE.js → timeline-definition-Z64GVDOM-C5isAn4X.js} +1 -1
  75. package/dist/web/assets/{treeView-QDETBFTQ-WARa9r09.js → treeView-QDETBFTQ-zEyUXRc4.js} +1 -1
  76. package/dist/web/assets/{treemap-6X3UGDF4-CTFjdb6k.js → treemap-6X3UGDF4-B-X0EQSE.js} +1 -1
  77. package/dist/web/assets/{vennDiagram-T6HMQDX7-CI9QrBwu.js → vennDiagram-T6HMQDX7-QMdGieCa.js} +1 -1
  78. package/dist/web/assets/{wardley-OPB4EBWU-Cfo_Nnla.js → wardley-OPB4EBWU-BaeAF8d8.js} +1 -1
  79. package/dist/web/assets/{wardleyDiagram-T6FBY63Y-CzGJ4fzZ.js → wardleyDiagram-T6FBY63Y-DTQ_uLUq.js} +1 -1
  80. package/dist/web/assets/{xychartDiagram-ELKLHX3M-BXCiD7gJ.js → xychartDiagram-ELKLHX3M-BMdBWzib.js} +1 -1
  81. package/dist/web/index.html +2 -2
  82. package/package.json +17 -15
  83. package/skills/rvw-watch-comments/SKILL.md +71 -32
  84. package/skills/rvw-watch-comments/agents/openai.yaml +2 -2
  85. package/skills/rvw-watch-comments/scripts/auto-ack.mjs +0 -0
  86. package/skills/rvw-watch-comments/scripts/preflight.mjs +0 -0
  87. package/skills/rvw-watch-comments/scripts/watch-driver.mjs +223 -48
  88. package/skills/rvw-watch-comments/scripts/watch-state.mjs +13 -2
  89. package/dist/web/assets/DocumentViewer-ADzspLC4.js +0 -1560
  90. package/dist/web/assets/WalkthroughViewer-yQibj2tK.js +0 -1
  91. package/dist/web/assets/architecture-TIHT7OUA-DEoBuEnc.js +0 -1
  92. package/dist/web/assets/channel-DjgdV-zW.js +0 -1
  93. package/dist/web/assets/classDiagram-JCYQIIEL-B6Qjam7Y.js +0 -1
  94. package/dist/web/assets/classDiagram-v2-OCEON4UE-B6Qjam7Y.js +0 -1
  95. package/dist/web/assets/eventmodeling-45OFAUF4-ndNeUBOZ.js +0 -1
  96. package/dist/web/assets/flowDiagram-UKHOOZJN-BVxK_6Tu.js +0 -1
  97. package/dist/web/assets/railroad-abnf-AHOZXSZD-ZKY5olmV.js +0 -1
  98. package/dist/web/assets/railroad-ebnf-EBAXGLYW-CrG1UCMo.js +0 -1
  99. package/dist/web/assets/railroad-peg-LSFZ7HO6-CoY6MIbj.js +0 -1
  100. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-0xn0gbpn.js +0 -1
  101. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-YJ3yh_d3.js +0 -8
@@ -1,6 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { randomUUID } from "node:crypto";
3
4
  import { spawn } from "node:child_process";
5
+ import {
6
+ chmodSync,
7
+ linkSync,
8
+ lstatSync,
9
+ readFileSync,
10
+ realpathSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs";
4
14
  import path from "node:path";
5
15
  import { StringDecoder } from "node:string_decoder";
6
16
  import { fileURLToPath } from "node:url";
@@ -10,9 +20,13 @@ const EXIT_WATCH = 20;
10
20
  const EXIT_SEQUENCE = 21;
11
21
  const EXIT_INGEST = 22;
12
22
  const EXIT_AUTO_ACK = 23;
23
+ const EXIT_ALREADY_RUNNING = 24;
24
+ const DEFAULT_MAX_IN_FLIGHT = 1;
25
+ const DEFAULT_AUTO_ACK_POLL_MS = 250;
13
26
  const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
14
27
  const stateScript = path.join(scriptDirectory, "watch-state.mjs");
15
28
  const autoAckScript = path.join(scriptDirectory, "auto-ack.mjs");
29
+ const DRIVER_LOCK_SUFFIX = ".watch-driver.lock";
16
30
 
17
31
  class DriverError extends Error {
18
32
  constructor(message, exitCode, details = null) {
@@ -25,12 +39,27 @@ class DriverError extends Error {
25
39
  function parseArguments(values) {
26
40
  let state = null;
27
41
  let autoAck = false;
42
+ let maxInFlight = DEFAULT_MAX_IN_FLIGHT;
43
+ let maxInFlightProvided = false;
28
44
  for (let index = 0; index < values.length; index += 1) {
29
45
  const value = values[index];
30
46
  if (value === "--auto-ack") {
31
47
  autoAck = true;
32
48
  continue;
33
49
  }
50
+ if (value === "--max-in-flight") {
51
+ const next = values[index + 1];
52
+ if (!next || next.startsWith("--")) {
53
+ throw new DriverError("--max-in-flight requires an integer", EXIT_WATCH);
54
+ }
55
+ maxInFlight = Number(next);
56
+ maxInFlightProvided = true;
57
+ if (!Number.isSafeInteger(maxInFlight) || maxInFlight < 1 || maxInFlight > 64) {
58
+ throw new DriverError("--max-in-flight must be an integer from 1 through 64", EXIT_WATCH);
59
+ }
60
+ index += 1;
61
+ continue;
62
+ }
34
63
  if (value === "--state") {
35
64
  const next = values[index + 1];
36
65
  if (!next || next.startsWith("--")) {
@@ -47,7 +76,10 @@ function parseArguments(values) {
47
76
  throw new DriverError(`Unexpected argument: ${value}`, EXIT_WATCH);
48
77
  }
49
78
  if (!state) throw new DriverError("Pass the task state database path", EXIT_WATCH);
50
- return { state: path.resolve(state), autoAck };
79
+ if (!autoAck && maxInFlightProvided) {
80
+ throw new DriverError("--max-in-flight requires --auto-ack", EXIT_WATCH);
81
+ }
82
+ return { state: path.resolve(state), autoAck, maxInFlight };
51
83
  }
52
84
 
53
85
  async function runNodeScript(script, args, input) {
@@ -86,12 +118,108 @@ function write(value) {
86
118
  process.stdout.write(`${JSON.stringify(value)}\n`);
87
119
  }
88
120
 
89
- async function dispatchAutoAck(state, pullRequest) {
90
- const pending = await stateCommand(state, "list");
91
- if (!pending.pending.some((batch) => batch.pullRequest === pullRequest)) {
92
- write({ ok: true, type: "queued", pullRequest, reason: "batch-not-yet-eligible" });
93
- return;
121
+ function fileIdentity(filePath) {
122
+ try {
123
+ const stat = lstatSync(filePath);
124
+ return { dev: stat.dev, ino: stat.ino };
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ function unlinkIfOwned(filePath, identity) {
131
+ const current = fileIdentity(filePath);
132
+ if (!identity || !current || current.dev !== identity.dev || current.ino !== identity.ino) return;
133
+ try {
134
+ unlinkSync(filePath);
135
+ } catch (error) {
136
+ if (error.code !== "ENOENT") throw error;
137
+ }
138
+ }
139
+
140
+ function processIsAlive(pid) {
141
+ if (!Number.isInteger(pid) || pid <= 0) return false;
142
+ try {
143
+ process.kill(pid, 0);
144
+ return true;
145
+ } catch (error) {
146
+ return error.code !== "ESRCH";
147
+ }
148
+ }
149
+
150
+ function lockOwner(lockPath) {
151
+ try {
152
+ const value = JSON.parse(readFileSync(lockPath, "utf8"));
153
+ return Number.isInteger(value?.pid) && value.pid > 0 ? value : null;
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ function acquireDriverLock(state) {
160
+ const canonicalState = realpathSync(state);
161
+ const lockPath = `${canonicalState}${DRIVER_LOCK_SUFFIX}`;
162
+ for (let attempt = 0; attempt < 2; attempt += 1) {
163
+ const stagingPath = `${lockPath}.${process.pid}.${randomUUID()}.tmp`;
164
+ try {
165
+ writeFileSync(
166
+ stagingPath,
167
+ `${JSON.stringify({ pid: process.pid, state: canonicalState, startedAt: new Date().toISOString() })}\n`,
168
+ { flag: "wx", mode: 0o600 },
169
+ );
170
+ chmodSync(stagingPath, 0o600);
171
+ linkSync(stagingPath, lockPath);
172
+ } catch (error) {
173
+ try {
174
+ unlinkSync(stagingPath);
175
+ } catch (unlinkError) {
176
+ if (unlinkError.code !== "ENOENT") throw unlinkError;
177
+ }
178
+ if (error.code !== "EEXIST") throw error;
179
+ const observedIdentity = fileIdentity(lockPath);
180
+ const owner = lockOwner(lockPath);
181
+ // Preserve unreadable locks rather than risking two live drivers.
182
+ if (!owner || processIsAlive(owner.pid)) {
183
+ throw new DriverError(
184
+ "Another watch-driver process already owns this task state",
185
+ EXIT_ALREADY_RUNNING,
186
+ {
187
+ state: canonicalState,
188
+ lockPath,
189
+ ownerPid: owner?.pid ?? null,
190
+ },
191
+ );
192
+ }
193
+ unlinkIfOwned(lockPath, observedIdentity);
194
+ continue;
195
+ }
196
+ try {
197
+ const identity = fileIdentity(lockPath);
198
+ if (!identity) throw new Error("Could not verify the watch-driver owner lock");
199
+ unlinkSync(stagingPath);
200
+ return { identity, path: lockPath };
201
+ } catch (error) {
202
+ const stagingIdentity = fileIdentity(stagingPath);
203
+ unlinkIfOwned(lockPath, stagingIdentity);
204
+ try {
205
+ unlinkSync(stagingPath);
206
+ } catch (unlinkError) {
207
+ if (unlinkError.code !== "ENOENT") throw unlinkError;
208
+ }
209
+ throw error;
210
+ }
94
211
  }
212
+ throw new DriverError("Could not acquire the watch-driver owner lock", EXIT_ALREADY_RUNNING, {
213
+ state: canonicalState,
214
+ lockPath,
215
+ });
216
+ }
217
+
218
+ function releaseDriverLock(lock) {
219
+ unlinkIfOwned(lock.path, lock.identity);
220
+ }
221
+
222
+ async function dispatchAutoAck(state, pullRequest) {
95
223
  const result = await runNodeScript(autoAckScript, [
96
224
  "--state",
97
225
  state,
@@ -104,14 +232,21 @@ async function dispatchAutoAck(state, pullRequest) {
104
232
  write({ ...result.json, type: "batch-acknowledged" });
105
233
  }
106
234
 
107
- async function dispatchPendingAutoAcks(state) {
108
- const pending = await stateCommand(state, "list");
109
- for (const batch of pending.pending) {
235
+ async function dispatchPendingAutoAcks(state, maxInFlight) {
236
+ const work = await stateCommand(state, "list");
237
+ const inFlight = Number(work.inFlight);
238
+ if (!Number.isSafeInteger(inFlight) || inFlight < 0 || !Array.isArray(work.pending)) {
239
+ throw new DriverError("watch-state list returned invalid work capacity", EXIT_INGEST, work);
240
+ }
241
+ let available = Math.max(0, maxInFlight - inFlight);
242
+ for (const batch of work.pending) {
243
+ if (available === 0) break;
110
244
  await dispatchAutoAck(state, batch.pullRequest);
245
+ available -= 1;
111
246
  }
112
247
  }
113
248
 
114
- async function processFrame(state, frame, autoAck, pullRequests) {
249
+ async function processFrame(state, frame, autoAck, maxInFlight) {
115
250
  if (!frame || typeof frame !== "object" || typeof frame.type !== "string") {
116
251
  throw new DriverError("Invalid RFC 7464 frame", EXIT_SEQUENCE, frame);
117
252
  }
@@ -127,10 +262,10 @@ async function processFrame(state, frame, autoAck, pullRequests) {
127
262
  databaseId: frame.databaseId,
128
263
  anchoredAtCurrent: frame.anchoredAtCurrent,
129
264
  autoAck,
265
+ maxInFlight: autoAck ? maxInFlight : null,
130
266
  });
131
267
  } else if (frame.type === "comment-posted" && ingested.status === "queued") {
132
- if (autoAck) pullRequests.add(frame.event.pullRequestUrl);
133
- else {
268
+ if (!autoAck) {
134
269
  write({
135
270
  ok: true,
136
271
  type: "pending",
@@ -142,7 +277,16 @@ async function processFrame(state, frame, autoAck, pullRequests) {
142
277
  }
143
278
  }
144
279
 
145
- async function runWatchOnce(state, autoAck, stopping) {
280
+ function autoAckPollMilliseconds() {
281
+ const configured = Number(
282
+ process.env.RVW_WATCH_AUTO_ACK_POLL_MS ?? String(DEFAULT_AUTO_ACK_POLL_MS),
283
+ );
284
+ return Number.isSafeInteger(configured) && configured >= 10 && configured <= 60_000
285
+ ? configured
286
+ : DEFAULT_AUTO_ACK_POLL_MS;
287
+ }
288
+
289
+ async function runWatchOnce(state, autoAck, maxInFlight, stopping) {
146
290
  const current = await stateCommand(state, "status");
147
291
  const args = ["comment", "watch"];
148
292
  if (current.cursor) args.push("--after", current.cursor);
@@ -160,6 +304,25 @@ async function runWatchOnce(state, autoAck, stopping) {
160
304
  child.once("close", (code, signal) => resolve({ code, signal }));
161
305
  });
162
306
  child.stdin.end();
307
+ let autoAckPump = null;
308
+ let autoAckError = null;
309
+ const pumpAutoAcks = async () => {
310
+ if (!autoAck || stopping.requested) return;
311
+ if (autoAckPump) return autoAckPump;
312
+ autoAckPump = dispatchPendingAutoAcks(state, maxInFlight);
313
+ try {
314
+ await autoAckPump;
315
+ } catch (error) {
316
+ autoAckError ??= error;
317
+ child.kill("SIGTERM");
318
+ throw error;
319
+ } finally {
320
+ autoAckPump = null;
321
+ }
322
+ };
323
+ const autoAckTimer = autoAck
324
+ ? setInterval(() => void pumpAutoAcks().catch(() => undefined), autoAckPollMilliseconds())
325
+ : null;
163
326
  const decoder = new StringDecoder("utf8");
164
327
  let buffered = "";
165
328
  let readySeen = false;
@@ -169,7 +332,6 @@ async function runWatchOnce(state, autoAck, stopping) {
169
332
  buffered += decoder.write(chunk);
170
333
  const lines = buffered.split("\n");
171
334
  buffered = lines.pop() ?? "";
172
- const pullRequests = new Set();
173
335
  for (const rawLine of lines) {
174
336
  if (!rawLine) continue;
175
337
  if (!rawLine.startsWith("\u001e")) {
@@ -183,9 +345,9 @@ async function runWatchOnce(state, autoAck, stopping) {
183
345
  }
184
346
  readySeen ||= frame.type === "ready";
185
347
  stoppedSeen ||= frame.type === "stopped";
186
- await processFrame(state, frame, autoAck, pullRequests);
348
+ await processFrame(state, frame, autoAck, maxInFlight);
187
349
  }
188
- for (const pullRequest of pullRequests) await dispatchAutoAck(state, pullRequest);
350
+ await pumpAutoAcks();
189
351
  }
190
352
  buffered += decoder.end();
191
353
  if (buffered.trim()) {
@@ -196,6 +358,8 @@ async function runWatchOnce(state, autoAck, stopping) {
196
358
  );
197
359
  }
198
360
  const status = await statusPromise;
361
+ if (autoAckPump) await autoAckPump.catch(() => undefined);
362
+ if (autoAckError) throw autoAckError;
199
363
  stopping.child = null;
200
364
  return { ...status, stderr, readySeen, stoppedSeen };
201
365
  } catch (error) {
@@ -203,6 +367,8 @@ async function runWatchOnce(state, autoAck, stopping) {
203
367
  await statusPromise.catch(() => undefined);
204
368
  stopping.child = null;
205
369
  throw error;
370
+ } finally {
371
+ if (autoAckTimer) clearInterval(autoAckTimer);
206
372
  }
207
373
  }
208
374
 
@@ -217,42 +383,51 @@ function delay(milliseconds) {
217
383
  }
218
384
 
219
385
  async function main() {
220
- const { state, autoAck } = parseArguments(process.argv.slice(2));
221
- const stopping = { requested: false, child: null };
222
- const stop = () => {
223
- stopping.requested = true;
224
- stopping.child?.kill("SIGTERM");
225
- };
226
- process.once("SIGINT", stop);
227
- process.once("SIGTERM", stop);
228
- const maxRestarts = Number(process.env.RVW_WATCH_DRIVER_MAX_RESTARTS ?? "5");
229
- let restarts = 0;
386
+ const { state, autoAck, maxInFlight } = parseArguments(process.argv.slice(2));
387
+ const driverLock = acquireDriverLock(state);
230
388
  try {
231
- while (!stopping.requested) {
232
- const startedAt = Date.now();
233
- if (autoAck) await dispatchPendingAutoAcks(state);
234
- const result = await runWatchOnce(state, autoAck, stopping);
235
- if (stopping.requested) return;
236
- if (Date.now() - startedAt >= 30_000) restarts = 0;
237
- restarts += 1;
238
- if (restarts > maxRestarts) {
239
- throw new DriverError("rvw comment watch exceeded its reconnect limit", EXIT_WATCH, result);
389
+ const stopping = { requested: false, child: null };
390
+ const stop = () => {
391
+ stopping.requested = true;
392
+ stopping.child?.kill("SIGTERM");
393
+ };
394
+ process.once("SIGINT", stop);
395
+ process.once("SIGTERM", stop);
396
+ const maxRestarts = Number(process.env.RVW_WATCH_DRIVER_MAX_RESTARTS ?? "5");
397
+ let restarts = 0;
398
+ try {
399
+ while (!stopping.requested) {
400
+ const startedAt = Date.now();
401
+ if (autoAck) await dispatchPendingAutoAcks(state, maxInFlight);
402
+ const result = await runWatchOnce(state, autoAck, maxInFlight, stopping);
403
+ if (stopping.requested) return;
404
+ if (Date.now() - startedAt >= 30_000) restarts = 0;
405
+ restarts += 1;
406
+ if (restarts > maxRestarts) {
407
+ throw new DriverError(
408
+ "rvw comment watch exceeded its reconnect limit",
409
+ EXIT_WATCH,
410
+ result,
411
+ );
412
+ }
413
+ const delayMs = retryDelay(restarts);
414
+ write({
415
+ ok: true,
416
+ type: "reconnecting",
417
+ attempt: restarts,
418
+ delayMs,
419
+ lastExitCode: result.code,
420
+ lastSignal: result.signal,
421
+ readySeen: result.readySeen,
422
+ });
423
+ await delay(delayMs);
240
424
  }
241
- const delayMs = retryDelay(restarts);
242
- write({
243
- ok: true,
244
- type: "reconnecting",
245
- attempt: restarts,
246
- delayMs,
247
- lastExitCode: result.code,
248
- lastSignal: result.signal,
249
- readySeen: result.readySeen,
250
- });
251
- await delay(delayMs);
425
+ } finally {
426
+ process.off("SIGINT", stop);
427
+ process.off("SIGTERM", stop);
252
428
  }
253
429
  } finally {
254
- process.off("SIGINT", stop);
255
- process.off("SIGTERM", stop);
430
+ releaseDriverLock(driverLock);
256
431
  }
257
432
  }
258
433
 
@@ -68,7 +68,8 @@ function openState(statePath, create) {
68
68
  fail(`State database does not exist: ${absolute}`);
69
69
  }
70
70
  const database = new DatabaseSync(absolute);
71
- database.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;");
71
+ database.exec("PRAGMA busy_timeout = 5000;");
72
+ database.exec("PRAGMA journal_mode = WAL;");
72
73
  database.exec(`
73
74
  CREATE TABLE IF NOT EXISTS meta (
74
75
  key TEXT PRIMARY KEY,
@@ -455,6 +456,16 @@ function listPending(database) {
455
456
  });
456
457
  }
457
458
 
459
+ function listWork(database) {
460
+ const inFlight = database
461
+ .prepare("SELECT count(*) AS count FROM batches WHERE status = 'in_flight'")
462
+ .get();
463
+ return {
464
+ inFlight: Number(inFlight.count),
465
+ pending: listPending(database),
466
+ };
467
+ }
468
+
458
469
  async function waitForPending(database, options) {
459
470
  const intervalMs = positiveIntegerOption(options, "interval-ms", 250);
460
471
  const follow = options.follow === true;
@@ -822,7 +833,7 @@ async function main() {
822
833
  return;
823
834
  }
824
835
  if (command === "list") {
825
- write({ ok: true, pending: listPending(database) });
836
+ write({ ok: true, ...listWork(database) });
826
837
  return;
827
838
  }
828
839
  if (command === "wait") {