@agfpd/iapeer 0.2.50 → 0.2.51
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.
- package/package.json +1 -1
- package/src/launch/canary.test.ts +43 -1
- package/src/launch/canary.ts +20 -12
package/package.json
CHANGED
|
@@ -69,7 +69,8 @@ describe('canary script (pure)', () => {
|
|
|
69
69
|
// proven live); the SERVER's liveness decides, after a dismissal grace sleep.
|
|
70
70
|
expect(s).not.toContain(`[ "$rc" -eq 0 ] || [ "$rc" -ge 128 ]; then exit 0`) // the v1 hole
|
|
71
71
|
expect(s).toContain('sleep 2') // dismissal grace window
|
|
72
|
-
expect(s).toContain(`
|
|
72
|
+
expect(s).toContain(`kill -0 "$SPID" 2>/dev/null; then exit 0`) // original server alive → nothing to record
|
|
73
|
+
expect(s).toContain(`[ -z "$SPID" ]`) // fallback only when the original pid was unavailable
|
|
73
74
|
expect(s).toContain('cause=server-vanished') // connection drop (SIGKILL/OOM class)
|
|
74
75
|
expect(s).toContain('cause=signaled-server-gone') // rc=0: channel/client-TERM, server died
|
|
75
76
|
expect(s).toContain('cause=client-killed-server-gone') // rc≥128: client took a hard kill
|
|
@@ -119,6 +120,7 @@ describe.if(tmuxAvailable)('canary live (sandbox tmux servers)', () => {
|
|
|
119
120
|
'notifier-canatear',
|
|
120
121
|
'claude-canasweep',
|
|
121
122
|
'claude-canaclient',
|
|
123
|
+
'claude-canareplaced',
|
|
122
124
|
]
|
|
123
125
|
|
|
124
126
|
afterAll(() => {
|
|
@@ -285,4 +287,44 @@ describe.if(tmuxAvailable)('canary live (sandbox tmux servers)', () => {
|
|
|
285
287
|
},
|
|
286
288
|
20000,
|
|
287
289
|
)
|
|
290
|
+
|
|
291
|
+
test(
|
|
292
|
+
'dirty server death followed by immediate replacement on the same socket still records the ORIGINAL death',
|
|
293
|
+
async () => {
|
|
294
|
+
const identity = 'claude-canareplaced'
|
|
295
|
+
const { sock, logDir } = bringUp(identity)
|
|
296
|
+
const pid = serverPid(sock)
|
|
297
|
+
expect(pid).toBeGreaterThan(0)
|
|
298
|
+
expect(ensureServerCanary({ identity, sock, exitLogDir: logDir })).toBe('spawned')
|
|
299
|
+
expect(await waitFor(() => canaryRunning(identity), 3000)).toBe(true)
|
|
300
|
+
await sleep(500)
|
|
301
|
+
|
|
302
|
+
process.kill(pid, 'SIGKILL')
|
|
303
|
+
// Reuse the same socket before the canary's 2s dismissal-grace window
|
|
304
|
+
// expires. The old socket-level probe treated this successor as "server
|
|
305
|
+
// alive" and lost the predecessor's death; the canary must key on SPID.
|
|
306
|
+
let started = false
|
|
307
|
+
for (let i = 0; i < 20; i++) {
|
|
308
|
+
const r = spawnSync('tmux', ['-S', sock, 'new-session', '-d', '-s', identity, 'sleep', '120'], {
|
|
309
|
+
stdio: 'ignore',
|
|
310
|
+
})
|
|
311
|
+
if (r.status === 0) {
|
|
312
|
+
started = true
|
|
313
|
+
break
|
|
314
|
+
}
|
|
315
|
+
await sleep(50)
|
|
316
|
+
}
|
|
317
|
+
expect(started).toBe(true)
|
|
318
|
+
|
|
319
|
+
const log = exitLogPath(logDir)
|
|
320
|
+
expect(await waitFor(() => existsSync(log) && readFileSync(log, 'utf8').includes('ev=server-exit'), 8000)).toBe(
|
|
321
|
+
true,
|
|
322
|
+
)
|
|
323
|
+
const line = readFileSync(log, 'utf8')
|
|
324
|
+
expect(line).toContain(`ev=server-exit identity=${identity}`)
|
|
325
|
+
expect(line).toContain(`server_pid=${pid}`)
|
|
326
|
+
expect(readdirSync(serverDeathsDir(logDir)).length).toBe(1)
|
|
327
|
+
},
|
|
328
|
+
20000,
|
|
329
|
+
)
|
|
288
330
|
})
|
package/src/launch/canary.ts
CHANGED
|
@@ -19,14 +19,18 @@
|
|
|
19
19
|
// sh recorder (`dismissCanary` → TERM → trap → silent exit). POSIX trap
|
|
20
20
|
// semantics make this race-free: the trap runs before any recording.
|
|
21
21
|
// • When wait-for returns — ANY code — the script sleeps 2 s (a concurrent
|
|
22
|
-
// dismissal TERM wins here), then probes SERVER
|
|
23
|
-
// silently (a lost canary is re-armed and logged
|
|
24
|
-
// within a tick);
|
|
25
|
-
// and unclaimed → ONE logfmt line `ev=server-exit`
|
|
26
|
-
// per-peer death-cause home, next to pane-died's
|
|
27
|
-
// forensics snapshot (vm_stat / swap / top-RSS ps /
|
|
28
|
-
// captured within seconds — the evidence the 60 s
|
|
29
|
-
// recover ("системных следов ноль" was the recurring
|
|
22
|
+
// dismissal TERM wins here), then probes the ORIGINAL SERVER PID captured at
|
|
23
|
+
// arm time: still alive → exit silently (a lost canary is re-armed and logged
|
|
24
|
+
// by the supervise retrofit within a tick); gone with nobody having dismissed
|
|
25
|
+
// us → the death is real and unclaimed → ONE logfmt line `ev=server-exit`
|
|
26
|
+
// into exits.log (the per-peer death-cause home, next to pane-died's
|
|
27
|
+
// `ev=session-exit`) + a forensics snapshot (vm_stat / swap / top-RSS ps /
|
|
28
|
+
// fresh DiagnosticReports) captured within seconds — the evidence the 60 s
|
|
29
|
+
// supervise tick can never recover ("системных следов ноль" was the recurring
|
|
30
|
+
// investigation outcome). PID identity matters: a wake/attach can replace the
|
|
31
|
+
// tmux server on the same socket before the 2 s grace elapses; probing only
|
|
32
|
+
// the socket would mistake the successor for the dead original and lose the
|
|
33
|
+
// canary record (live canary-gap 11.06).
|
|
30
34
|
// The raw wait_rc still ATTRIBUTES the death (cause=server-vanished /
|
|
31
35
|
// signaled-server-gone / client-killed-server-gone).
|
|
32
36
|
// Residual blind spot (structural): a killer that SIGKILLs the sh recorder
|
|
@@ -98,12 +102,16 @@ export function canaryScript(o: CanaryScriptOptions): string {
|
|
|
98
102
|
// `rc=0 || rc>=128 → silent` was exactly that hole). v2 contract instead:
|
|
99
103
|
// • deliberate teardowns DISMISS this sh (TERM → trap above) — the sleep
|
|
100
104
|
// below gives a concurrently-delivered dismissal time to win;
|
|
101
|
-
// • then the SERVER's liveness, not the exit code, decides: alive →
|
|
105
|
+
// • then the ORIGINAL SERVER PID's liveness, not the exit code, decides: alive →
|
|
102
106
|
// nothing to record (a lost canary is re-armed and logged by the
|
|
103
|
-
// supervise retrofit within a tick); dead and nobody
|
|
104
|
-
// the death is real and unclaimed → record it.
|
|
107
|
+
// supervise retrofit within a tick); original server dead and nobody
|
|
108
|
+
// dismissed us → the death is real and unclaimed → record it. IMPORTANT:
|
|
109
|
+
// check the ORIGINAL SPID, not merely "some server answers on the same
|
|
110
|
+
// socket". A wake/attach can replace the server inside this 2 s window;
|
|
111
|
+
// socket-liveness would then silently swallow the predecessor's death.
|
|
105
112
|
`sleep 2`,
|
|
106
|
-
`if
|
|
113
|
+
`if [ -n "$SPID" ] && kill -0 "$SPID" 2>/dev/null; then exit 0; fi`,
|
|
114
|
+
`if [ -z "$SPID" ] && '${o.tmuxBin}' -S '${o.sock}' has-session 2>/dev/null; then exit 0; fi`,
|
|
107
115
|
// The exit code still ATTRIBUTES the recorded death (raw wait_rc is kept):
|
|
108
116
|
// rc=0 → signaled-server-gone (channel signal or client-TERM, server died)
|
|
109
117
|
// rc≥128 → client-killed-server-gone (client took a non-TERM kill)
|