@agfpd/iapeer 0.4.122 → 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 +1 -1
- package/src/daemon/fleet.ts +15 -1
- package/src/daemon/mutewatch.ts +62 -3
package/package.json
CHANGED
package/src/daemon/fleet.ts
CHANGED
|
@@ -617,7 +617,7 @@ export function buildFleetHandler(opts: FleetHandlerOptions = {}): FleetHandler
|
|
|
617
617
|
// observations in; the snapshot below reads it. Reading is O(1): no file touched per request.
|
|
618
618
|
const activity = opts.activity ?? new ActivityBoard()
|
|
619
619
|
const activityOf = (personality: string, runtime: string): TurnActivity | undefined =>
|
|
620
|
-
activity.get(personality, runtime, Date.now())
|
|
620
|
+
structuralActivity(personality, runtime, env) ?? activity.get(personality, runtime, Date.now())
|
|
621
621
|
let opsPromise: Promise<FleetOps> | undefined
|
|
622
622
|
const getOps = (): Promise<FleetOps> => {
|
|
623
623
|
if (opts.ops) return Promise.resolve(opts.ops)
|
|
@@ -900,6 +900,20 @@ async function runCommand(
|
|
|
900
900
|
}
|
|
901
901
|
}
|
|
902
902
|
|
|
903
|
+
/**
|
|
904
|
+
* Activity of a STRUCTURAL runtime straight from its own state (docs/20: state.json `status` is the
|
|
905
|
+
* busy/idle source, not a transcript watch). busy/approval → working, idle → idle; anything else (or
|
|
906
|
+
* no private dir) → undefined, so the transcript turn-watch decides for claude/codex as before.
|
|
907
|
+
*/
|
|
908
|
+
export function structuralActivity(personality: string, runtime: string, env: NodeJS.ProcessEnv): TurnActivity | undefined {
|
|
909
|
+
const dir = endpointPrivateDir(runtime, personality, { env })
|
|
910
|
+
if (!dir) return undefined
|
|
911
|
+
const status = readOpencodeState(join(dir, 'state.json'))?.status
|
|
912
|
+
if (status === 'busy' || status === 'approval') return 'working'
|
|
913
|
+
if (status === 'idle') return 'idle'
|
|
914
|
+
return undefined
|
|
915
|
+
}
|
|
916
|
+
|
|
903
917
|
/** Serve the SSE event stream: optional replay of the newest N historical events,
|
|
904
918
|
* then live tail. At-least-once semantics (a reconnect may re-see events — `id`
|
|
905
919
|
* is the event's epoch-ms for client-side dedup); heartbeat comment every 15 s. */
|
package/src/daemon/mutewatch.ts
CHANGED
|
@@ -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) {
|