@agfpd/iapeer 0.4.123 → 0.4.124

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.4.123",
3
+ "version": "0.4.124",
4
4
  "description": "Run Claude Code and Codex CLI as local background agents that message each other, wake on demand, and share one memory.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
@@ -78,6 +78,8 @@ import { join } from 'node:path'
78
78
  import type { LifecycleConfig } from '../lifecycle/index.ts'
79
79
  import type { NoticeBoard } from './notices.ts'
80
80
  import { readSessionSlices } from './sessionfiles.ts'
81
+ import { endpointPrivateDir } from '../storage/index.ts'
82
+ import { readOpencodeState } from '../opencode/journal.ts'
81
83
 
82
84
  /** How much of a session file's tail is parsed. The evidence is always at the end. */
83
85
  const TAIL_BYTES = 256 * 1024
@@ -89,7 +91,7 @@ const CODEX_WALK_DEPTH = 3
89
91
 
90
92
  export interface MuteDetection {
91
93
  personality: string
92
- runtime: 'claude' | 'codex'
94
+ runtime: 'claude' | 'codex' | 'opencode'
93
95
  /** The runtime's OWN taxonomy value — claude's `error`, codex's rate_limit_reached_type. */
94
96
  errorType: string
95
97
  model?: string
@@ -405,6 +407,41 @@ export interface MuteWatchPaths {
405
407
  export interface ScanDeps extends MuteWatchPaths {
406
408
  /** personality ← cwd. Built from the registry by the composition point. */
407
409
  peerByCwd: (cwd: string) => string | undefined
410
+ /** OpenCode endpoints: personality → its journal (core-owned private dir; docs/20). Optional:
411
+ * absent → no opencode sweep (tests of the claude/codex paths). */
412
+ opencodeJournals?: () => Array<{ personality: string; events: string; model?: string }>
413
+ }
414
+
415
+ /**
416
+ * OpenCode (docs/20): the core plugin journals native `session.error` events unchanged. That IS the
417
+ * structural fact — the error's own `name` (APIError, ProviderAuthError, …) and status are the
418
+ * runtime's taxonomy, the message is verbatim. Read only events newer than the floor (by the journal
419
+ * line's own `time`, so an overlapping sweep re-reads the same occurrence without recounting).
420
+ */
421
+ export function parseOpencodeJournal(tail: string, floorMs: number): Omit<RawDetection, 'cwd'> | null {
422
+ let last: Omit<RawDetection, 'cwd'> | null = null
423
+ for (const line of tail.split('\n')) {
424
+ if (!line.includes('"session.error"')) continue
425
+ let e: { type?: string; time?: number; sessionID?: string | null; properties?: { error?: { name?: string; data?: { message?: string; statusCode?: number } } } }
426
+ try {
427
+ e = JSON.parse(line)
428
+ } catch {
429
+ continue
430
+ }
431
+ if (e.type !== 'session.error' || typeof e.time !== 'number' || e.time < floorMs) continue
432
+ const err = e.properties?.error
433
+ if (!err) continue
434
+ const name = err.name ?? 'UnknownError'
435
+ if (name === 'MessageAbortedError') continue // an interrupt the operator asked for, not a failure
436
+ const status = err.data?.statusCode
437
+ last = {
438
+ errorType: status ? `${name}:${status}` : name,
439
+ content: err.data?.message ?? name,
440
+ ...(e.sessionID ? { sessionId: e.sessionID } : {}),
441
+ atMs: e.time,
442
+ }
443
+ }
444
+ return last
408
445
  }
409
446
 
410
447
  /**
@@ -442,6 +479,19 @@ export function scanMuteEvents(deps: ScanDeps, sinceMs: number): MuteDetection[]
442
479
  const t = readSessionSlices(file, TAIL_BYTES)
443
480
  if (t) push(parseCodexRollout(t.tail, floor, t.head), 'codex')
444
481
  }
482
+ // opencode: the core-owned journal per endpoint (personality is known — no cwd mapping needed).
483
+ for (const j of deps.opencodeJournals?.() ?? []) {
484
+ let mt = 0
485
+ try {
486
+ mt = statSync(j.events).mtimeMs
487
+ } catch {
488
+ continue
489
+ }
490
+ if (mt < floor) continue
491
+ const t = readSessionSlices(j.events, TAIL_BYTES)
492
+ const d = t ? parseOpencodeJournal(t.tail, floor) : null
493
+ if (d) out.push({ ...d, ...(j.model ? { model: j.model } : {}), cwd: '', personality: j.personality, runtime: 'opencode' })
494
+ }
445
495
  return out
446
496
  }
447
497
 
@@ -490,7 +540,7 @@ export interface StartMuteWatchOptions {
490
540
  /** Override the runtime session roots (tests). Default: resolveMuteWatchPaths(env). */
491
541
  paths?: MuteWatchPaths
492
542
  /** Override the registry read (tests). Default: a lazy import of the CLI's listPeers. */
493
- listPeers?: () => Array<{ personality: string; cwd: string }>
543
+ listPeers?: () => Array<{ personality: string; cwd: string; runtimes?: string[] }>
494
544
  now?: () => number
495
545
  onError?: (err: unknown) => void
496
546
  }
@@ -516,7 +566,7 @@ export function startMuteWatch(cfg: LifecycleConfig, board: NoticeBoard, opts: S
516
566
  try {
517
567
  if (!listPeers) {
518
568
  const cli = await import('../cli/index.ts')
519
- listPeers = () => cli.listPeers({ env }).map(p => ({ personality: p.personality, cwd: p.cwd }))
569
+ listPeers = () => cli.listPeers({ env }).map(p => ({ personality: p.personality, cwd: p.cwd, runtimes: p.runtimes.map(r => r.runtime) }))
520
570
  }
521
571
  const rows = listPeers()
522
572
  // cwd → personality. Compared case-insensitively: macOS paths are case-insensitive and
@@ -530,6 +580,15 @@ export function startMuteWatch(cfg: LifecycleConfig, board: NoticeBoard, opts: S
530
580
  now,
531
581
  onError: opts.onError,
532
582
  peerByCwd: (cwd: string) => byCwd.get(cwd.replace(/\/+$/, '').toLowerCase()),
583
+ opencodeJournals: () =>
584
+ rows
585
+ .filter(r => (r.runtimes ?? []).includes('opencode'))
586
+ .flatMap(r => {
587
+ const dir = endpointPrivateDir('opencode', r.personality, { env })
588
+ if (!dir) return []
589
+ const model = readOpencodeState(join(dir, 'state.json'))?.model
590
+ return [{ personality: r.personality, events: join(dir, 'events.jsonl'), ...(model ? { model } : {}) }]
591
+ }),
533
592
  }
534
593
  since = muteWatchTick(deps, since).sinceMs
535
594
  } catch (e) {