@essentialai/cogent-bridge 3.23.3 → 3.23.5

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.
@@ -0,0 +1,198 @@
1
+ /**
2
+ * CROSS-PROCESS SESSION WAKE LOCK — never two `claude --resume <S>` at once in one checkout.
3
+ *
4
+ * ── THE DEFECT THIS FIXES, AND HOW IT DIFFERS FROM wake-claim.ts ─────────────────────────
5
+ * `wake-claim.ts` excludes on the MESSAGE id: it stops one message, fanned by the relay to N
6
+ * sockets of the same peer, from being answered N times. That is a real defect and it is fixed.
7
+ *
8
+ * It cannot help here. Measured on a real user (peer `marvin-coder`, 2026-09-05): the SAME peer
9
+ * was registered on the FREE relay at plugin v3.20.0 AND on the TEAM relay at v3.23.1 — same cwd,
10
+ * same thread id, two live bridge processes. Reachable because credentials are keyed on cwd ALONE
11
+ * (`~/.cogent/credentials/<sha256(cwd)>.json`, credential-store.ts:64-71); the endpoint is not part
12
+ * of the key, so a second join overwrites the file while the first process keeps running from its
13
+ * in-memory token and live WebSocket. Nothing on disk then records that the first relay is in play,
14
+ * which is why nothing reaps it.
15
+ *
16
+ * Two relays mint two DIFFERENT message ids, so both per-message claims succeed and both processes
17
+ * resume the SAME session concurrently. `scripts/wake-dedup-matrix-test.mjs --topology two-relay`
18
+ * reproduces it offline against two local relays: two wakes, one session id, 4001ms of overlap.
19
+ *
20
+ * So the exclusion has to key on the RESUME TARGET — (cwd, sessionId) — not on the message.
21
+ *
22
+ * ── WHY IT WAITS INSTEAD OF DROPPING ─────────────────────────────────────────────────────
23
+ * Two relays carry two genuinely distinct questions; answering both is CORRECT. What is never
24
+ * correct is running both turns at once against one worktree. So a loser waits for the holder and
25
+ * then proceeds, which serialises the turns without losing a message. Dropping would trade a
26
+ * corruption bug for a silence bug, and silence is the worse failure mode in this codebase.
27
+ *
28
+ * ── FAILS OPEN, DELIBERATELY ─────────────────────────────────────────────────────────────
29
+ * Every uncertain path — unreadable lock dir, corrupt holder record, a holder that outlives the
30
+ * wait budget — resolves to "go ahead and wake". A lock that can mute an agent for good is worse
31
+ * than the overlap it prevents. That is the 3.21.5 lesson: a fix whose blast radius is "auto-wake
32
+ * stops working" is not a safe fix, however correct it looks.
33
+ */
34
+ import crypto from "node:crypto";
35
+ import fs from "node:fs";
36
+ import os from "node:os";
37
+ import path from "node:path";
38
+ import { getConfig } from "../config.js";
39
+ import { logger } from "../logger.js";
40
+ /** Hard ceiling on how long one lease may exclude others. Mirrors wake-claim's CLAIM_MAX_MS. */
41
+ export const SESSION_WAKE_MAX_MS = 1_800_000; // 30 min
42
+ /** Multiplier over COGENT_TIMEOUT_MS covering primary + rotated-session retry, as wake-claim does. */
43
+ const LEASE_TIMEOUT_FACTOR = 3;
44
+ /** How often a waiter re-checks. Short enough to be invisible, long enough not to spin. */
45
+ const POLL_MS = 250;
46
+ /** MUST match wake-claim / wake-inflight / credential-store — one cwd, one key, everywhere. */
47
+ function hashCwd(cwd) {
48
+ return crypto.createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 16);
49
+ }
50
+ const safe = (s) => String(s).replace(/[^A-Za-z0-9._-]/g, "_");
51
+ /** `~/.cogent/wake-sessions/<sha256(cwd)[..16]>.<sessionId>.json` */
52
+ export function sessionWakeLockPath(sessionId, cwd = process.cwd()) {
53
+ return path.join(os.homedir(), ".cogent", "wake-sessions", `${hashCwd(cwd)}.${safe(sessionId)}.json`);
54
+ }
55
+ function leaseTtlMs() {
56
+ let timeout = 300_000;
57
+ try {
58
+ const c = getConfig()?.COGENT_TIMEOUT_MS;
59
+ if (typeof c === "number" && Number.isFinite(c) && c > 0)
60
+ timeout = c;
61
+ }
62
+ catch {
63
+ /* config not loaded — documented default */
64
+ }
65
+ return Math.min(timeout * LEASE_TIMEOUT_FACTOR, SESSION_WAKE_MAX_MS);
66
+ }
67
+ /** Default budget for waiting out another process's turn: one full turn. */
68
+ function defaultMaxWaitMs() {
69
+ try {
70
+ const c = getConfig()?.COGENT_TIMEOUT_MS;
71
+ if (typeof c === "number" && Number.isFinite(c) && c > 0)
72
+ return c;
73
+ }
74
+ catch {
75
+ /* config not loaded */
76
+ }
77
+ return 300_000;
78
+ }
79
+ /**
80
+ * Is the recorded holder still a live process?
81
+ *
82
+ * Our OWN pid counts as DEAD here, unlike wake-claim: in-process wakes are already serialised by
83
+ * AutoRelayService's `processing` flag, so a lease still bearing this pid is a leak from an earlier
84
+ * turn, never a concurrent one. Blocking on ourselves would deadlock the agent for the full budget.
85
+ */
86
+ function holderAlive(pid) {
87
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
88
+ return false;
89
+ if (pid === process.pid)
90
+ return false;
91
+ try {
92
+ process.kill(pid, 0); // signal 0 = existence check only
93
+ return true;
94
+ }
95
+ catch (err) {
96
+ // EPERM means it EXISTS but belongs to another user — alive for our purposes.
97
+ return err?.code === "EPERM";
98
+ }
99
+ }
100
+ /** Read the current holder, or null when absent/corrupt (both mean "free"). */
101
+ function readHolder(p) {
102
+ try {
103
+ const held = JSON.parse(fs.readFileSync(p, "utf-8"));
104
+ if (!held || typeof held.expiresAt !== "number")
105
+ return null;
106
+ return held;
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
113
+ /**
114
+ * Become the single process resuming `sessionId` in `cwd`, waiting out any other holder.
115
+ *
116
+ * Never returns `acquired: false` today — every path either takes the lease or fails open — but the
117
+ * field is explicit so a caller reads the intent rather than assuming it.
118
+ */
119
+ export async function acquireSessionWake(sessionId, cwd = process.cwd(), opts = {}) {
120
+ const p = sessionWakeLockPath(sessionId, cwd);
121
+ const maxWaitMs = opts.maxWaitMs ?? defaultMaxWaitMs();
122
+ const started = Date.now();
123
+ let contended = false;
124
+ const lease = () => ({
125
+ acquired: true,
126
+ waitedMs: Date.now() - started,
127
+ contended,
128
+ release: () => releaseSessionWake(sessionId, cwd),
129
+ });
130
+ /** Fail open: the caller wakes, and nothing is left behind that could mute the next turn. */
131
+ const failOpen = (why) => {
132
+ logger.warn(`session-wake-lock: ${why} — waking anyway (fail-open)`);
133
+ return { acquired: true, waitedMs: Date.now() - started, contended, release: () => { } };
134
+ };
135
+ try {
136
+ fs.mkdirSync(path.dirname(p), { recursive: true });
137
+ }
138
+ catch (err) {
139
+ return failOpen(`cannot create lock dir (${err.message})`);
140
+ }
141
+ for (;;) {
142
+ const now = Date.now();
143
+ const body = JSON.stringify({
144
+ pid: process.pid,
145
+ sessionId,
146
+ startedAt: now,
147
+ expiresAt: now + leaseTtlMs(),
148
+ });
149
+ try {
150
+ fs.writeFileSync(p, body, { flag: "wx" }); // atomic: exactly one winner
151
+ if (contended) {
152
+ logger.info(`Auto-relay: waited ${Date.now() - started}ms for another bridge process to finish its ` +
153
+ `turn on session ${sessionId} before resuming — turns are now serialised, not overlapping.`);
154
+ }
155
+ return lease();
156
+ }
157
+ catch (err) {
158
+ if (err?.code !== "EEXIST") {
159
+ return failOpen(`cannot write lock (${err.message})`);
160
+ }
161
+ }
162
+ // Held. Take it over when the holder is gone, expired, or unreadable — a lease that outlives
163
+ // its holder would mute this agent until the writer-stamped expiry.
164
+ const held = readHolder(p);
165
+ if (!held || held.expiresAt <= now || !holderAlive(held.pid)) {
166
+ try {
167
+ fs.writeFileSync(p, body); // steal
168
+ }
169
+ catch (err) {
170
+ return failOpen(`cannot steal abandoned lock (${err.message})`);
171
+ }
172
+ logger.debug(`session-wake-lock: took over an abandoned lease for session ${sessionId}`);
173
+ return lease();
174
+ }
175
+ if (!contended) {
176
+ contended = true;
177
+ logger.info(`Auto-relay: pid ${held.pid} is already resuming session ${sessionId} in this checkout — ` +
178
+ `waiting for it rather than running a second '--resume' over the same worktree. ` +
179
+ `Two bridge processes are registered for this cwd (check for a stale one).`);
180
+ }
181
+ if (Date.now() - started >= maxWaitMs) {
182
+ // Budget spent and the holder is STILL alive. Proceeding risks the overlap this lock
183
+ // exists to prevent; refusing risks silence. Silence is worse — but say so loudly.
184
+ return failOpen(`pid ${held.pid} still holds session ${sessionId} after ${Date.now() - started}ms`);
185
+ }
186
+ await sleep(POLL_MS);
187
+ }
188
+ }
189
+ /** Release the lease. Best-effort; an orphan expires or is stolen when its holder dies. */
190
+ export function releaseSessionWake(sessionId, cwd = process.cwd()) {
191
+ try {
192
+ fs.unlinkSync(sessionWakeLockPath(sessionId, cwd));
193
+ }
194
+ catch {
195
+ /* already gone, or never ours */
196
+ }
197
+ }
198
+ //# sourceMappingURL=session-wake-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-wake-lock.js","sourceRoot":"","sources":["../../src/services/session-wake-lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /** Hard ceiling on how long one lease may exclude others. Mirrors wake-claim's CLAIM_MAX_MS. */
2
+ export declare const WAKE_LOCK_MAX_MS = 1800000;
3
+ /** `~/.cogent/wake-locks/<sha256(cwd)[..16]>.json` — ONE checkout, ONE turn. */
4
+ export declare function wakeLockPath(cwd?: string): string;
5
+ export interface WakeLease {
6
+ /** True whenever the caller may wake — including every fail-open path. */
7
+ acquired: boolean;
8
+ /** Milliseconds spent waiting for another process. 0 when uncontended. */
9
+ waitedMs: number;
10
+ /** True when another live process held the lease and we had to wait for it. */
11
+ contended: boolean;
12
+ /** Idempotent. Safe to call even when the lease was never really taken. */
13
+ release: () => void;
14
+ }
15
+ /**
16
+ * Become the single process running an agent turn in `cwd`, waiting out any other holder.
17
+ *
18
+ * `sessionId` is recorded in the lease for diagnostics; it is NOT part of the key (see the header).
19
+ *
20
+ * Never returns `acquired: false` today — every path either takes the lease or fails open — but the
21
+ * field is explicit so a caller reads the intent rather than assuming it.
22
+ */
23
+ export declare function acquireWakeLock(cwd?: string, sessionId?: string, opts?: {
24
+ maxWaitMs?: number;
25
+ }): Promise<WakeLease>;
26
+ /** Test-only: reset the in-process nesting counter. Production code must never call this. */
27
+ export declare function __resetWakeLockDepthForTests(): void;
28
+ /** Release the lease. Best-effort; an orphan expires or is stolen when its holder dies. */
29
+ export declare function releaseWakeLock(cwd?: string): void;
30
+ //# sourceMappingURL=wake-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wake-lock.d.ts","sourceRoot":"","sources":["../../src/services/wake-lock.ts"],"names":[],"mappings":"AAuDA,gGAAgG;AAChG,eAAO,MAAM,gBAAgB,UAAY,CAAC;AAmB1C,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,CAEhE;AAiED,MAAM,WAAW,SAAS;IACxB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,SAAS,EAAE,OAAO,CAAC;IACnB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAID;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,GAAG,GAAE,MAAsB,EAC3B,SAAS,SAAY,EACrB,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAChC,OAAO,CAAC,SAAS,CAAC,CAyGpB;AAED,6FAA6F;AAC7F,wBAAgB,4BAA4B,IAAI,IAAI,CAEnD;AAED,2FAA2F;AAC3F,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAsB,GAAG,IAAI,CAMjE"}
@@ -0,0 +1,257 @@
1
+ /**
2
+ * CROSS-PROCESS WAKE LOCK — never two agent turns running at once in ONE CHECKOUT.
3
+ *
4
+ * ── THE DEFECT THIS FIXES, AND HOW IT DIFFERS FROM wake-claim.ts ─────────────────────────
5
+ * `wake-claim.ts` excludes on the MESSAGE id: it stops one message, fanned by the relay to N
6
+ * sockets of the same peer, from being answered N times. That is a real defect and it is fixed.
7
+ *
8
+ * It cannot help here. Measured on a real user (peer `marvin-coder`, 2026-09-05): the SAME peer
9
+ * was registered on the FREE relay at plugin v3.20.0 AND on the TEAM relay at v3.23.1 — same cwd,
10
+ * same thread id, two live bridge processes. Reachable because credentials are keyed on cwd ALONE
11
+ * (`~/.cogent/credentials/<sha256(cwd)>.json`, credential-store.ts:64-71); the endpoint is not part
12
+ * of the key, so a second join overwrites the file while the first process keeps running from its
13
+ * in-memory token and live WebSocket. Nothing on disk then records that the first relay is in play,
14
+ * which is why nothing reaps it.
15
+ *
16
+ * Two relays mint two DIFFERENT message ids, so both per-message claims succeed and both processes
17
+ * resume the SAME session concurrently. `scripts/wake-dedup-matrix-test.mjs --topology two-relay`
18
+ * reproduces it offline against two local relays: two wakes, one session id, 4001ms of overlap.
19
+ *
20
+ * ── WHY THE KEY IS THE CWD, NOT THE SESSION ──────────────────────────────────────────────
21
+ * The first cut of this keyed on (cwd, sessionId). That is too narrow. `pinnedSessionId` is
22
+ * per-process in-memory state (auto-relay.ts:338) and `decideResolution` returns whatever pin it
23
+ * was handed whenever that pin still validates, so two bridge processes in one checkout can hold
24
+ * DIFFERENT pins — they would take different locks and overlap anyway.
25
+ *
26
+ * More importantly the damage is a property of the WORKTREE, not of the session: two `claude`
27
+ * processes editing one checkout corrupt it whether or not they resume the same session. So the
28
+ * key is the cwd. The session id is recorded in the lease for diagnostics only.
29
+ *
30
+ * `wake-claim.ts` explicitly rejected a per-cwd lock — "a per-cwd lock would ... LOSE messages,
31
+ * the loser would silently drop a message it was the only one free to handle". That objection is
32
+ * about DROP semantics and does not apply here: this lock WAITS. Same key, opposite conclusion.
33
+ *
34
+ * Blast radius is deliberately small: with a single bridge process the lock is never contended, so
35
+ * nothing about normal operation changes. Contention only arises in the topology that IS the bug.
36
+ *
37
+ * ── WHY IT WAITS INSTEAD OF DROPPING ─────────────────────────────────────────────────────
38
+ * Two relays carry two genuinely distinct questions; answering both is CORRECT. What is never
39
+ * correct is running both turns at once against one worktree. So a loser waits for the holder and
40
+ * then proceeds, which serialises the turns without losing a message. Dropping would trade a
41
+ * corruption bug for a silence bug, and silence is the worse failure mode in this codebase.
42
+ *
43
+ * ── FAILS OPEN, DELIBERATELY ─────────────────────────────────────────────────────────────
44
+ * Every uncertain path — unreadable lock dir, corrupt holder record, a holder that outlives the
45
+ * wait budget — resolves to "go ahead and wake". A lock that can mute an agent for good is worse
46
+ * than the overlap it prevents. That is the 3.21.5 lesson: a fix whose blast radius is "auto-wake
47
+ * stops working" is not a safe fix, however correct it looks.
48
+ */
49
+ import crypto from "node:crypto";
50
+ import fs from "node:fs";
51
+ import os from "node:os";
52
+ import path from "node:path";
53
+ import { getConfig } from "../config.js";
54
+ import { logger } from "../logger.js";
55
+ /** Hard ceiling on how long one lease may exclude others. Mirrors wake-claim's CLAIM_MAX_MS. */
56
+ export const WAKE_LOCK_MAX_MS = 1_800_000; // 30 min
57
+ /** Multiplier over COGENT_TIMEOUT_MS covering primary + rotated-session retry, as wake-claim does. */
58
+ const LEASE_TIMEOUT_FACTOR = 3;
59
+ /** How often a waiter re-checks. Short enough to be invisible, long enough not to spin. */
60
+ const POLL_MS = 250;
61
+ /** MUST match wake-claim / wake-inflight / credential-store — one cwd, one key, everywhere. */
62
+ function hashCwd(cwd) {
63
+ return crypto.createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 16);
64
+ }
65
+ /** `~/.cogent/wake-locks/<sha256(cwd)[..16]>.json` — ONE checkout, ONE turn. */
66
+ export function wakeLockPath(cwd = process.cwd()) {
67
+ return path.join(os.homedir(), ".cogent", "wake-locks", `${hashCwd(cwd)}.json`);
68
+ }
69
+ function leaseTtlMs() {
70
+ let timeout = 300_000;
71
+ try {
72
+ const c = getConfig()?.COGENT_TIMEOUT_MS;
73
+ if (typeof c === "number" && Number.isFinite(c) && c > 0)
74
+ timeout = c;
75
+ }
76
+ catch {
77
+ /* config not loaded — documented default */
78
+ }
79
+ return Math.min(timeout * LEASE_TIMEOUT_FACTOR, WAKE_LOCK_MAX_MS);
80
+ }
81
+ /** Default budget for waiting out another process's turn: one full turn. */
82
+ function defaultMaxWaitMs() {
83
+ try {
84
+ const c = getConfig()?.COGENT_TIMEOUT_MS;
85
+ if (typeof c === "number" && Number.isFinite(c) && c > 0)
86
+ return c;
87
+ }
88
+ catch {
89
+ /* config not loaded */
90
+ }
91
+ return 300_000;
92
+ }
93
+ /**
94
+ * How many nested acquisitions this process currently holds.
95
+ *
96
+ * 🔴 WHY RE-ENTRANCY IS NOT OPTIONAL. `_injectOnly` is called from handleIncoming (auto-relay.ts:811),
97
+ * OUTSIDE the `processing` lock, so it can run while a wake in THIS SAME process holds the lease.
98
+ * Without this counter the inner acquire would treat the self-pid record as stale, steal it, and
99
+ * then delete it on release — leaving the outer wake running with no lease and letting a second
100
+ * process in. That is the hole this lock exists to close, reopened one level down.
101
+ */
102
+ let heldDepth = 0;
103
+ /**
104
+ * Is the recorded holder still a live process?
105
+ *
106
+ * A record bearing OUR pid counts as dead ONLY when heldDepth is 0 — i.e. we are not actually
107
+ * holding it, so the file is a leak from an earlier turn. While we do hold it, re-entrancy is
108
+ * handled before this is ever consulted.
109
+ */
110
+ function holderAlive(pid) {
111
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
112
+ return false;
113
+ if (pid === process.pid)
114
+ return heldDepth > 0;
115
+ try {
116
+ process.kill(pid, 0); // signal 0 = existence check only
117
+ return true;
118
+ }
119
+ catch (err) {
120
+ // EPERM means it EXISTS but belongs to another user — alive for our purposes.
121
+ return err?.code === "EPERM";
122
+ }
123
+ }
124
+ /** Read the current holder, or null when absent/corrupt (both mean "free"). */
125
+ function readHolder(p) {
126
+ try {
127
+ const held = JSON.parse(fs.readFileSync(p, "utf-8"));
128
+ if (!held || typeof held.expiresAt !== "number")
129
+ return null;
130
+ return held;
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ }
136
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
137
+ /**
138
+ * Become the single process running an agent turn in `cwd`, waiting out any other holder.
139
+ *
140
+ * `sessionId` is recorded in the lease for diagnostics; it is NOT part of the key (see the header).
141
+ *
142
+ * Never returns `acquired: false` today — every path either takes the lease or fails open — but the
143
+ * field is explicit so a caller reads the intent rather than assuming it.
144
+ */
145
+ export async function acquireWakeLock(cwd = process.cwd(), sessionId = "unknown", opts = {}) {
146
+ const p = wakeLockPath(cwd);
147
+ const maxWaitMs = opts.maxWaitMs ?? defaultMaxWaitMs();
148
+ const started = Date.now();
149
+ let contended = false;
150
+ // Re-entrant: this process already holds the checkout, so nothing may block and nothing may be
151
+ // written. Only the OUTERMOST release removes the file.
152
+ if (heldDepth > 0) {
153
+ heldDepth++;
154
+ let released = false;
155
+ return {
156
+ acquired: true,
157
+ waitedMs: 0,
158
+ contended: false,
159
+ release: () => {
160
+ if (released)
161
+ return;
162
+ released = true;
163
+ heldDepth--;
164
+ },
165
+ };
166
+ }
167
+ const lease = () => {
168
+ heldDepth++;
169
+ let released = false;
170
+ return {
171
+ acquired: true,
172
+ waitedMs: Date.now() - started,
173
+ contended,
174
+ release: () => {
175
+ if (released)
176
+ return;
177
+ released = true;
178
+ heldDepth--;
179
+ if (heldDepth === 0)
180
+ releaseWakeLock(cwd);
181
+ },
182
+ };
183
+ };
184
+ /** Fail open: the caller wakes, and nothing is left behind that could mute the next turn. */
185
+ const failOpen = (why) => {
186
+ logger.warn(`wake-lock: ${why} — waking anyway (fail-open)`);
187
+ return { acquired: true, waitedMs: Date.now() - started, contended, release: () => { } };
188
+ };
189
+ try {
190
+ fs.mkdirSync(path.dirname(p), { recursive: true });
191
+ }
192
+ catch (err) {
193
+ return failOpen(`cannot create lock dir (${err.message})`);
194
+ }
195
+ for (;;) {
196
+ const now = Date.now();
197
+ const body = JSON.stringify({
198
+ pid: process.pid,
199
+ sessionId,
200
+ startedAt: now,
201
+ expiresAt: now + leaseTtlMs(),
202
+ });
203
+ try {
204
+ fs.writeFileSync(p, body, { flag: "wx" }); // atomic: exactly one winner
205
+ if (contended) {
206
+ logger.info(`Auto-relay: waited ${Date.now() - started}ms for another bridge process to finish its ` +
207
+ `turn in this checkout before resuming — turns are serialised, not overlapping.`);
208
+ }
209
+ return lease();
210
+ }
211
+ catch (err) {
212
+ if (err?.code !== "EEXIST") {
213
+ return failOpen(`cannot write lock (${err.message})`);
214
+ }
215
+ }
216
+ // Held. Take it over when the holder is gone, expired, or unreadable — a lease that outlives
217
+ // its holder would mute this agent until the writer-stamped expiry.
218
+ const held = readHolder(p);
219
+ if (!held || held.expiresAt <= now || !holderAlive(held.pid)) {
220
+ try {
221
+ fs.writeFileSync(p, body); // steal
222
+ }
223
+ catch (err) {
224
+ return failOpen(`cannot steal abandoned lock (${err.message})`);
225
+ }
226
+ logger.debug(`wake-lock: took over an abandoned lease in ${cwd}`);
227
+ return lease();
228
+ }
229
+ if (!contended) {
230
+ contended = true;
231
+ logger.info(`Auto-relay: pid ${held.pid} is already running an agent turn in this checkout ` +
232
+ `(session ${held.sessionId}) — waiting for it rather than running a second resume over ` +
233
+ `the same worktree. Two bridge processes are live for this cwd; if that is unexpected, ` +
234
+ `check for a stale one left behind by a plugin update.`);
235
+ }
236
+ if (Date.now() - started >= maxWaitMs) {
237
+ // Budget spent and the holder is STILL alive. Proceeding risks the overlap this lock
238
+ // exists to prevent; refusing risks silence. Silence is worse — but say so loudly.
239
+ return failOpen(`pid ${held.pid} still holds this checkout after ${Date.now() - started}ms`);
240
+ }
241
+ await sleep(POLL_MS);
242
+ }
243
+ }
244
+ /** Test-only: reset the in-process nesting counter. Production code must never call this. */
245
+ export function __resetWakeLockDepthForTests() {
246
+ heldDepth = 0;
247
+ }
248
+ /** Release the lease. Best-effort; an orphan expires or is stolen when its holder dies. */
249
+ export function releaseWakeLock(cwd = process.cwd()) {
250
+ try {
251
+ fs.unlinkSync(wakeLockPath(cwd));
252
+ }
253
+ catch {
254
+ /* already gone, or never ours */
255
+ }
256
+ }
257
+ //# sourceMappingURL=wake-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wake-lock.js","sourceRoot":"","sources":["../../src/services/wake-lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ interface BridgeEntry {
2
+ pid: number;
3
+ startedAt: number;
4
+ version: string;
5
+ peerId: string;
6
+ endpoint: string;
7
+ cwd: string;
8
+ }
9
+ /** `~/.cogent/bridges/<sha256(cwd)[..16]>.<peerId>/` — one directory per identity. */
10
+ export declare function bridgeDirFor(cwd: string, peerId: string): string;
11
+ /**
12
+ * Announce this process as a bridge serving `peerId` in `cwd`.
13
+ *
14
+ * Called once the peer is actually registered, so a bridge that never registered never competes
15
+ * for ownership. Best-effort: a failure here leaves us un-announced, which can only make us LOSE
16
+ * ownership to someone else, never wrongly take it.
17
+ */
18
+ export declare function announceBridge(cwd: string, peerId: string, version: string, endpoint: string): void;
19
+ /** Remove this process's announcement. Called on graceful shutdown; a crash is handled by pruning. */
20
+ export declare function withdrawBridge(cwd: string, peerId: string): void;
21
+ /**
22
+ * Every LIVE bridge announced for this identity, newest process first.
23
+ *
24
+ * Prunes entries whose process is gone, so a machine that has been through a dozen plugin updates
25
+ * does not accumulate junk here forever.
26
+ */
27
+ export declare function listLiveBridges(cwd: string, peerId: string): BridgeEntry[];
28
+ export interface OwnershipVerdict {
29
+ /** True when this process should do this peer's work. Every uncertain path returns true. */
30
+ isOwner: boolean;
31
+ /** The bridge that owns it instead, when we do not. */
32
+ owner?: {
33
+ pid: number;
34
+ version: string;
35
+ endpoint: string;
36
+ };
37
+ /** How many live bridges serve this identity, including us. */
38
+ liveCount: number;
39
+ }
40
+ /**
41
+ * Should THIS process wake for `peerId` in `cwd`?
42
+ *
43
+ * No when a strictly newer live bridge serves the same identity — that bridge is the one the user
44
+ * most recently started, and we are the leftover.
45
+ */
46
+ export declare function checkWakeOwnership(cwd: string, peerId: string): OwnershipVerdict;
47
+ export {};
48
+ //# sourceMappingURL=wake-ownership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wake-ownership.d.ts","sourceRoot":"","sources":["../../src/services/wake-ownership.ts"],"names":[],"mappings":"AA0DA,UAAU,WAAW;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAQD,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEhE;AAeD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAgBnG;AAED,sGAAsG;AACtG,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAMhE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,EAAE,CAiC1E;AAED,MAAM,WAAW,gBAAgB;IAC/B,4FAA4F;IAC5F,OAAO,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,KAAK,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,CA0BhF"}