@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.2.50",
3
+ "version": "0.2.51",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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(`has-session 2>/dev/null; then exit 0`) // server alive → nothing to record
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
  })
@@ -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 LIVENESS: alive → exit
23
- // silently (a lost canary is re-armed and logged by the supervise retrofit
24
- // within a tick); dead with nobody having dismissed us → the death is real
25
- // and unclaimed → ONE logfmt line `ev=server-exit` into exits.log (the
26
- // per-peer death-cause home, next to pane-died's `ev=session-exit`) + a
27
- // forensics snapshot (vm_stat / swap / top-RSS ps / fresh DiagnosticReports)
28
- // captured within seconds — the evidence the 60 s supervise tick can never
29
- // recover ("системных следов ноль" was the recurring investigation outcome).
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 dismissed us →
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 '${o.tmuxBin}' -S '${o.sock}' has-session 2>/dev/null; then exit 0; fi`,
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)