@agfpd/iapeer 0.2.90 → 0.2.92
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/cli/index.ts +52 -0
- package/src/lifecycle/index.ts +38 -0
- package/src/shadow/render.ts +8 -1
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
purgeIdentityState,
|
|
42
42
|
removeSessionState,
|
|
43
43
|
setEphemeralArmed,
|
|
44
|
+
setFreshNext,
|
|
44
45
|
setIdleReaped,
|
|
45
46
|
setNewEager,
|
|
46
47
|
setStopped,
|
|
@@ -292,6 +293,41 @@ export function startPeer(personality: string, runtime: string | undefined, opts
|
|
|
292
293
|
return out
|
|
293
294
|
}
|
|
294
295
|
|
|
296
|
+
export interface RefreshOutcome {
|
|
297
|
+
personality: string
|
|
298
|
+
runtime: Runtime
|
|
299
|
+
action: 'refresh-armed' | 'skipped-non-agentic'
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Runtimes that consume the layered system prompt (doctrine/fragments) — the only ones a soft-reload
|
|
303
|
+
* affects. Routers (notifier/telegram) and absent runtimes carry no doctrine. */
|
|
304
|
+
const AGENTIC_RUNTIMES: ReadonlySet<string> = new Set(['claude', 'codex'])
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* refresh <peer> [runtime] (or --all): arm a LAZY soft-reload. Each agentic (claude/codex) runtime of the
|
|
308
|
+
* peer comes up FRESH on its NEXT natural wake — re-reading doctrine/fragments from disk — WITHOUT killing
|
|
309
|
+
* the live session and WITHOUT eager-relaunch / burst-wake. Non-agentic runtimes (notifier/telegram/absent)
|
|
310
|
+
* are skipped (no doctrine). H4-safe: writes a `.fresh-next` marker only — never wakes / reaps / kills, so
|
|
311
|
+
* it is inert for launchd-managed peers (their lifecycle is KeepAlive's, resolveWakeMode is not their path).
|
|
312
|
+
* The lazy counterpart to `iapeer new` (hard kill) and self-fresh/.new-eager (eager relaunch + burst wake).
|
|
313
|
+
*/
|
|
314
|
+
export function refreshPeer(personality: string, runtime: string | undefined, opts: CliEnvOptions = {}): RefreshOutcome[] {
|
|
315
|
+
const env = opts.env ?? process.env
|
|
316
|
+
const cfg = loadLifecycleConfig(env)
|
|
317
|
+
const peer = findPeer(readPeersIndex({ env }), personality)
|
|
318
|
+
if (!peer) throw new Error(`peer "${personality}" is not registered`)
|
|
319
|
+
const out: RefreshOutcome[] = []
|
|
320
|
+
for (const rt of targetRuntimes(peer, runtime)) {
|
|
321
|
+
if (!AGENTIC_RUNTIMES.has(rt)) {
|
|
322
|
+
out.push({ personality, runtime: rt, action: 'skipped-non-agentic' })
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
setFreshNext(cfg, buildProcessAddress(rt, personality))
|
|
326
|
+
out.push({ personality, runtime: rt, action: 'refresh-armed' })
|
|
327
|
+
}
|
|
328
|
+
return out
|
|
329
|
+
}
|
|
330
|
+
|
|
295
331
|
export interface AddRuntimeOutcome {
|
|
296
332
|
personality: string
|
|
297
333
|
action: 'added' | 'already' | 'skipped-infra-peer' | 'failed'
|
|
@@ -923,6 +959,8 @@ const USAGE = `usage: iapeer <verb> [args]
|
|
|
923
959
|
until \`start\`; always-on (notifier/telegram): launchctl bootout. --all = every registered peer
|
|
924
960
|
start <peer> [runtime] | --all warm peer: clear the stop-flag — wakeable again; does NOT launch a session (the daemon brings
|
|
925
961
|
it up on the peer's first message); always-on: launchctl bootstrap. --all = every registered peer
|
|
962
|
+
refresh <peer> [runtime] | --all LAZY soft-reload: agentic peer comes up FRESH (re-reads doctrine) on its NEXT natural wake — no
|
|
963
|
+
kill, no burst-wake; non-agentic runtimes skipped. --all = whole fleet. (eager: \`new\`/self-fresh)
|
|
926
964
|
remove <peer> [--force] delete a peer's registry record (locked writer); refuses a LIVE peer unless --force
|
|
927
965
|
send <target> (--message <text> | --message-file <f|->) [--from <id>] [--attachment <p>]… [--topic <t>] manual IAP send (fallback)
|
|
928
966
|
<runtime> launch the cwd's peer FRESH; on a TTY drop into it (like attach), else detached
|
|
@@ -1383,6 +1421,20 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1383
1421
|
for (const o of outcomes) out(`${o.personality} (${o.runtime}): ${o.action}${o.reason ? ` — ${o.reason}` : ''}\n`)
|
|
1384
1422
|
return outcomes.some(o => o.action === 'refused-foreign-launchd') ? 1 : 0
|
|
1385
1423
|
}
|
|
1424
|
+
case 'refresh': {
|
|
1425
|
+
// LAZY soft-reload (fleet doctrine refresh): arm `.fresh-next` so each agentic peer comes up FRESH
|
|
1426
|
+
// on its NEXT natural wake (re-reads doctrine/fragments) — no kill, no eager relaunch, no burst-wake.
|
|
1427
|
+
// --all marks the whole registered fleet. H4-safe (marker only); non-agentic runtimes are skipped.
|
|
1428
|
+
const peers = flags.all === true
|
|
1429
|
+
? readPeersIndex({ env }).peers.map(p => p.personality)
|
|
1430
|
+
: positionals[0]
|
|
1431
|
+
? [positionals[0]]
|
|
1432
|
+
: null
|
|
1433
|
+
if (!peers) return usage(errOut)
|
|
1434
|
+
const outcomes = peers.flatMap(p => refreshPeer(p, flags.all === true ? undefined : positionals[1], { env }))
|
|
1435
|
+
for (const o of outcomes) out(`${o.personality} (${o.runtime}): ${o.action}\n`)
|
|
1436
|
+
return 0
|
|
1437
|
+
}
|
|
1386
1438
|
case 'remove': {
|
|
1387
1439
|
// Reap a registry record through the locked writer (the operator path over
|
|
1388
1440
|
// registry.removePeer). Idempotent on an absent peer (exit 0). Refuses a LIVE
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -292,6 +292,32 @@ export function clearNewEager(cfg: LifecycleConfig, identity: string): void {
|
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
function freshNextPath(cfg: LifecycleConfig, identity: string): string {
|
|
296
|
+
return join(cfg.stateDir, `${identity}.fresh-next`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** True iff the identity carries a LAZY soft-reload mark (→ FRESH on its next natural wake,
|
|
300
|
+
* re-reading doctrine/fragments from disk). Unlike .new-eager this is NOT eager — no relaunch,
|
|
301
|
+
* no burst-wake; it only changes the resume decision when the peer next wakes on its own. */
|
|
302
|
+
export function hasFreshNext(cfg: LifecycleConfig, identity: string): boolean {
|
|
303
|
+
return existsSync(freshNextPath(cfg, identity))
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Set the lazy soft-reload mark (the `iapeer refresh` verb does this — no kill, no relaunch). */
|
|
307
|
+
export function setFreshNext(cfg: LifecycleConfig, identity: string): void {
|
|
308
|
+
mkdirSync(cfg.stateDir, { recursive: true, mode: 0o700 })
|
|
309
|
+
writeFileSync(freshNextPath(cfg, identity), `${new Date().toISOString()}\n`, { mode: 0o600 })
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Consume the lazy soft-reload mark (resolveWakeMode does this on the fresh decision). */
|
|
313
|
+
export function clearFreshNext(cfg: LifecycleConfig, identity: string): void {
|
|
314
|
+
try {
|
|
315
|
+
rmSync(freshNextPath(cfg, identity), { force: true })
|
|
316
|
+
} catch {
|
|
317
|
+
/* already gone */
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
295
321
|
function ephemeralArmedPath(cfg: LifecycleConfig, identity: string): string {
|
|
296
322
|
return join(cfg.stateDir, `${identity}.ephemeral-armed`)
|
|
297
323
|
}
|
|
@@ -564,6 +590,18 @@ export function resolveWakeMode(
|
|
|
564
590
|
if (!r.ok) return { resume: false, failReason: r.reason ?? 'resume requested but nothing to resume', cause: 'attach-nothing-to-resume' }
|
|
565
591
|
return { resume: true, resumeRef: r.ref, cause: 'attach' }
|
|
566
592
|
}
|
|
593
|
+
// 3-soft-reload (LAZY fleet refresh, `iapeer refresh`): a `.fresh-next` mark means "come up FRESH on the
|
|
594
|
+
// next natural wake" — re-read doctrine/fragments from disk WITHOUT killing the live session or burst-
|
|
595
|
+
// waking the fleet. Checked at the TOP of branch 3, BEFORE the type/death-cause logic, so it overrides
|
|
596
|
+
// resume for ALL peer types — including telegram-fronted (human-conversational) ones, which otherwise
|
|
597
|
+
// RESUME and chew stale doctrine session-after-session until an explicit /new. Consume-on-fire: clear
|
|
598
|
+
// .fresh-next AND .idle-reaped (mirror the ephemeral hygiene below — else a stale .idle-reaped would make
|
|
599
|
+
// a later CRASH read as a clean park in 3b → RESUME a killed context, exactly the 3a hazard).
|
|
600
|
+
if (hasFreshNext(cfg, identity)) {
|
|
601
|
+
clearFreshNext(cfg, identity)
|
|
602
|
+
clearIdleReaped(cfg, identity)
|
|
603
|
+
return { resume: false, cause: 'soft-reload' }
|
|
604
|
+
}
|
|
567
605
|
// 3. default (a message woke a dead/asleep peer): decide by the death cause.
|
|
568
606
|
// 3-ephemeral (M1): a stateless worker ALWAYS wakes fresh on delivery — never resume,
|
|
569
607
|
// regardless of death cause or topic. Its clean-window-per-task is the whole point.
|
package/src/shadow/render.ts
CHANGED
|
@@ -60,7 +60,14 @@ export function composerOccupancyFromModel(term: Terminal, cols: number, rows: n
|
|
|
60
60
|
const l = b.getLine(py)!
|
|
61
61
|
for (let x = px + 1; x < cols; x++) {
|
|
62
62
|
const c = l.getCell(x); if ((c?.getChars() || ' ').trim() === '') continue
|
|
63
|
-
|
|
63
|
+
// OCCUPIED iff a real human glyph after the prompt: DEFAULT foreground AND non-dim. Verified on BOTH
|
|
64
|
+
// runtimes — typed input renders at the terminal default fg (isFgDefault, fg=-1) and non-dim. Everything
|
|
65
|
+
// else after the prompt is runtime chrome / ghost and must be ignored: claude's dim/palette-246
|
|
66
|
+
// placeholder; codex's DIM default-fg suggestion ("Find and fix a bug in @filename", varies) AND its
|
|
67
|
+
// TRUECOLOR status hint ("<model> <effort> · <cwd>"). The old check (`!dim && !palette-246`) ghosted the
|
|
68
|
+
// dim suggestion but MISSED codex's truecolor status (non-dim, non-palette) → false OCCUPIED → spurious
|
|
69
|
+
// delivery queue ("queuedBy: composer") whenever a human was attached to a codex session.
|
|
70
|
+
if (!c!.isDim() && c!.isFgDefault()) return true
|
|
64
71
|
}
|
|
65
72
|
return false
|
|
66
73
|
}
|