@agfpd/iapeer 0.1.2 → 0.2.1

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.
@@ -1,25 +1,32 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
- import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
3
3
  import { tmpdir } from 'os'
4
4
  import { join } from 'path'
5
5
  import {
6
6
  attachPeer,
7
- clearNewMark,
7
+ clearNewEager,
8
8
  clearStopped,
9
9
  composeFirstMessage,
10
+ countRecentDeaths,
10
11
  folderLaunch,
11
- hasNewMark,
12
+ hasIdleReaped,
13
+ hasNewEager,
12
14
  isLaunchdManaged,
13
15
  isStopped,
14
16
  lastActiveRuntime,
15
17
  loadLifecycleConfig,
18
+ readDeaths,
19
+ readTopic,
20
+ recordDeath,
16
21
  resolveWakeMode,
17
22
  resolveWakeRuntime,
18
- setNewMark,
23
+ setIdleReaped,
24
+ setNewEager,
19
25
  setStopped,
20
26
  superviseTick,
21
27
  wakeOrSpawn,
22
28
  withWakeLock,
29
+ writeTopic,
23
30
  type LifecycleConfig,
24
31
  } from './index.ts'
25
32
  import { upsertPeer, type PeerRecord } from '../registry/index.ts'
@@ -72,10 +79,12 @@ describe('resolveWakeRuntime (H5)', () => {
72
79
  // ─────────────────────────────────────────────────────────────────────────────
73
80
 
74
81
  describe('isLaunchdManaged (H4 detector, live read-only)', () => {
75
- test('a launchd-managed peer (boris has com.iapeer.boris.plist) → true', () => {
76
- // The live host has com.iapeer.boris.plist — the daemon must treat boris as
77
- // READ-ONLY (never wake/reap). This proves the detector fires on the fleet.
78
- expect(isLaunchdManaged('boris')).toBe(true)
82
+ test('a launchd-managed peer (timer has com.iapeer.timer.plist) → true', () => {
83
+ // Post foundation-migration the always-on INFRA peers (timer/watcher/arthur) keep
84
+ // their com.iapeer.<p>.plist; the daemon must treat them READ-ONLY (never
85
+ // wake/reap). This proves the detector fires on the live fleet. (boris and the
86
+ // agent peers became warm-on-demand — plist relocated — so they are NOT managed.)
87
+ expect(isLaunchdManaged('timer')).toBe(true)
79
88
  })
80
89
 
81
90
  test('a made-up daemon-owned name (no plist) → false', () => {
@@ -167,6 +176,7 @@ describe('superviseTick H4 guard', () => {
167
176
  sockDir: '/tmp',
168
177
  stateDir,
169
178
  logDir: stateDir,
179
+ eventLogDir: stateDir, // isolate the decision log into the temp dir (no real-root leak)
170
180
  bootDeadlineSecs: 1,
171
181
  readyGateSecs: 1,
172
182
  idleSecs: 1,
@@ -195,11 +205,17 @@ describe('superviseTick H4 guard', () => {
195
205
  })
196
206
 
197
207
  test('a no-plist peer with a dead session → reaped-gone, state removed', () => {
208
+ const c = cfg()
198
209
  const id = writeState('iapeer-supgone') // no plist, no live tmux session
199
- const out = superviseTick(cfg(), { env: env(), nowMs: Date.now() })
210
+ const out = superviseTick(c, { env: env(), nowMs: Date.now() })
200
211
  const o = out.find(x => x.identity === id)
201
212
  expect(o?.action).toBe('reaped-gone')
202
213
  expect(existsSync(join(stateDir, `${id}.session`))).toBe(false)
214
+ // the decision leaves a DURABLE trace line (the observability contract) — and it
215
+ // lands in the SANDBOXED eventLogDir, never the real ~/.iapeer.
216
+ const logged = readFileSync(join(c.eventLogDir, 'lifecycle.log'), 'utf8')
217
+ expect(logged).toContain(`ev=supervise identity=${id} action=reaped-gone`)
218
+ expect(logged).toContain('outcome=fresh-next-msg')
203
219
  })
204
220
 
205
221
  test('empty state dir → no outcomes', () => {
@@ -338,54 +354,119 @@ describe('C2 initial_prompt (composeFirstMessage)', () => {
338
354
  })
339
355
 
340
356
  // ─────────────────────────────────────────────────────────────────────────────
341
- // C3a + C4a resolveWakeMode (resume-vs-fresh; the contract-divergence fix)
357
+ // resolveWakeMode TARGET redesign (daemon decides fresh-vs-resume by DEATH CAUSE
358
+ // = .idle-reaped marker, plus peer-type/topic; NO agent-dropped fresh mark).
342
359
  // ─────────────────────────────────────────────────────────────────────────────
343
360
 
344
- describe('resolveWakeMode (C3a default-resume + C4a /new-mark)', () => {
361
+ /** A temp cwd with a peer-profile; interfaces.telegram present → human-conversational. */
362
+ function profileCwd(human: boolean): string {
363
+ const cwd = mkdtempSync(join(tmpdir(), 'iapeer-wm-cwd-'))
364
+ mkdirSync(join(cwd, '.iapeer'), { recursive: true })
365
+ writeFileSync(
366
+ join(cwd, '.iapeer', 'peer-profile.json'),
367
+ JSON.stringify({
368
+ personality: 'p',
369
+ runtime: 'claude',
370
+ runtimes: ['claude'],
371
+ intelligence: human ? 'natural' : 'artificial',
372
+ ...(human ? { interfaces: { telegram: { user_id: 1 } } } : {}),
373
+ }),
374
+ )
375
+ return cwd
376
+ }
377
+
378
+ describe('resolveWakeMode (TARGET: death-cause + peer-type/topic)', () => {
345
379
  let stateDir: string
380
+ let cwds: string[]
346
381
  beforeEach(() => {
347
382
  stateDir = mkdtempSync(join(tmpdir(), 'iapeer-wakemode-'))
383
+ cwds = []
348
384
  })
349
385
  afterEach(() => {
350
386
  rmSync(stateDir, { recursive: true, force: true })
387
+ for (const c of cwds) rmSync(c, { recursive: true, force: true })
351
388
  })
352
389
  const cfg = () => ({ stateDir } as LifecycleConfig)
390
+ const cwd = (human = false) => {
391
+ const c = profileCwd(human)
392
+ cwds.push(c)
393
+ return c
394
+ }
353
395
  const hasTranscript = () => ({ ok: true, ref: 'uuid-1' })
354
396
  const noTranscript = () => ({ ok: false, reason: 'no transcript to resume' })
355
397
 
356
- test('DEFAULT (undefined) + transcript exists RESUME (the warm-asleep contract fix)', () => {
357
- expect(resolveWakeMode(cfg(), 'claude-p', '/cwd', undefined, hasTranscript)).toEqual({ resume: true, resumeRef: 'uuid-1' })
398
+ // ── branch 1/2: explicit fresh / explicit resume (unchanged) ────────────────
399
+ test('argsResume=false (folder-launch) FRESH', () => {
400
+ expect(resolveWakeMode(cfg(), 'claude-p', cwd(), false, hasTranscript)).toEqual({ resume: false, cause: 'folder-launch' })
358
401
  })
359
- test('DEFAULT + NO transcript → FRESH (first-ever launch, not an error)', () => {
360
- expect(resolveWakeMode(cfg(), 'claude-p', '/cwd', undefined, noTranscript)).toEqual({ resume: false })
402
+ test('argsResume=true (attach) + transcript → RESUME', () => {
403
+ expect(resolveWakeMode(cfg(), 'claude-p', cwd(), true, hasTranscript)).toEqual({ resume: true, resumeRef: 'uuid-1', cause: 'attach' })
361
404
  })
362
- test('explicit resume=true + nothing to resume → FAIL-LOUD (failReason, no silent fresh)', () => {
363
- const m = resolveWakeMode(cfg(), 'claude-p', '/cwd', true, noTranscript)
405
+ test('argsResume=true + nothing to resume → FAIL-LOUD (failReason, no silent fresh)', () => {
406
+ const m = resolveWakeMode(cfg(), 'claude-p', cwd(), true, noTranscript)
364
407
  expect(m.resume).toBe(false)
365
408
  expect(m.failReason).toMatch(/nothing to resume|no transcript/)
366
409
  })
367
- test('explicit resume=false → FRESH', () => {
368
- expect(resolveWakeMode(cfg(), 'claude-p', '/cwd', false, hasTranscript)).toEqual({ resume: false })
410
+
411
+ // ── branch 3a: default + NOT idle-reaped it died on its own → FRESH ────────
412
+ test('DEFAULT + NOT idle-reaped (crash/self-close) → FRESH even when a transcript exists', () => {
413
+ // INVERSION of the old polarity: absence of the daemon's idle-reaped marker = died
414
+ // on its own = clean fresh, NOT a resume of a possibly-broken context.
415
+ expect(resolveWakeMode(cfg(), 'claude-p', cwd(), undefined, hasTranscript)).toEqual({ resume: false, cause: 'crash-or-self-close' })
416
+ })
417
+
418
+ // ── branch 3b: default + idle-reaped → resume-eligible, CONSUME the marker ───
419
+ test('DEFAULT + idle-reaped + human-conversational (interfaces.telegram) → RESUME, marker consumed', () => {
420
+ const c = cfg()
421
+ setIdleReaped(c, 'claude-p')
422
+ const human = cwd(true)
423
+ expect(resolveWakeMode(c, 'claude-p', human, undefined, hasTranscript)).toEqual({ resume: true, resumeRef: 'uuid-1', cause: 'idle-reaped-human' })
424
+ expect(hasIdleReaped(c, 'claude-p')).toBe(false) // consumed
369
425
  })
370
- test('/new-mark present FRESH and CONSUMES the mark (even if a transcript exists)', () => {
426
+ test('DEFAULT + idle-reaped + executor + NO incoming topic RESUME (continue the work)', () => {
371
427
  const c = cfg()
372
- setNewMark(c, 'claude-p')
373
- expect(hasNewMark(c, 'claude-p')).toBe(true)
374
- expect(resolveWakeMode(c, 'claude-p', '/cwd', undefined, hasTranscript)).toEqual({ resume: false })
375
- expect(hasNewMark(c, 'claude-p')).toBe(false) // consumed
428
+ setIdleReaped(c, 'claude-p')
429
+ expect(resolveWakeMode(c, 'claude-p', cwd(false), undefined, hasTranscript)).toEqual({ resume: true, resumeRef: 'uuid-1', cause: 'idle-reaped-resume' })
430
+ })
431
+ test('DEFAULT + idle-reaped + executor + SAME topic → RESUME', () => {
432
+ const c = cfg()
433
+ setIdleReaped(c, 'claude-p')
434
+ writeTopic(c, 'claude-p', 'deploy')
435
+ expect(resolveWakeMode(c, 'claude-p', cwd(false), undefined, hasTranscript, 'deploy')).toEqual({ resume: true, resumeRef: 'uuid-1', cause: 'idle-reaped-resume' })
436
+ })
437
+ test('DEFAULT + idle-reaped + executor + DIFFERENT topic → FRESH (new work), marker consumed', () => {
438
+ const c = cfg()
439
+ setIdleReaped(c, 'claude-p')
440
+ writeTopic(c, 'claude-p', 'deploy')
441
+ expect(resolveWakeMode(c, 'claude-p', cwd(false), undefined, hasTranscript, 'unrelated-bug')).toEqual({ resume: false, cause: 'idle-reaped-new-topic' })
442
+ expect(hasIdleReaped(c, 'claude-p')).toBe(false) // consumed even on the fresh executor branch
376
443
  })
377
444
  })
378
445
 
379
- describe('C4a /new-mark round-trip', () => {
446
+ describe('idle-reaped marker round-trip', () => {
380
447
  test('set/has/clear', () => {
381
- const stateDir = mkdtempSync(join(tmpdir(), 'iapeer-newmark-'))
448
+ const stateDir = mkdtempSync(join(tmpdir(), 'iapeer-idlereap-'))
382
449
  const cfg = { stateDir } as LifecycleConfig
383
450
  try {
384
- expect(hasNewMark(cfg, 'claude-y')).toBe(false)
385
- setNewMark(cfg, 'claude-y')
386
- expect(hasNewMark(cfg, 'claude-y')).toBe(true)
387
- clearNewMark(cfg, 'claude-y')
388
- expect(hasNewMark(cfg, 'claude-y')).toBe(false)
451
+ expect(hasIdleReaped(cfg, 'claude-y')).toBe(false)
452
+ setIdleReaped(cfg, 'claude-y')
453
+ expect(hasIdleReaped(cfg, 'claude-y')).toBe(true)
454
+ } finally {
455
+ rmSync(stateDir, { recursive: true, force: true })
456
+ }
457
+ })
458
+ })
459
+
460
+ describe('new-eager marker round-trip', () => {
461
+ test('set/has/clear', () => {
462
+ const stateDir = mkdtempSync(join(tmpdir(), 'iapeer-neweager-'))
463
+ const cfg = { stateDir } as LifecycleConfig
464
+ try {
465
+ expect(hasNewEager(cfg, 'claude-y')).toBe(false)
466
+ setNewEager(cfg, 'claude-y')
467
+ expect(hasNewEager(cfg, 'claude-y')).toBe(true)
468
+ clearNewEager(cfg, 'claude-y')
469
+ expect(hasNewEager(cfg, 'claude-y')).toBe(false)
389
470
  } finally {
390
471
  rmSync(stateDir, { recursive: true, force: true })
391
472
  }
@@ -393,55 +474,54 @@ describe('C4a /new-mark round-trip', () => {
393
474
  })
394
475
 
395
476
  // ─────────────────────────────────────────────────────────────────────────────
396
- // C4beager fresh re-launch detection (superviseTick flags a dead +/new session)
477
+ // superviseTickdeath-cause accounting (TARGET redesign)
478
+ // • idle-reap is the ONLY place .idle-reaped is written
479
+ // • a crash/self-close death writes NO marker (lazy fresh on next message)
480
+ // • a dead session carrying .new-eager → needs-eager-fresh (mark LEFT for relaunch)
481
+ // • every dead session records a death (crash-loop accounting)
397
482
  // ─────────────────────────────────────────────────────────────────────────────
398
483
 
399
- describe('C4b eager fresh re-launch (superviseTick detection)', () => {
400
- test('a DEAD session carrying a /new-mark needs-eager-fresh (mark LEFT for relaunch)', () => {
401
- const root = mkdtempSync(join(tmpdir(), 'iapeer-c4b-root-'))
402
- const laDir = mkdtempSync(join(tmpdir(), 'iapeer-c4b-la-')) // empty → not launchd-managed
484
+ describe('superviseTick death-cause accounting (TARGET)', () => {
485
+ function deadSessionEnv(personality: string): { env: NodeJS.ProcessEnv; cfg: LifecycleConfig; root: string; laDir: string } {
486
+ const root = mkdtempSync(join(tmpdir(), 'iapeer-sup-tgt-root-'))
487
+ const laDir = mkdtempSync(join(tmpdir(), 'iapeer-sup-tgt-la-')) // empty → not launchd-managed
488
+ const env = { ...process.env, IAPEER_ROOT: root, IAPEER_LAUNCHAGENTS_DIR: laDir, IAPEER_SOCK_DIR: join(root, 'socks') }
489
+ const cfg = loadLifecycleConfig(env)
490
+ mkdirSync(cfg.stateDir, { recursive: true })
491
+ writeFileSync(
492
+ join(cfg.stateDir, `claude-${personality}.session`),
493
+ JSON.stringify({ identity: `claude-${personality}`, runtime: 'claude', personality, cwd: `/tmp/${personality}`, wokeAt: Date.now() }),
494
+ )
495
+ return { env, cfg, root, laDir }
496
+ }
497
+
498
+ test('a DEAD session carrying .new-eager → needs-eager-fresh (mark LEFT for relaunch), death recorded', () => {
499
+ const { env, cfg, root, laDir } = deadSessionEnv('z')
403
500
  try {
404
- const env = {
405
- ...process.env,
406
- IAPEER_ROOT: root,
407
- IAPEER_LAUNCHAGENTS_DIR: laDir,
408
- IAPEER_SOCK_DIR: join(root, 'socks'), // isolated, no live session here → dead
409
- }
410
- const cfg = loadLifecycleConfig(env)
411
- mkdirSync(cfg.stateDir, { recursive: true })
412
- // a session-state for a peer whose session is NOT live (dead)
413
- writeFileSync(
414
- join(cfg.stateDir, 'claude-z.session'),
415
- JSON.stringify({ identity: 'claude-z', runtime: 'claude', personality: 'z', cwd: '/tmp/z', wokeAt: Date.now() }),
416
- )
417
- setNewMark(cfg, 'claude-z')
501
+ setNewEager(cfg, 'claude-z')
418
502
  const out = superviseTick(cfg, { env })
419
503
  const o = out.find(x => x.identity === 'claude-z')
420
504
  expect(o?.action).toBe('needs-eager-fresh')
421
505
  expect(o?.personality).toBe('z')
422
506
  expect(o?.runtime).toBe('claude')
423
- // the session-state is removed, but the /new-mark is LEFT for the eager
424
- // relaunch's resolveWakeMode to consume (so the relaunch resolves to fresh).
425
- expect(hasNewMark(cfg, 'claude-z')).toBe(true)
507
+ // the eager mark is LEFT for processEagerRelaunches to consume
508
+ expect(hasNewEager(cfg, 'claude-z')).toBe(true)
509
+ // every dead session records a death for the crash-loop ring
510
+ expect(readDeaths(cfg, 'claude-z').length).toBe(1)
426
511
  } finally {
427
512
  rmSync(root, { recursive: true, force: true })
428
513
  rmSync(laDir, { recursive: true, force: true })
429
514
  }
430
515
  })
431
516
 
432
- test('a DEAD session with NO /new-mark → reaped-gone (not eager)', () => {
433
- const root = mkdtempSync(join(tmpdir(), 'iapeer-c4b2-root-'))
434
- const laDir = mkdtempSync(join(tmpdir(), 'iapeer-c4b2-la-'))
517
+ test('a DEAD session with NO .new-eager → reaped-gone, NO .idle-reaped written (crash leaves no marker)', () => {
518
+ const { env, cfg, root, laDir } = deadSessionEnv('w')
435
519
  try {
436
- const env = { ...process.env, IAPEER_ROOT: root, IAPEER_LAUNCHAGENTS_DIR: laDir, IAPEER_SOCK_DIR: join(root, 'socks') }
437
- const cfg = loadLifecycleConfig(env)
438
- mkdirSync(cfg.stateDir, { recursive: true })
439
- writeFileSync(
440
- join(cfg.stateDir, 'claude-w.session'),
441
- JSON.stringify({ identity: 'claude-w', runtime: 'claude', personality: 'w', cwd: '/tmp/w', wokeAt: Date.now() }),
442
- )
443
520
  const out = superviseTick(cfg, { env })
444
521
  expect(out.find(x => x.identity === 'claude-w')?.action).toBe('reaped-gone')
522
+ // a crash/self-close is NOT daemon-initiated → no idle-reaped marker → next wake FRESH
523
+ expect(hasIdleReaped(cfg, 'claude-w')).toBe(false)
524
+ expect(readDeaths(cfg, 'claude-w').length).toBe(1)
445
525
  } finally {
446
526
  rmSync(root, { recursive: true, force: true })
447
527
  rmSync(laDir, { recursive: true, force: true })
@@ -449,6 +529,71 @@ describe('C4b eager fresh re-launch (superviseTick detection)', () => {
449
529
  })
450
530
  })
451
531
 
532
+ // ─────────────────────────────────────────────────────────────────────────────
533
+ // Crash-loop guard — wakeOrSpawn refuses to (re)launch after N deaths in the window
534
+ // ─────────────────────────────────────────────────────────────────────────────
535
+
536
+ describe('crash-loop guard', () => {
537
+ test('countRecentDeaths windows correctly; recordDeath rings', () => {
538
+ const stateDir = mkdtempSync(join(tmpdir(), 'iapeer-deaths-'))
539
+ const cfg = { stateDir } as LifecycleConfig
540
+ try {
541
+ const now = 1_000_000
542
+ recordDeath(cfg, 'claude-d', now - 400_000) // outside a 300s window
543
+ recordDeath(cfg, 'claude-d', now - 10_000)
544
+ recordDeath(cfg, 'claude-d', now - 5_000)
545
+ expect(countRecentDeaths(cfg, 'claude-d', 300, now)).toBe(2)
546
+ expect(countRecentDeaths(cfg, 'claude-d', 600, now)).toBe(3)
547
+ } finally {
548
+ rmSync(stateDir, { recursive: true, force: true })
549
+ }
550
+ })
551
+
552
+ test('wakeOrSpawn REFUSES after crashLoopMax deaths within the window (FAILED, no launch)', async () => {
553
+ const root = mkdtempSync(join(tmpdir(), 'iapeer-clg-root-'))
554
+ const laDir = mkdtempSync(join(tmpdir(), 'iapeer-clg-la-')) // empty → not launchd-managed
555
+ const peerCwd = mkdtempSync(join(tmpdir(), 'iapeer-clg-cwd-')) // REAL cwd so the cwd-existence check passes and the guard is what fires
556
+ try {
557
+ await upsertPeer(
558
+ { personality: 'clg', runtime: 'claude', cwd: peerCwd, intelligence: 'artificial' },
559
+ { rootDir: root },
560
+ )
561
+ const env = { ...process.env, IAPEER_ROOT: root, IAPEER_LAUNCHAGENTS_DIR: laDir, IAPEER_SOCK_DIR: join(root, 'socks'), IAPEER_CRASHLOOP_MAX: '3', IAPEER_CRASHLOOP_WINDOW_SECS: '300' }
562
+ const cfg = loadLifecycleConfig(env)
563
+ const now = Date.now()
564
+ recordDeath(cfg, 'claude-clg', now)
565
+ recordDeath(cfg, 'claude-clg', now)
566
+ recordDeath(cfg, 'claude-clg', now)
567
+ const r = await wakeOrSpawn({ personality: 'clg', runtime: 'claude', task: 'must not launch' }, { env })
568
+ expect(r.status).toBe('FAILED')
569
+ expect(r.woke).toBe(false)
570
+ expect(r.reason).toMatch(/crash-loop guard/)
571
+ } finally {
572
+ rmSync(root, { recursive: true, force: true })
573
+ rmSync(laDir, { recursive: true, force: true })
574
+ rmSync(peerCwd, { recursive: true, force: true })
575
+ }
576
+ })
577
+ })
578
+
579
+ // ─────────────────────────────────────────────────────────────────────────────
580
+ // .topic — executor discriminator round-trip
581
+ // ─────────────────────────────────────────────────────────────────────────────
582
+
583
+ describe('topic marker round-trip', () => {
584
+ test('writeTopic/readTopic', () => {
585
+ const stateDir = mkdtempSync(join(tmpdir(), 'iapeer-topic-'))
586
+ const cfg = { stateDir } as LifecycleConfig
587
+ try {
588
+ expect(readTopic(cfg, 'claude-t')).toBe('')
589
+ writeTopic(cfg, 'claude-t', 'deploy-pipeline')
590
+ expect(readTopic(cfg, 'claude-t')).toBe('deploy-pipeline')
591
+ } finally {
592
+ rmSync(stateDir, { recursive: true, force: true })
593
+ }
594
+ })
595
+ })
596
+
452
597
  // ─────────────────────────────────────────────────────────────────────────────
453
598
  // Ф-D launch / attach — operator verbs (error paths; success paths are live-verified)
454
599
  // ─────────────────────────────────────────────────────────────────────────────
@@ -3,6 +3,7 @@ import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'fs'
3
3
  import { tmpdir } from 'os'
4
4
  import { join } from 'path'
5
5
  import {
6
+ clampDescription,
6
7
  findPeer,
7
8
  readPeersIndex,
8
9
  removePeer,
@@ -10,7 +11,7 @@ import {
10
11
  withPeersLock,
11
12
  type PeerRecord,
12
13
  } from './index.ts'
13
- import { defaultIntelligenceForRuntime, type Intelligence } from '../core/constants.ts'
14
+ import { MAX_DESCRIPTION_LEN, defaultIntelligenceForRuntime, type Intelligence } from '../core/constants.ts'
14
15
  import { writeFileAtomic, resolvePeersPaths } from '../storage/index.ts'
15
16
 
16
17
  let root: string
@@ -398,3 +399,34 @@ describe('Companion fix — withPeersLock fail-closed sandbox isolation', () =>
398
399
  expect(out).toBe('ok')
399
400
  })
400
401
  })
402
+
403
+ // ─────────────────────────────────────────────────────────────────────────────
404
+ // clampDescription boundary — the limit was raised 250 → 450 so self-documenting
405
+ // API-peer descriptions (notifier timer/watcher, ~408 chars) survive intact. The
406
+ // boundary is exact: length == MAX passes untouched, length == MAX+1 truncates.
407
+ // ─────────────────────────────────────────────────────────────────────────────
408
+
409
+ describe('clampDescription — MAX_DESCRIPTION_LEN boundary (450)', () => {
410
+ test('the limit is 450', () => {
411
+ expect(MAX_DESCRIPTION_LEN).toBe(450)
412
+ })
413
+ test('a 450-char description passes through untouched', () => {
414
+ const at = 'x'.repeat(450)
415
+ const r = clampDescription(at)
416
+ expect(r.truncated).toBe(false)
417
+ expect(r.description).toBe(at)
418
+ expect(r.description.length).toBe(450)
419
+ })
420
+ test('a 451-char description is truncated to 450', () => {
421
+ const over = 'y'.repeat(451)
422
+ const r = clampDescription(over)
423
+ expect(r.truncated).toBe(true)
424
+ expect(r.description.length).toBe(450)
425
+ expect(r.description).toBe('y'.repeat(450))
426
+ })
427
+ test('upsertPeer persists a full 450-char description (no clamp at the boundary)', async () => {
428
+ const desc = 'z'.repeat(450)
429
+ await upsertPeer({ personality: 'verbose', runtime: 'claude', cwd: '/tmp/verbose', description: desc }, opts())
430
+ expect(findPeer(readPeersIndex(opts()), 'verbose')!.description).toBe(desc)
431
+ })
432
+ })