instar 1.3.646 → 1.3.648

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 (87) hide show
  1. package/dist/commands/reflect.d.ts.map +1 -1
  2. package/dist/commands/reflect.js +7 -3
  3. package/dist/commands/reflect.js.map +1 -1
  4. package/dist/commands/server.d.ts.map +1 -1
  5. package/dist/commands/server.js +41 -2
  6. package/dist/commands/server.js.map +1 -1
  7. package/dist/commands/setup.d.ts.map +1 -1
  8. package/dist/commands/setup.js +17 -0
  9. package/dist/commands/setup.js.map +1 -1
  10. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  11. package/dist/config/ConfigDefaults.js +14 -0
  12. package/dist/config/ConfigDefaults.js.map +1 -1
  13. package/dist/core/CoherenceGate.d.ts.map +1 -1
  14. package/dist/core/CoherenceGate.js +24 -0
  15. package/dist/core/CoherenceGate.js.map +1 -1
  16. package/dist/core/CoherenceReviewer.d.ts +9 -0
  17. package/dist/core/CoherenceReviewer.d.ts.map +1 -1
  18. package/dist/core/CoherenceReviewer.js +18 -2
  19. package/dist/core/CoherenceReviewer.js.map +1 -1
  20. package/dist/core/InputGuard.d.ts.map +1 -1
  21. package/dist/core/InputGuard.js +17 -3
  22. package/dist/core/InputGuard.js.map +1 -1
  23. package/dist/core/MessageSentinel.d.ts.map +1 -1
  24. package/dist/core/MessageSentinel.js +19 -2
  25. package/dist/core/MessageSentinel.js.map +1 -1
  26. package/dist/core/MessagingToneGate.d.ts +8 -0
  27. package/dist/core/MessagingToneGate.d.ts.map +1 -1
  28. package/dist/core/MessagingToneGate.js +16 -1
  29. package/dist/core/MessagingToneGate.js.map +1 -1
  30. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  31. package/dist/core/PostUpdateMigrator.js +10 -0
  32. package/dist/core/PostUpdateMigrator.js.map +1 -1
  33. package/dist/core/SingleInstanceLock.d.ts +103 -0
  34. package/dist/core/SingleInstanceLock.d.ts.map +1 -0
  35. package/dist/core/SingleInstanceLock.js +300 -0
  36. package/dist/core/SingleInstanceLock.js.map +1 -0
  37. package/dist/core/SpawnCapIntelligenceProvider.d.ts +89 -0
  38. package/dist/core/SpawnCapIntelligenceProvider.d.ts.map +1 -0
  39. package/dist/core/SpawnCapIntelligenceProvider.js +164 -0
  40. package/dist/core/SpawnCapIntelligenceProvider.js.map +1 -0
  41. package/dist/core/hostSpawnSemaphore.d.ts +188 -0
  42. package/dist/core/hostSpawnSemaphore.d.ts.map +1 -0
  43. package/dist/core/hostSpawnSemaphore.js +471 -0
  44. package/dist/core/hostSpawnSemaphore.js.map +1 -0
  45. package/dist/core/intelligenceProviderFactory.d.ts +0 -12
  46. package/dist/core/intelligenceProviderFactory.d.ts.map +1 -1
  47. package/dist/core/intelligenceProviderFactory.js +27 -9
  48. package/dist/core/intelligenceProviderFactory.js.map +1 -1
  49. package/dist/core/types.d.ts +18 -0
  50. package/dist/core/types.d.ts.map +1 -1
  51. package/dist/core/types.js.map +1 -1
  52. package/dist/feedback-factory/store/JsonlFeedbackStore.d.ts +2 -0
  53. package/dist/feedback-factory/store/JsonlFeedbackStore.d.ts.map +1 -1
  54. package/dist/feedback-factory/store/JsonlFeedbackStore.js +36 -2
  55. package/dist/feedback-factory/store/JsonlFeedbackStore.js.map +1 -1
  56. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  57. package/dist/messaging/TelegramAdapter.js +6 -2
  58. package/dist/messaging/TelegramAdapter.js.map +1 -1
  59. package/dist/monitoring/CoherenceMonitor.d.ts.map +1 -1
  60. package/dist/monitoring/CoherenceMonitor.js +6 -1
  61. package/dist/monitoring/CoherenceMonitor.js.map +1 -1
  62. package/dist/monitoring/CommitmentSentinel.d.ts.map +1 -1
  63. package/dist/monitoring/CommitmentSentinel.js +7 -2
  64. package/dist/monitoring/CommitmentSentinel.js.map +1 -1
  65. package/dist/scaffold/templates.d.ts.map +1 -1
  66. package/dist/scaffold/templates.js +5 -0
  67. package/dist/scaffold/templates.js.map +1 -1
  68. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  69. package/dist/server/CapabilityIndex.js +9 -0
  70. package/dist/server/CapabilityIndex.js.map +1 -1
  71. package/dist/server/routes.d.ts.map +1 -1
  72. package/dist/server/routes.js +28 -0
  73. package/dist/server/routes.js.map +1 -1
  74. package/dist/utils/jsonl-tail.d.ts +50 -0
  75. package/dist/utils/jsonl-tail.d.ts.map +1 -0
  76. package/dist/utils/jsonl-tail.js +96 -0
  77. package/dist/utils/jsonl-tail.js.map +1 -0
  78. package/package.json +4 -2
  79. package/scripts/lint-no-direct-destructive.js +4 -0
  80. package/scripts/lint-no-unbounded-llm-spawn.js +137 -0
  81. package/src/data/builtin-manifest.json +63 -63
  82. package/src/scaffold/templates.ts +5 -0
  83. package/upgrades/1.3.647.md +56 -0
  84. package/upgrades/1.3.648.md +99 -0
  85. package/upgrades/eli16/eventloop-bounded-jsonl-reads.md +47 -0
  86. package/upgrades/side-effects/eventloop-bounded-jsonl-reads.md +96 -0
  87. package/upgrades/side-effects/forkbomb-prevention-simple.md +138 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * SingleInstanceLock — P2 of the SIMPLE fork-bomb prevention design.
