@agfpd/iapeer 0.2.54 → 0.2.56

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.54",
3
+ "version": "0.2.56",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -77,7 +77,12 @@ describe('listTools', () => {
77
77
  const tools = listTools(readPeersIndex()) as any[]
78
78
  expect(tools.map(t => t.name).sort()).toEqual(['send_to_peer'])
79
79
  expect(tools.every(t => t._meta['anthropic/alwaysLoad'] === true)).toBe(true)
80
- expect(tools.find(t => t.name === 'send_to_peer').description).toContain('arthur')
80
+ const sendTool = tools.find(t => t.name === 'send_to_peer')
81
+ expect(sendTool.description).toContain('arthur')
82
+ expect(sendTool.description).toContain('wakes it and delivers there')
83
+ expect(sendTool.description).toContain('parallel to the default runtime')
84
+ expect(sendTool.inputSchema.properties.runtime.description).toContain('wakes it warm-on-demand')
85
+ expect(sendTool.inputSchema.properties.runtime.description).toContain('parallel session')
81
86
  })
82
87
  })
83
88
 
@@ -98,7 +98,7 @@ export function buildSendDescription(index: PeersIndex): string {
98
98
  }
99
99
  lines.push(
100
100
  '',
101
- "Use `personality`, not a runtime-prefixed identity. Set `runtime` only for a current-send override. Descriptions identify the peer's role; runtimes identify reachable delivery endpoints; intelligence identifies the nature of the peer (artificial / natural / absent). Inbound peer messages arrive as <iap from-personality=\"...\" from-runtime=\"...\" from-intelligence=\"...\"> blocks. The topic attribute is optional.",
101
+ "Use `personality`, not a runtime-prefixed identity. Set `runtime` to target a specific declared runtime for this send. If that runtime is warm-asleep, the daemon wakes it and delivers there; use this to intentionally bring up a second session of the same peer in parallel to the default runtime (for example codex alongside claude). Descriptions identify the peer's role; runtimes identify reachable delivery endpoints; intelligence identifies the nature of the peer (artificial / natural / absent). Inbound peer messages arrive as <iap from-personality=\"...\" from-runtime=\"...\" from-intelligence=\"...\"> blocks. The topic attribute is optional.",
102
102
  )
103
103
  return lines.join('\n')
104
104
  }
