@agfpd/iapeer 0.2.23 → 0.2.24

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.23",
3
+ "version": "0.2.24",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,7 @@ import {
17
17
  signalCanaryClean,
18
18
  } from './canary.ts'
19
19
  import { killSession } from '../lifecycle/index.ts'
20
+ import { teardownAlwaysOnSession } from './launchdRun.ts'
20
21
 
21
22
  const tmuxAvailable = spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0
22
23
 
@@ -92,7 +93,7 @@ describe.if(tmuxAvailable)('canary live (sandbox tmux servers)', () => {
92
93
  afterAll(() => {
93
94
  for (const sock of socks) {
94
95
  // teardown is DELIBERATE → signal each canary before killing its server
95
- for (const id of ['claude-canadirty', 'claude-canaclean', 'claude-canakill']) {
96
+ for (const id of ['claude-canadirty', 'claude-canaclean', 'claude-canakill', 'notifier-canatear']) {
96
97
  signalCanaryClean(sock, id)
97
98
  }
98
99
  spawnSync('tmux', ['-S', sock, 'kill-server'], { stdio: 'ignore' })
@@ -152,6 +153,32 @@ describe.if(tmuxAvailable)('canary live (sandbox tmux servers)', () => {
152
153
  20000,
153
154
  )
154
155
 
156
+ test(
157
+ 'teardownAlwaysOnSession (signal-exit of runAlwaysOn) kills session+server, canary stays silent',
158
+ async () => {
159
+ const identity = 'notifier-canatear'
160
+ const { sock, logDir } = bringUp(identity)
161
+ expect(ensureServerCanary({ identity, sock, exitLogDir: logDir })).toBe('spawned')
162
+ expect(await waitFor(() => canaryRunning(identity), 3000)).toBe(true)
163
+ await sleep(500)
164
+
165
+ teardownAlwaysOnSession(sock, identity) // the bootout/shutdown path
166
+ // the whole server must be gone (the poller dies WITH the watcher — грабля closed)
167
+ expect(
168
+ await waitFor(
169
+ () => spawnSync('tmux', ['-S', sock, 'has-session', '-t', identity], { stdio: 'ignore' }).status !== 0,
170
+ 3000,
171
+ ),
172
+ ).toBe(true)
173
+ expect(await waitFor(() => !canaryRunning(identity), 3000)).toBe(true)
174
+ await sleep(300)
175
+ expect(existsSync(exitLogPath(logDir)) && readFileSync(exitLogPath(logDir), 'utf8').includes('ev=server-exit')).toBe(
176
+ false,
177
+ )
178
+ },
179
+ 20000,
180
+ )
181
+
155
182
  test(
156
183
  'killSession (lifecycle clean reap) signals the canary before kill-server → no record',
157
184
  async () => {
@@ -23,6 +23,7 @@ import { buildProcessAddress, buildSocketPath } from '../core/socket.ts'
23
23
  import { peerLogsDir, pluginLogsDir } from '../storage/index.ts'
24
24
  import { readPeerProfile } from '../identity/index.ts'
25
25
  import { getAdapter, launch } from './index.ts'
26
+ import { signalCanaryClean } from './canary.ts'
26
27
  import type { LaunchConfig, LaunchSpec } from './types.ts'
27
28
 
28
29
  /** Block-watch poll cadence — seconds, deliberately NOT a tight loop (the session
@@ -39,6 +40,29 @@ function sessionAlive(sock: string, identity: string): boolean {
39
40
  return spawnSync('tmux', ['-S', sock, 'has-session', '-t', identity], { stdio: 'ignore' }).status === 0
40
41
  }
41
42
 
43
+ /**
44
+ * Tear down the always-on session WITH its tmux server (when this session was the
45
+ * last one) — the signal-exit counterpart of lifecycle.killSession, local to avoid
46
+ * a launch ⇆ lifecycle import. Canary-signaled first: this is a DELIBERATE stop.
47
+ *
48
+ * Closes the live грабля «bootout не убивает поллера» (boris 10.06, second
49
+ * strike): `launchctl bootout` TERMs THIS watcher process, but the detached tmux
50
+ * server — and the runtime poller inside it — survived holding STALE in-memory
51
+ * state (e.g. a notifier trigger's old target after a same-id replace), so a
52
+ * plain bootout+bootstrap was NOT a real restart. With the teardown, the session
53
+ * dies with its watcher: bootout = full stop, bootstrap = fresh bring-up that
54
+ * re-reads durable state. `iapeer stop` (bootout + killSession) is unchanged —
55
+ * both paths now converge on the same end state.
56
+ */
57
+ export function teardownAlwaysOnSession(sock: string, identity: string): void {
58
+ signalCanaryClean(sock, identity)
59
+ spawnSync('tmux', ['-S', sock, 'kill-session', '-t', identity], { stdio: 'ignore' })
60
+ const ls = spawnSync('tmux', ['-S', sock, 'list-sessions', '-F', '#{session_name}'], { encoding: 'utf8' })
61
+ if (!(ls.stdout ?? '').trim()) {
62
+ spawnSync('tmux', ['-S', sock, 'kill-server'], { stdio: 'ignore' })
63
+ }
64
+ }
65
+
42
66
  /**
43
67
  * Build the always-on LaunchSpec for an infra peer, reading intelligence from the
44
68
  * local peer-profile.json. launchd sets WorkingDirectory = peer cwd, so that file
@@ -159,6 +183,12 @@ export async function runAlwaysOn(personality: string, runtime: string, cwd: str
159
183
  })
160
184
  interrupt = null
161
185
  }
186
+ // Signal-initiated exit (bootout / shutdown / kickstart -k) tears the session
187
+ // down WITH this watcher — without it the detached tmux poller outlived bootout
188
+ // holding stale in-memory state (см. teardownAlwaysOnSession). A natural session
189
+ // death (stop=false) skips this: there is nothing to tear down, exit 0 →
190
+ // KeepAlive respawns a fresh bring-up.
191
+ if (stop) teardownAlwaysOnSession(sock, identity)
162
192
  return 0
163
193
  }
164
194