3
+ *
4
+ * Spec: docs/specs/forkbomb-prevention-simple.md (§P2, §D-LOCK).
5
+ *
6
+ * A per-agent, HOST-LOCAL lock so launchd + fleet + tmux cannot run DUPLICATE
7
+ * server instances of the same agent — the 3× multiplier that made the
8
+ * 2026-06-20 fork-bomb catastrophic (three concurrent server instances each
9
+ * re-flooding `claude -p`).
10
+ *
11
+ * MECHANISM (mirrors ProjectRoundLock O_CREAT|O_EXCL + the ResumeQueue host-lock
12
+ * contract):
13
+ * - Lock file `<stateDir>/local/server-instance.lock`, holder record =
14
+ * `{ pid, hostname, heartbeat }` (ms-epoch mtime-style heartbeat).
15
+ * - A FOREIGN-hostname lock is NEVER pid-probed or reclaimed (refuse-loud —
16
+ * the multi-machine shared-state-dir hazard, 2026-06-15): a legit standby on
17
+ * a DIFFERENT host with its own non-shared state dir boots freely.
18
+ * - Same-host stale reclaim is gated on a `df -P` host-local-disk confirmation
19
+ * (fail-closed: cannot-confirm-local → never reclaim).
20
+ * - DEPLOY HANDOFF (not too blunt): on finding a LIVE same-host holder, WAIT a
21
+ * bounded grace for it to release (a normal restart kills-then-respawns, so
22
+ * the outgoing holder's exit handler frees the lock) before refusing — so a
23
+ * clean restart hands off and only a genuine DUPLICATE (two independent
24
+ * supervisors racing) is refused.
25
+ * - Override: `INSTAR_ALLOW_SECOND_INSTANCE=1` boots a deliberate admin/debug
26
+ * instance without the lock.
27
+ *
28
+ * Release is wired by the CALLER via BOTH a `finally`/shutdown path AND a
29
+ * process exit handler (SIGTERM/SIGINT/exit) — see `installReleaseHandlers`.
30
+ */
31
+ interface InstanceLockRecord {
32
+ pid: number;
33
+ hostname: string;
34
+ heartbeat: number;
35
+ }
36
+ export interface SingleInstanceLockResult {
37
+ acquired: boolean;
38
+ /** Why not (when acquired=false). */
39
+ reason?: 'duplicate-live-instance' | 'foreign-host-conflict' | 'override-bypassed';
40
+ /** The conflicting holder (when refused on a same/foreign-host live lock). */
41
+ currentHolder?: InstanceLockRecord;
42
+ /** True when bypassed via INSTAR_ALLOW_SECOND_INSTANCE (acquired stays true). */
43
+ overridden?: boolean;
44
+ }
45
+ export interface SingleInstanceLockDeps {
46
+ /** `.instar` state dir — the lock lives under `<stateDir>/local/`. */
47
+ stateDir: string;
48
+ now?: () => number;
49
+ hostname?: () => string;
50
+ pidAlive?: (pid: number) => boolean;
51
+ /** Host-local FS probe (tests override). */
52
+ isStateDirHostLocal?: (stateDir: string) => boolean;
53
+ /** Deploy-handoff grace (ms) to wait for an outgoing same-host holder. Default 8000. */
54
+ handoffGraceMs?: number;
55
+ /** Poll interval (ms) while waiting for handoff. Default 250. */
56
+ pollIntervalMs?: number;
57
+ /** Awaitable delay (tests override). */
58
+ sleep?: (ms: number) => Promise<void>;
59
+ /** Override env read (tests inject). Default `process.env`. */
60
+ env?: NodeJS.ProcessEnv;
61
+ /** Logger (default console.error). */
62
+ log?: (msg: string) => void;
63
+ }
64
+ /** `df -P` host-local classifier (FAIL-CLOSED) — same contract as ResumeQueue/semaphore. */
65
+ export declare function isStateDirHostLocalForLock(stateDir: string): boolean;
66
+ export declare class SingleInstanceLock {
67
+ private readonly stateDir;
68
+ private readonly now;
69
+ private readonly host;
70
+ private readonly pidAlive;
71
+ private readonly isStateDirHostLocal;
72
+ private readonly handoffGraceMs;
73
+ private readonly pollIntervalMs;
74
+ private readonly sleep;
75
+ private readonly env;
76
+ private readonly log;
77
+ private held;
78
+ constructor(deps: SingleInstanceLockDeps);
79
+ lockPath(): string;
80
+ /**
81
+ * Try to acquire the single-instance lock. Resolves with `acquired: false`
82
+ * (and a reason) on a genuine duplicate; `acquired: true` on success or a
83
+ * deliberate override.
84
+ */
85
+ acquire(): Promise<SingleInstanceLockResult>;
86
+ /** Refresh the heartbeat (optional — the lock is primarily pid-guarded). */
87
+ heartbeat(): void;
88
+ /** Release the lock — ONLY if WE still hold it (pid+host match). Idempotent. */
89
+ release(): void;
90
+ private readLock;
91
+ /** Atomic O_CREAT|O_EXCL write. Returns false if another writer won the race. */
92
+ private tryWriteLock;
93
+ private ensureDir;
94
+ }
95
+ /**
96
+ * Wire release on every exit path (SIGTERM/SIGINT/exit). The caller's graceful
97
+ * shutdown should ALSO call `lock.release()` directly (belt-and-suspenders); the
98
+ * `exit` handler is the last-resort net for a hard exit. Returns a disposer that
99
+ * removes the handlers (rarely needed; useful in tests).
100
+ */
101
+ export declare function installReleaseHandlers(lock: SingleInstanceLock): () => void;
102
+ export {};
103
+ //# sourceMappingURL=SingleInstanceLock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SingleInstanceLock.d.ts","sourceRoot":"","sources":["../../src/core/SingleInstanceLock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAUH,UAAU,kBAAkB;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,MAAM,CAAC,EAAE,yBAAyB,GAAG,uBAAuB,GAAG,mBAAmB,CAAC;IACnF,8EAA8E;IAC9E,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,iFAAiF;IACjF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACpC,4CAA4C;IAC5C,mBAAmB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IACpD,wFAAwF;IACxF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wCAAwC;IACxC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,sCAAsC;IACtC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,4FAA4F;AAC5F,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAmBpE;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2B;IACpD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAgC;IACpE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAwB;IAC5C,OAAO,CAAC,IAAI,CAAS;gBAET,IAAI,EAAE,sBAAsB;IAaxC,QAAQ,IAAI,MAAM;IAIlB;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,wBAAwB,CAAC;IAoFlD,4EAA4E;IAC5E,SAAS,IAAI,IAAI;IAajB,gFAAgF;IAChF,OAAO,IAAI,IAAI;IAkBf,OAAO,CAAC,QAAQ;IAehB,iFAAiF;IACjF,OAAO,CAAC,YAAY;IAmCpB,OAAO,CAAC,SAAS;CAIlB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAiB3E"}
@@ -0,0 +1,300 @@
1
+ /**
2
+ * SingleInstanceLock — P2 of the SIMPLE fork-bomb prevention design.
3
+ *
4
+ * Spec: docs/specs/forkbomb-prevention-simple.md (§P2, §D-LOCK).
5
+ *
6
+ * A per-agent, HOST-LOCAL lock so launchd + fleet + tmux cannot run DUPLICATE
7
+ * server instances of the same agent — the 3× multiplier that made the
8
+ * 2026-06-20 fork-bomb catastrophic (three concurrent server instances each
9
+ * re-flooding `claude -p`).
10
+ *
11
+ * MECHANISM (mirrors ProjectRoundLock O_CREAT|O_EXCL + the ResumeQueue host-lock
12
+ * contract):
13
+ * - Lock file `<stateDir>/local/server-instance.lock`, holder record =
14
+ * `{ pid, hostname, heartbeat }` (ms-epoch mtime-style heartbeat).
15
+ * - A FOREIGN-hostname lock is NEVER pid-probed or reclaimed (refuse-loud —
16
+ * the multi-machine shared-state-dir hazard, 2026-06-15): a legit standby on
17
+ * a DIFFERENT host with its own non-shared state dir boots freely.
18
+ * - Same-host stale reclaim is gated on a `df -P` host-local-disk confirmation
19
+ * (fail-closed: cannot-confirm-local → never reclaim).
20
+ * - DEPLOY HANDOFF (not too blunt): on finding a LIVE same-host holder, WAIT a
21
+ * bounded grace for it to release (a normal restart kills-then-respawns, so
22
+ * the outgoing holder's exit handler frees the lock) before refusing — so a
23
+ * clean restart hands off and only a genuine DUPLICATE (two independent
24
+ * supervisors racing) is refused.
25
+ * - Override: `INSTAR_ALLOW_SECOND_INSTANCE=1` boots a deliberate admin/debug
26
+ * instance without the lock.
27
+ *
28
+ * Release is wired by the CALLER via BOTH a `finally`/shutdown path AND a
29
+ * process exit handler (SIGTERM/SIGINT/exit) — see `installReleaseHandlers`.
30
+ */
31
+ import fs from 'node:fs';
32
+ import os from 'node:os';
33
+ import path from 'node:path';
34
+ import { execFileSync } from 'node:child_process';
35
+ import { SafeFsExecutor } from './SafeFsExecutor.js';
36
+ import { isAlive } from './ProjectRoundLock.js';
37
+ /** `df -P` host-local classifier (FAIL-CLOSED) — same contract as ResumeQueue/semaphore. */
38
+ export function isStateDirHostLocalForLock(stateDir) {
39
+ let out;
40
+ try {
41
+ // lint-allow-sync-spawn: a bounded (3s) one-shot host-FS classification, run
42
+ // only at server-BOOT during single-instance-lock acquisition (never on a
43
+ // request/event-loop path). Mirrors ResumeQueue's host-lock df -P contract.
44
+ out = execFileSync('df', ['-P', stateDir], { timeout: 3000, encoding: 'utf-8' });
45
+ }
46
+ catch {
47
+ // @silent-fallback-ok: df failed ⇒ cannot confirm local ⇒ fail-closed.
48
+ return false;
49
+ }
50
+ const lines = out.trim().split('\n');
51
+ if (lines.length < 2)
52
+ return false;
53
+ const source = lines[1]?.trim().split(/\s+/)[0] ?? '';
54
+ if (!source)
55
+ return false;
56
+ if (source.startsWith('//'))
57
+ return false;
58
+ if (/^[^/][^:]*:/.test(source))
59
+ return false;
60
+ if (source.startsWith('/dev/'))
61
+ return true;
62
+ return false;
63
+ }
64
+ export class SingleInstanceLock {
65
+ stateDir;
66
+ now;
67
+ host;
68
+ pidAlive;
69
+ isStateDirHostLocal;
70
+ handoffGraceMs;
71
+ pollIntervalMs;
72
+ sleep;
73
+ env;
74
+ log;
75
+ held = false;
76
+ constructor(deps) {
77
+ this.stateDir = deps.stateDir;
78
+ this.now = deps.now ?? (() => Date.now());
79
+ this.host = (deps.hostname ?? (() => os.hostname()))();
80
+ this.pidAlive = deps.pidAlive ?? isAlive;
81
+ this.isStateDirHostLocal = deps.isStateDirHostLocal ?? isStateDirHostLocalForLock;
82
+ this.handoffGraceMs = deps.handoffGraceMs ?? 8000;
83
+ this.pollIntervalMs = deps.pollIntervalMs ?? 250;
84
+ this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
85
+ this.env = deps.env ?? process.env;
86
+ this.log = deps.log ?? ((m) => console.error(m));
87
+ }
88
+ lockPath() {
89
+ return path.join(this.stateDir, 'local', 'server-instance.lock');
90
+ }
91
+ /**
92
+ * Try to acquire the single-instance lock. Resolves with `acquired: false`
93
+ * (and a reason) on a genuine duplicate; `acquired: true` on success or a
94
+ * deliberate override.
95
+ */
96
+ async acquire() {
97
+ // Operator override — a deliberate second/admin instance.
98
+ if (this.env['INSTAR_ALLOW_SECOND_INSTANCE'] === '1') {
99
+ this.log('[single-instance] INSTAR_ALLOW_SECOND_INSTANCE=1 — bypassing the single-instance lock (deliberate second instance).');
100
+ // Still write our record so a LATER third instance sees a holder.
101
+ this.tryWriteLock();
102
+ this.held = true;
103
+ return { acquired: true, overridden: true };
104
+ }
105
+ const deadline = this.now() + this.handoffGraceMs;
106
+ for (;;) {
107
+ const existing = this.readLock();
108
+ if (!existing) {
109
+ // Free — take it.
110
+ if (this.tryWriteLock()) {
111
+ this.held = true;
112
+ return { acquired: true };
113
+ }
114
+ // Lost a creation race — loop and re-read.
115
+ if (this.now() >= deadline) {
116
+ return { acquired: false, reason: 'duplicate-live-instance' };
117
+ }
118
+ await this.sleep(this.pollIntervalMs);
119
+ continue;
120
+ }
121
+ // FOREIGN-host lock → NEVER reclaim (refuse-loud). A standby on another
122
+ // host with its own non-shared state dir boots freely; this branch only
123
+ // fires when two hosts SHARE a state dir (a misconfiguration we refuse
124
+ // rather than corrupt).
125
+ if (existing.hostname && existing.hostname !== this.host) {
126
+ this.log(`[single-instance] lock held by FOREIGN host "${existing.hostname}" (this host "${this.host}"). ` +
127
+ `Refusing — the state dir must be host-local; a shared volume is unsupported. ` +
128
+ `If this is a deliberate second instance, set INSTAR_ALLOW_SECOND_INSTANCE=1.`);
129
+ return { acquired: false, reason: 'foreign-host-conflict', currentHolder: existing };
130
+ }
131
+ // Same-host lock. Is the holder still alive?
132
+ if (this.pidAlive(existing.pid)) {
133
+ // A LIVE same-host holder. Could be a normal restart's outgoing instance
134
+ // (it will exit + release shortly) OR a genuine duplicate. Wait a bounded
135
+ // grace for a handoff before refusing.
136
+ if (this.now() >= deadline) {
137
+ this.log(`[single-instance] refusing to start: a LIVE instance (pid ${existing.pid}) of this agent is already ` +
138
+ `running on this host after a ${this.handoffGraceMs}ms handoff grace. This is the duplicate-flood guard. ` +
139
+ `If this is intentional, set INSTAR_ALLOW_SECOND_INSTANCE=1.`);
140
+ return { acquired: false, reason: 'duplicate-live-instance', currentHolder: existing };
141
+ }
142
+ await this.sleep(this.pollIntervalMs);
143
+ continue;
144
+ }
145
+ // Same-host DEAD holder — reclaim ONLY on a host-local-disk confirmation.
146
+ if (!this.isStateDirHostLocal(this.stateDir)) {
147
+ this.log(`[single-instance] same-host lock holder (pid ${existing.pid}) is dead, but the state dir could not be ` +
148
+ `confirmed host-local (df -P) — refusing to reclaim (fail-closed). Set INSTAR_ALLOW_SECOND_INSTANCE=1 to override.`);
149
+ return { acquired: false, reason: 'foreign-host-conflict', currentHolder: existing };
150
+ }
151
+ // Reclaim the stale lock atomically and take it.
152
+ try {
153
+ SafeFsExecutor.safeUnlinkSync(this.lockPath(), { operation: 'SingleInstanceLock.acquire:reclaim-stale' });
154
+ }
155
+ catch {
156
+ /* race with another reclaimer — loop and re-read */
157
+ }
158
+ if (this.tryWriteLock()) {
159
+ this.held = true;
160
+ this.log(`[single-instance] reclaimed a stale same-host lock (dead pid ${existing.pid}).`);
161
+ return { acquired: true };
162
+ }
163
+ if (this.now() >= deadline) {
164
+ return { acquired: false, reason: 'duplicate-live-instance', currentHolder: existing };
165
+ }
166
+ await this.sleep(this.pollIntervalMs);
167
+ }
168
+ }
169
+ /** Refresh the heartbeat (optional — the lock is primarily pid-guarded). */
170
+ heartbeat() {
171
+ if (!this.held)
172
+ return;
173
+ const rec = this.readLock();
174
+ if (rec && rec.pid === process.pid && rec.hostname === this.host) {
175
+ rec.heartbeat = this.now();
176
+ try {
177
+ fs.writeFileSync(this.lockPath(), JSON.stringify(rec), 'utf-8');
178
+ }
179
+ catch {
180
+ /* best-effort */
181
+ }
182
+ }
183
+ }
184
+ /** Release the lock — ONLY if WE still hold it (pid+host match). Idempotent. */
185
+ release() {
186
+ if (!this.held)
187
+ return;
188
+ this.held = false;
189
+ const rec = this.readLock();
190
+ if (!rec)
191
+ return;
192
+ if (rec.pid !== process.pid || rec.hostname !== this.host) {
193
+ // A successor already took it — never delete someone else's lock.
194
+ return;
195
+ }
196
+ try {
197
+ SafeFsExecutor.safeUnlinkSync(this.lockPath(), { operation: 'SingleInstanceLock.release' });
198
+ }
199
+ catch {
200
+ /* @silent-fallback-ok: the lock is already gone — release is idempotent */
201
+ }
202
+ }
203
+ // ── Internals ─────────────────────────────────────────────────────
204
+ readLock() {
205
+ try {
206
+ const raw = fs.readFileSync(this.lockPath(), 'utf-8');
207
+ const obj = JSON.parse(raw);
208
+ if (typeof obj?.pid !== 'number')
209
+ return null;
210
+ if (typeof obj?.hostname !== 'string')
211
+ return null;
212
+ const heartbeat = typeof obj?.heartbeat === 'number' ? obj.heartbeat : 0;
213
+ return { pid: obj.pid, hostname: obj.hostname, heartbeat };
214
+ }
215
+ catch {
216
+ // @silent-fallback-ok: a missing/corrupt lock file reads as "no holder"
217
+ // (null) — the acquire loop then treats the slot as free, the safe direction.
218
+ return null;
219
+ }
220
+ }
221
+ /** Atomic O_CREAT|O_EXCL write. Returns false if another writer won the race. */
222
+ tryWriteLock() {
223
+ this.ensureDir();
224
+ const rec = { pid: process.pid, hostname: this.host, heartbeat: this.now() };
225
+ const body = JSON.stringify(rec);
226
+ const tmp = `${this.lockPath()}.tmp.${process.pid}`;
227
+ try {
228
+ const fd = fs.openSync(tmp, 'wx', 0o600);
229
+ fs.writeSync(fd, body);
230
+ fs.closeSync(fd);
231
+ }
232
+ catch {
233
+ // @silent-fallback-ok: a tmp-file write race ⇒ report "did not acquire"
234
+ // (false); the acquire loop re-reads and retries — never a silent grant.
235
+ try {
236
+ SafeFsExecutor.safeUnlinkSync(tmp, { operation: 'SingleInstanceLock.tryWriteLock:cleanup-tmp' });
237
+ }
238
+ catch { /* @silent-fallback-ok: tmp cleanup is best-effort */ }
239
+ return false;
240
+ }
241
+ // Use link()/rename guarded by an O_EXCL pre-check: rename would clobber an
242
+ // existing lock, so only rename when the target does NOT exist. The window
243
+ // is narrow; a clobber would only happen between two simultaneous fresh
244
+ // starts, and the pid-guarded release prevents deleting the winner's lock.
245
+ try {
246
+ if (fs.existsSync(this.lockPath())) {
247
+ // Someone created the lock between our read and now — back off.
248
+ try {
249
+ SafeFsExecutor.safeUnlinkSync(tmp, { operation: 'SingleInstanceLock.tryWriteLock:exists-backoff' });
250
+ }
251
+ catch { /* @silent-fallback-ok: tmp cleanup is best-effort */ }
252
+ return false;
253
+ }
254
+ fs.renameSync(tmp, this.lockPath());
255
+ return true;
256
+ }
257
+ catch {
258
+ // @silent-fallback-ok: a rename race ⇒ report "did not acquire" (false);
259
+ // the acquire loop re-reads and retries — never a silent grant past the cap.
260
+ try {
261
+ SafeFsExecutor.safeUnlinkSync(tmp, { operation: 'SingleInstanceLock.tryWriteLock:cleanup-tmp' });
262
+ }
263
+ catch { /* @silent-fallback-ok: tmp cleanup is best-effort */ }
264
+ return false;
265
+ }
266
+ }
267
+ ensureDir() {
268
+ const localDir = path.join(this.stateDir, 'local');
269
+ if (!fs.existsSync(localDir))
270
+ fs.mkdirSync(localDir, { recursive: true });
271
+ }
272
+ }
273
+ /**
274
+ * Wire release on every exit path (SIGTERM/SIGINT/exit). The caller's graceful
275
+ * shutdown should ALSO call `lock.release()` directly (belt-and-suspenders); the
276
+ * `exit` handler is the last-resort net for a hard exit. Returns a disposer that
277
+ * removes the handlers (rarely needed; useful in tests).
278
+ */
279
+ export function installReleaseHandlers(lock) {
280
+ const onExit = () => {
281
+ try {
282
+ lock.release();
283
+ }
284
+ catch { /* best-effort on teardown */ }
285
+ };
286
+ // 'exit' must be synchronous — release() is sync, so this is safe.
287
+ process.on('exit', onExit);
288
+ // SIGTERM/SIGINT: the server already installs its own graceful shutdown that
289
+ // exits the process, which fires 'exit'. We add direct handlers too so the
290
+ // lock frees even if some other SIGTERM handler exits first.
291
+ const onSignal = () => { onExit(); };
292
+ process.on('SIGTERM', onSignal);
293
+ process.on('SIGINT', onSignal);
294
+ return () => {
295
+ process.off('exit', onExit);
296
+ process.off('SIGTERM', onSignal);
297
+ process.off('SIGINT', onSignal);
298
+ };
299
+ }
300
+ //# sourceMappingURL=SingleInstanceLock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SingleInstanceLock.js","sourceRoot":"","sources":["../../src/core/SingleInstanceLock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * SpawnCapIntelligenceProvider — P1 chokepoint + P3 bounded ingress of the
3
+ * SIMPLE fork-bomb prevention design (docs/specs/forkbomb-prevention-simple.md).
4
+ *
5
+ * A thin WRAPPER provider, layered EXACTLY like wrapIntelligenceWithCircuitBreaker:
6
+ * its `evaluate()` ACQUIRES a host-wide spawn slot (the HostSpawnSemaphore),
7
+ * calls the inner provider's `evaluate()` (the actual `claude -p` / `codex exec`
8
+ * spawn), and RELEASES the slot in a `finally`. It is installed at every return
9
+ * arm of `buildIntelligenceProvider`, so EVERY provider the factory hands out is
10
+ * bounded — and the acquire is PER-`evaluate()`, which is load-bearing:
11
+ * CoherenceGate builds its provider ONCE and fans ~10 reviewers in parallel
12
+ * through that ONE shared instance (the primary incident driver), so each of the
13
+ * N concurrent `evaluate()` calls must independently acquire a slot. A
14
+ * build-time acquire would NOT bind the fan-out.
15
+ *
16
+ * P3 — BOUNDED INGRESS (never an unbounded wait queue):
17
+ * When the host cap is saturated, acquire POLLS the holder-set on a short
18
+ * interval (~100ms) up to `acquireMs` (default 5000ms) — NOT an in-memory
19
+ * waiter queue. Each poll is a cheap lock+count; the caller's large prompt
20
+ * state stays where it already lives (no queue-node heap growth). A bound on
21
+ * CONCURRENT POLLERS (`waitersMax`, default 64) caps the waiters too.
22
+ *
23
+ * On genuine exhaustion (timeout OR waiters-full):
24
+ * - A GATING call (options.attribution.gating === true) THROWS
25
+ * LlmCapacityUnavailableError — the caller's catch fails CLOSED (hold), NOT
26
+ * auto-pass. The four gate seams (MessageSentinel / InputGuard /
27
+ * MessagingToneGate / CoherenceReviewer) recognize this typed error.
28
+ * - A non-gating BACKGROUND call also THROWS LlmCapacityUnavailableError;
29
+ * its existing catch degrades to its heuristic/no-LLM path (loud + counted
30
+ * via DegradationReporter, never silent) — same shape as a circuit-open
31
+ * throw, so existing background catches already handle it.
32
+ *
33
+ * This wrapper SHEDS BEFORE the inner provider runs — no subprocess spawns on a
34
+ * shed, which is the whole point (no `claude -p`, no RSS).
35
+ */
36
+ import type { IntelligenceProvider, IntelligenceOptions } from './types.js';
37
+ import { HostSpawnSemaphore } from './hostSpawnSemaphore.js';
38
+ /**
39
+ * Thrown when the host-wide spawn cap is saturated and the bounded-acquire
40
+ * window elapsed (or the concurrent-waiter ceiling was hit). A typed,
41
+ * recognizable shed — distinct from a rate-limit (LlmCircuitOpenError) and from
42
+ * a provider error — so each gate seam can fail CLOSED on it specifically.
43
+ */
44
+ export declare class LlmCapacityUnavailableError extends Error {
45
+ readonly reason: 'acquire-timeout' | 'waiters-full';
46
+ readonly waitedMs: number;
47
+ readonly capacityUnavailable: true;
48
+ constructor(reason: 'acquire-timeout' | 'waiters-full', waitedMs: number);
49
+ }
50
+ /** Narrowing helper for the gate seams (avoids `instanceof` import churn). */
51
+ export declare function isCapacityUnavailable(err: unknown): err is LlmCapacityUnavailableError;
52
+ export interface SpawnCapProviderDeps {
53
+ semaphore?: HostSpawnSemaphore;
54
+ /** Poll-retry budget in ms. Default resolved from env/config/5000. */
55
+ acquireMs?: number;
56
+ /** Concurrent-poller ceiling. Default resolved from env/config/64. */
57
+ waitersMax?: number;
58
+ /** Poll interval in ms (default 100). */
59
+ pollIntervalMs?: number;
60
+ now?: () => number;
61
+ /** Unique-id generator (default the semaphore's). */
62
+ genId?: () => string;
63
+ /** Awaitable delay (tests override to avoid real timers). */
64
+ sleep?: (ms: number) => Promise<void>;
65
+ }
66
+ /** Live count of callers currently polling for a spawn slot (P3 observability). */
67
+ export declare function activeSpawnPollers(): number;
68
+ /** Test seam. */
69
+ export declare function _resetSpawnPollersForTest(): void;
70
+ export declare class SpawnCapIntelligenceProvider implements IntelligenceProvider {
71
+ private readonly inner;
72
+ private readonly semaphore;
73
+ private readonly acquireMs;
74
+ private readonly waitersMax;
75
+ private readonly pollIntervalMs;
76
+ private readonly now;
77
+ private readonly genId;
78
+ private readonly sleep;
79
+ constructor(inner: IntelligenceProvider, deps?: SpawnCapProviderDeps);
80
+ evaluate(prompt: string, options?: IntelligenceOptions): Promise<string>;
81
+ /** Poll the semaphore every `pollIntervalMs` until acquired or the budget elapses. */
82
+ private pollAcquire;
83
+ }
84
+ /**
85
+ * Wrap a provider with the host spawn cap. No-ops on null (so a possibly-null
86
+ * factory result passes through unchanged) and is idempotent (never double-wraps).
87
+ */
88
+ export declare function wrapIntelligenceWithSpawnCap(provider: IntelligenceProvider | null | undefined, deps?: SpawnCapProviderDeps): IntelligenceProvider | null;
89
+ //# sourceMappingURL=SpawnCapIntelligenceProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpawnCapIntelligenceProvider.d.ts","sourceRoot":"","sources":["../../src/core/SpawnCapIntelligenceProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAC5E,OAAO,EACL,kBAAkB,EAInB,MAAM,yBAAyB,CAAC;AAEjC;;;;;GAKG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;IAGlD,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,cAAc;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM;IAH3B,QAAQ,CAAC,mBAAmB,EAAG,IAAI,CAAU;gBAElC,MAAM,EAAE,iBAAiB,GAAG,cAAc,EAC1C,QAAQ,EAAE,MAAM;CAO5B;AAED,8EAA8E;AAC9E,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,2BAA2B,CAKtF;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAOD,mFAAmF;AACnF,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,iBAAiB;AACjB,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAED,qBAAa,4BAA6B,YAAW,oBAAoB;IAUrE,OAAO,CAAC,QAAQ,CAAC,KAAK;IATxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;gBAGnC,KAAK,EAAE,oBAAoB,EAC5C,IAAI,GAAE,oBAAyB;IAY3B,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;IA0C9E,sFAAsF;YACxE,WAAW;CAiB1B;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,oBAAoB,GAAG,IAAI,GAAG,SAAS,EACjD,IAAI,CAAC,EAAE,oBAAoB,GAC1B,oBAAoB,GAAG,IAAI,CAI7B"}