@@ -122,7 +122,7 @@ export function listTools(index: PeersIndex): unknown[] {
122
122
  runtime: {
123
123
  type: 'string',
124
124
  description:
125
- 'Optional delivery runtime override for this send. Runtime ids are short channel names such as claude, codex, telegram.',
125
+ 'Optional target runtime for this send. If the specified runtime is declared and wakeable but asleep, the daemon wakes it warm-on-demand and delivers there; choosing a non-default runtime can intentionally start that peer\'s parallel session. Runtime ids are short channel names such as claude, codex, telegram.',
126
126
  pattern: RUNTIME_RE_SOURCE,
127
127
  },
128
128
  message: {
@@ -87,7 +87,6 @@ describe('boot first-message delivery (load-buffer + bracketed paste)', () => {
87
87
  sockDir: root,
88
88
  bootDeadlineSecs: 10,
89
89
  readyGateSecs: 8,
90
- maxAgeSecs: 120,
91
90
  logDir: join(root, 'logs'),
92
91
  }
93
92
  try {
@@ -340,14 +340,18 @@ export const launch: LaunchFn = async (
340
340
  const paneLog = `${cfg.logDir}/${identity}.log`
341
341
  tmux(sock, 'pipe-pane', '-t', identity, `cat >> ${shellQuote(paneLog)}`)
342
342
 
343
- // (4) Session self-TTL: a tmux server-side timer kills the session at max-age,
344
- // independent of any supervisor (bounds a zombie even if no sweep runs).
345
- // SKIPPED for an always-on (infra) session — launchd KeepAlive owns its
346
- // lifecycle, so a self-kill timer would fight it and tear down the endpoint.
347
- if (!cfg.alwaysOn) {
348
- tmux(sock, 'run-shell', '-b', '-d', String(cfg.maxAgeSecs),
349
- `tmux -S ${shellQuote(sock)} kill-session -t ${shellQuote(identity)}`)
350
- }
343
+ // (4) [REMOVED 0.2.55] Session self-TTL. Was a tmux server-side
344
+ // `run-shell -b -d <maxAgeSecs>` → `kill-session`, intended to bound a zombie
345
+ // even if no supervise sweep runs. It was a BLIND wall-clock kill armed ONCE
346
+ // at birth and never re-armed, so it guillotined HEALTHY long-lived sessions
347
+ // at exactly birth+maxAge (4 h) — the recurrent boris/iapeer "mystery deaths".
348
+ // By construction any session that survives to maxAge is active: idle ones are
349
+ // already idle-reaped at idleSecs (1 h) by the activity-aware supervisor — so
350
+ // the blind kill could ONLY ever destroy live work (its sole "useful" case,
351
+ // daemon dead the whole interval, leaves an inert orphan nothing can deliver
352
+ // to, self-healed by idle-reap on daemon restart). The activity-aware idle-reap
353
+ // (superviseTick) is now the SOLE lifecycle bound. See ADR «миграция …» / vault
354
+ // note «Четырёхчасовой TTL-backstop …». (alwaysOn infra never armed it.)
351
355
 
352
356
  // (5) Router runtime (telegram): no TUI input surface — there is no boot dialog
353
357
  // and no ready marker to gate on. The process is up; return READY.
@@ -15,7 +15,6 @@ const launchCfg: LaunchConfig = {
15
15
  sockDir: '/tmp',
16
16
  bootDeadlineSecs: 1,
17
17
  readyGateSecs: 1,
18
- maxAgeSecs: 1,
19
18
  logDir: '/tmp/iapeer-test-logs',
20
19
  }
21
20
  function spec(over: Partial<LaunchSpec> = {}): LaunchSpec {
@@ -125,7 +125,6 @@ export async function runAlwaysOn(personality: string, runtime: string, cwd: str
125
125
  sockDir,
126
126
  bootDeadlineSecs: 30,
127
127
  readyGateSecs: 30,
128
- maxAgeSecs: 0, // unused — alwaysOn skips the self-TTL
129
128
  // GLOBAL infra logs (Фаза §8): ~/.iapeer/logs/<personality>/ — match the plist's
130
129
  // stdout/stderr dir (installAlwaysOnPlist), not per-peer <cwd>/.iapeer/logs/.
131
130
  logDir: peerLogsDir(personality, { env }),
@@ -47,7 +47,6 @@ describe('launch creates a missing socket dir', () => {
47
47
  sockDir,
48
48
  bootDeadlineSecs: 1,
49
49
  readyGateSecs: 1,
50
- maxAgeSecs: 0,
51
50
  logDir: join(root, 'logs'),
52
51
  alwaysOn: true,
53
52
  }
@@ -296,7 +296,6 @@ export interface LaunchConfig extends LaunchAdapterConfig {
296
296
  sockDir: string
297
297
  bootDeadlineSecs: number
298
298
  readyGateSecs: number
299
- maxAgeSecs: number
300
299
  /** Log dir for pipe-pane output. */
301
300
  logDir: string
302
301
  /**
@@ -312,10 +311,12 @@ export interface LaunchConfig extends LaunchAdapterConfig {
312
311
  exitLogDir?: string
313
312
  env?: NodeJS.ProcessEnv
314
313
  /**
315
- * Always-on bring-up (infra runtimes held by launchd KeepAlive): SKIP the
316
- * session self-TTL so the session is not auto-killed at maxAgeSecs. The TTL is
317
- * a warm-on-demand zombie bound; an always-on session's lifecycle is owned by
318
- * launchd (KeepAlive respawns it), so the self-kill timer must not fire.
314
+ * Always-on bring-up (infra runtimes held by launchd KeepAlive). Marks the
315
+ * session as infra-owned: launchd (KeepAlive) owns its lifecycle, the daemon is
316
+ * READ-ONLY for it (H4), and it opts out of the warm-on-demand `-v` tmux
317
+ * command-log (a rare-relaunch infra session has no rotation point and is not the
318
+ * dying warm class). Historically it also skipped the session self-TTL; that TTL
319
+ * was removed in 0.2.55 (see launch step 4), so the exemption is now moot.
319
320
  */
320
321
  alwaysOn?: boolean
321
322
  }
@@ -329,7 +330,7 @@ export interface LaunchResult {
329
330
 
330
331
  /**
331
332
  * Bring up ONE session: pre-clean stale tmux server → tmux new-session -d with
332
- * adapter.buildArgv → pipe-pane → session self-TTL → boot (answer dialogs via
333
+ * adapter.buildArgv → pipe-pane → boot (answer dialogs via
333
334
  * adapter, wait for adapter.isInputReady, deliver the first message via
334
335
  * load-buffer + bracketed paste — the same byte-path as warm delivery) →
335
336
  * ready-gate (adapter.newestActivityMtime strictly advances).
@@ -68,7 +68,6 @@ export interface LifecycleConfig {
68
68
  bootDeadlineSecs: number
69
69
  readyGateSecs: number
70
70
  idleSecs: number
71
- maxAgeSecs: number
72
71
  /** Crash-loop guard: refuse to (re)launch after this many deaths within the window. */
73
72
  crashLoopMax: number
74
73
  /** Crash-loop guard: the sliding window (seconds) the death count is measured over. */
@@ -109,7 +108,6 @@ export function loadLifecycleConfig(env: NodeJS.ProcessEnv = process.env): Lifec
109
108
  bootDeadlineSecs: num(env.IAPEER_BOOT_DEADLINE_SECS, 240),
110
109
  readyGateSecs: num(env.IAPEER_READY_GATE_SECS, 120),
111
110
  idleSecs: num(env.IAPEER_IDLE_SECS, 3600),
112
- maxAgeSecs: num(env.IAPEER_MAX_AGE_SECS, 14400),
113
111
  crashLoopMax: num(env.IAPEER_CRASHLOOP_MAX, 3),
114
112
  crashLoopWindowSecs: num(env.IAPEER_CRASHLOOP_WINDOW_SECS, 300),
115
113
  ephemeralQuietSecs: num(env.IAPEER_EPHEMERAL_QUIET_SECS, 20),
@@ -988,7 +986,6 @@ export async function wakeOrSpawn(args: WakeArgs, deps: WakeDeps = {}): Promise<
988
986
  sockDir: cfg.sockDir,
989
987
  bootDeadlineSecs: cfg.bootDeadlineSecs,
990
988
  readyGateSecs: cfg.readyGateSecs,
991
- maxAgeSecs: cfg.maxAgeSecs,
992
989
  logDir: cfg.logDir,
993
990
  // Exit-cause log → next to lifecycle.log (~/.iapeer/logs/iapeer), where the
994
991
  // investigator already looks: a self-death now leaves `exits.log` with the
@@ -1007,13 +1004,15 @@ export async function wakeOrSpawn(args: WakeArgs, deps: WakeDeps = {}): Promise<
1007
1004
  return { status: 'FAILED', woke: false, runtime, reason: result.reason }
1008
1005
  }
1009
1006
  // The session is up and the message delivered. Recording supervise-state must not
1010
- // turn a successful wake into a failure (audit #18): on a write hiccup the session is
1011
- // still bounded by its tmux self-TTL — log loudly rather than throw past a live spawn.
1007
+ // turn a successful wake into a failure (audit #18): log loudly rather than throw
1008
+ // past a live spawn. NB since 0.2.55 (self-TTL removed) the session-state file IS
1009
+ // the only thing that enrolls this session into the activity-aware idle-reap — a
1010
+ // failed write leaves it UNSUPERVISED with no TTL backstop, hence the loud WARN.
1012
1011
  try {
1013
1012
  writeSessionState(cfg, { identity, runtime, personality: args.personality, cwd, wokeAt: Date.now() })
1014
1013
  } catch (e) {
1015
1014
  process.stderr.write(
1016
- `[iapeer] WARN session-state write failed for ${identity} — session is live + TTL-bounded but not idle-reap-supervised: ${e instanceof Error ? e.message : String(e)}\n`,
1015
+ `[iapeer] WARN session-state write failed for ${identity} — session is live but UNSUPERVISED (not enrolled in idle-reap; self-TTL removed): ${e instanceof Error ? e.message : String(e)}\n`,
1017
1016
  )
1018
1017
  }
1019
1018
  // A NEWLY launched session starts UNARMED by definition (no outbound yet) — clear
@@ -129,7 +129,6 @@ describe('withWakeLock', () => {
129
129
  bootDeadlineSecs: 1,
130
130
  readyGateSecs: 1,
131
131
  idleSecs: 1,
132
- maxAgeSecs: 1,
133
132
  }) as LifecycleConfig
134
133
 
135
134
  test('two concurrent locks on the same identity run strictly serialized', async () => {
@@ -194,7 +193,6 @@ describe('superviseTick H4 guard', () => {
194
193
  bootDeadlineSecs: 1,
195
194
  readyGateSecs: 1,
196
195
  idleSecs: 1,
197
- maxAgeSecs: 1,
198
196
  }) as LifecycleConfig
199
197
 
200
198
  function writeState(personality: string): string {