@agfpd/iapeer 0.4.14 → 0.4.15
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/CHANGELOG.md +31 -2
- package/package.json +1 -1
- package/src/lifecycle/index.ts +59 -22
package/CHANGELOG.md
CHANGED
|
@@ -10,14 +10,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.4.15] - 2026-06-21
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Ephemeral idle-reap no longer kills a **working** session mid-turn. An armed ephemeral
|
|
18
|
+
worker (one that has sent its reply) was reaped after 20s of *transcript* silence, but the
|
|
19
|
+
transcript goes quiet during a long model generation — so a still-working worker was killed
|
|
20
|
+
mid-turn. The reap now folds in the pane-log (TUI render-stream) mtime, so "quiet" means the
|
|
21
|
+
turn truly ended: a working session is never reaped, while an idle one still reaps in ~20s
|
|
22
|
+
(keeping the ephemeral serial conveyor draining promptly).
|
|
23
|
+
|
|
24
|
+
## [0.4.14] - 2026-06-21
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Completed the bot_username cutover.** The telegram sender-guard (`hasTelegramPresence`)
|
|
29
|
+
now keys on `interfaces.telegram.bot_username` (the cutover's binding key) instead of the
|
|
30
|
+
legacy `bot`; the host registry was regenerated from the per-cwd source of truth, dropping
|
|
31
|
+
the stale `interfaces.telegram.bot` field across the fleet. telegram-runtime already
|
|
32
|
+
resolved on `bot_username`, so the foundation guard was the last consumer of `bot`.
|
|
33
|
+
- `recordDrift` (verify / reconcile) now compares the `interfaces` passport with an
|
|
34
|
+
order-insensitive canonical compare, so a source↔derived divergence is detected and healed
|
|
35
|
+
by `verify --fix` — the class that had let the derived registry silently keep a field
|
|
36
|
+
removed from the source of truth.
|
|
37
|
+
|
|
13
38
|
### Changed
|
|
14
39
|
|
|
15
40
|
- Fact-actualized the repo docs against the current code: pty-only (tmux references removed),
|
|
16
41
|
no canary/forensics in postmortem diagnostics, and the legacy `runtime`-mirror drop.
|
|
42
|
+
- Unified the README to the fleet documentation style.
|
|
17
43
|
|
|
18
44
|
### Added
|
|
19
45
|
|
|
20
|
-
- This changelog.
|
|
46
|
+
- This changelog (Keep a Changelog), shipped in the npm package.
|
|
47
|
+
- HD hero GIF in the README (1440×726 — sharper terminal text).
|
|
21
48
|
|
|
22
49
|
## [0.4.13] - 2026-06-20
|
|
23
50
|
|
|
@@ -206,7 +233,9 @@ Initial public release — the foundation core of the iapeer multi-agent ecosyst
|
|
|
206
233
|
- **Heterogeneous peers** — AI agents (Claude, Codex), humans (Telegram), and services
|
|
207
234
|
(timer = cron, watcher = event) are all first-class and addressed the same way.
|
|
208
235
|
|
|
209
|
-
[Unreleased]: https://github.com/agfpd/iapeer/compare/v0.4.
|
|
236
|
+
[Unreleased]: https://github.com/agfpd/iapeer/compare/v0.4.15...HEAD
|
|
237
|
+
[0.4.15]: https://github.com/agfpd/iapeer/compare/v0.4.14...v0.4.15
|
|
238
|
+
[0.4.14]: https://github.com/agfpd/iapeer/compare/v0.4.13...v0.4.14
|
|
210
239
|
[0.4.13]: https://github.com/agfpd/iapeer/compare/v0.4.12...v0.4.13
|
|
211
240
|
[0.4.12]: https://github.com/agfpd/iapeer/compare/v0.4.11...v0.4.12
|
|
212
241
|
[0.4.11]: https://github.com/agfpd/iapeer/compare/v0.4.10...v0.4.11
|
package/package.json
CHANGED
package/src/lifecycle/index.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// before any wake or reap. wakeOrSpawn refuses a launchd peer; superviseTick and
|
|
13
13
|
// sweepZombies skip it. Only daemon-owned (no-plist) peers are managed here.
|
|
14
14
|
|
|
15
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs'
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'fs'
|
|
16
16
|
import { homedir } from 'os'
|
|
17
17
|
import { join } from 'path'
|
|
18
18
|
import { spawnSync } from 'child_process'
|
|
@@ -74,20 +74,21 @@ export interface LifecycleConfig {
|
|
|
74
74
|
crashLoopWindowSecs: number
|
|
75
75
|
/** wake_policy:ephemeral M2 — the quiet window (seconds of activity-proxy silence)
|
|
76
76
|
* after which an ARMED ephemeral session (it has sent its outbound reply) is
|
|
77
|
-
* reaped.
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* reaped. Kept SHORT so the M3 serial conveyor drains the peer's next queued task
|
|
78
|
+
* promptly once its session is truly idle. "Quiet" here is BOTH the transcript proxy
|
|
79
|
+
* AND the pane-log (TUI render-stream) silent — the pane-log advances ~1s while the
|
|
80
|
+
* TUI renders a turn (generation OR a tool), so a still-working session is NEVER
|
|
81
|
+
* reaped mid-turn even though the transcript is momentarily quiet (the live incident
|
|
82
|
+
* this fixes: an armed worker generating its next step >20s was reaped at age=29s). */
|
|
81
83
|
ephemeralQuietSecs: number
|
|
82
84
|
/** wake_policy:ephemeral — the UNARMED idle bound (seconds): an ephemeral session
|
|
83
85
|
* that never armed (finished silently / lost its arm to a daemon-restart window)
|
|
84
|
-
* is reaped after this much
|
|
85
|
-
* ended silently without its final outbound stalled its M3
|
|
86
|
-
* generic idleSecs (1 h) — with serial drain that blocks the whole
|
|
87
|
-
* silent worker. Bound chosen ≫
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* is lost — ephemeral workers must emit activity (or their reply) within it. */
|
|
86
|
+
* is reaped after this much TRUE-idle silence (transcript AND pane-log both quiet).
|
|
87
|
+
* Live case: a worker that ended silently without its final outbound stalled its M3
|
|
88
|
+
* FIFO for the FULL generic idleSecs (1 h) — with serial drain that blocks the whole
|
|
89
|
+
* conveyor per silent worker. Bound chosen ≫ a brief no-output gap and ≪ idleSecs.
|
|
90
|
+
* A still-WORKING silent-tool worker keeps the pane-log fresh via the TUI render, so it
|
|
91
|
+
* is no longer reaped mid-task — only a genuinely idle/wedged session crosses this. */
|
|
91
92
|
ephemeralUnarmedIdleSecs: number
|
|
92
93
|
}
|
|
93
94
|
|
|
@@ -343,6 +344,21 @@ export function clearEphemeralArmed(cfg: LifecycleConfig, identity: string): voi
|
|
|
343
344
|
}
|
|
344
345
|
}
|
|
345
346
|
|
|
347
|
+
/** mtime (ms) of a hosted session's pane-log (`<logDir>/<identity>.log`) — the RAW TUI
|
|
348
|
+
* render byte-stream the supervisor appends for the session's whole life. It advances
|
|
349
|
+
* ~1s while the TUI is rendering a turn (the "esc to interrupt" elapsed counter ticks
|
|
350
|
+
* during BOTH model generation AND a tool run) and goes quiet at the prompt. So it is
|
|
351
|
+
* the truest active-turn liveness signal — the SAME mtime telegram-runtime reads for its
|
|
352
|
+
* typing/activity indicator — and unlike the transcript proxy it does NOT go quiet during
|
|
353
|
+
* a long model generation (no JSONL write until the message completes). null if absent. */
|
|
354
|
+
function paneLogMtimeMs(cfg: LifecycleConfig, identity: string): number | null {
|
|
355
|
+
try {
|
|
356
|
+
return statSync(join(cfg.logDir, `${identity}.log`)).mtimeMs
|
|
357
|
+
} catch {
|
|
358
|
+
return null
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
346
362
|
function deathsPath(cfg: LifecycleConfig, identity: string): string {
|
|
347
363
|
return join(cfg.stateDir, `${identity}.deaths`)
|
|
348
364
|
}
|
|
@@ -1147,6 +1163,9 @@ export interface SuperviseDeps {
|
|
|
1147
1163
|
/** Liveness seam (default: the real `sessionAlive` = pty-host OR tmux-fallback). Tests inject a
|
|
1148
1164
|
* fixed verdict so the idle path is exercised hermetically — no live tmux/pty session needed. */
|
|
1149
1165
|
sessionAlive?: (sock: string, identity: string) => boolean
|
|
1166
|
+
/** Pane-log mtime seam (default: the real pane-log file mtime). Tests inject a controlled value to
|
|
1167
|
+
* exercise the ephemeral active-turn vs idle distinction without a real pane-log on disk. */
|
|
1168
|
+
paneLogMtime?: (identity: string) => number | null
|
|
1150
1169
|
}
|
|
1151
1170
|
|
|
1152
1171
|
export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): SuperviseOutcome[] {
|
|
@@ -1154,6 +1173,7 @@ export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): S
|
|
|
1154
1173
|
const nowMs = deps.nowMs ?? Date.now()
|
|
1155
1174
|
const activityMtime = deps.newestActivityMtime ?? ((rt: string, c: string) => getAdapter(rt as Runtime).newestActivityMtime(c))
|
|
1156
1175
|
const isAlive = deps.sessionAlive ?? sessionAlive
|
|
1176
|
+
const paneLogMt = deps.paneLogMtime ?? ((identity: string) => paneLogMtimeMs(cfg, identity))
|
|
1157
1177
|
const verbose = superviseLogVerbose(env)
|
|
1158
1178
|
// Pane-log volume cap (launch/cmdlog.ts capPaneLogs): the per-identity pane-log
|
|
1159
1179
|
// (<logDir>/<identity>.log) is the RAW TUI byte stream pipe-pane/the supervisor
|
|
@@ -1241,17 +1261,31 @@ export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): S
|
|
|
1241
1261
|
} catch {
|
|
1242
1262
|
/* no adapter for this runtime yet → wokeAt baseline */
|
|
1243
1263
|
}
|
|
1264
|
+
const ephemeral = isEphemeralPeer(s.cwd)
|
|
1265
|
+
if (ephemeral) {
|
|
1266
|
+
// FOLD IN the pane-log (TUI render-stream) mtime for ephemeral peers. The transcript
|
|
1267
|
+
// proxy goes QUIET during a long model generation (no JSONL write until the message
|
|
1268
|
+
// completes), so the short ephemeral windows below mis-read mid-generation as "done"
|
|
1269
|
+
// and reap the worker MID-TURN (live incident 2026-06-20: Index reaped age=29s while
|
|
1270
|
+
// generating the next step after a tool_result). The pane-log advances ~1s while the
|
|
1271
|
+
// TUI renders a turn (model generation OR a tool run — the "esc to interrupt" elapsed
|
|
1272
|
+
// counter ticks) and goes quiet only at the prompt, so it is the truer active-turn
|
|
1273
|
+
// signal. max() means "quiet" iff BOTH are silent — it can only DELAY a reap, never
|
|
1274
|
+
// cause an earlier one, so the pipeline serial-drain (idle → quiet → reaped) is
|
|
1275
|
+
// preserved while a working session is never killed mid-turn.
|
|
1276
|
+
const plMt = paneLogMt(s.identity)
|
|
1277
|
+
if (plMt !== null) mt = Math.max(mt, plMt)
|
|
1278
|
+
}
|
|
1244
1279
|
const ageSecs = Math.floor((nowMs - mt) / 1000)
|
|
1245
1280
|
// wake_policy:ephemeral M2 — die-after-reply: an ARMED ephemeral session (the
|
|
1246
1281
|
// daemon routed its outbound reply) is reaped after a QUIET window, checked
|
|
1247
|
-
// BEFORE the idle branch (quiet ≪ idle). Quiet =
|
|
1248
|
-
// silent for ephemeralQuietSecs —
|
|
1249
|
-
//
|
|
1250
|
-
// NOT armed (still mid-task
|
|
1251
|
-
//
|
|
1252
|
-
// NO
|
|
1253
|
-
|
|
1254
|
-
if (isEphemeralPeer(s.cwd) && hasEphemeralArmed(cfg, s.identity) && ageSecs > cfg.ephemeralQuietSecs) {
|
|
1282
|
+
// BEFORE the idle branch (quiet ≪ idle). Quiet = transcript AND pane-log both
|
|
1283
|
+
// silent for ephemeralQuietSecs — a still-rendering turn keeps the pane-log fresh,
|
|
1284
|
+
// so the worker finishes its turn first; post-reply housekeeping also resets it.
|
|
1285
|
+
// NOT armed (still mid-task) → the ordinary idle bound below is its only reaper.
|
|
1286
|
+
// Deliberate, policy-driven death: NO .idle-reaped (an ephemeral peer never resumes)
|
|
1287
|
+
// and NO recordDeath (the crash-loop ring counts faults, not policy reaps).
|
|
1288
|
+
if (ephemeral && hasEphemeralArmed(cfg, s.identity) && ageSecs > cfg.ephemeralQuietSecs) {
|
|
1255
1289
|
killSession(sock, s.identity)
|
|
1256
1290
|
clearEphemeralArmed(cfg, s.identity)
|
|
1257
1291
|
removeSessionState(cfg, s.identity)
|
|
@@ -1274,8 +1308,11 @@ export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): S
|
|
|
1274
1308
|
// silent-finish path is `iapeer self-done` (arm without waking anyone —
|
|
1275
1309
|
// the invariant «нет пустых пробуждений» stays intact); this branch only
|
|
1276
1310
|
// bounds the damage when a worker does neither. Policy reap: NO .idle-reaped
|
|
1277
|
-
// (ephemeral never resumes), NO recordDeath (the ring counts faults).
|
|
1278
|
-
|
|
1311
|
+
// (ephemeral never resumes), NO recordDeath (the ring counts faults). The pane-log
|
|
1312
|
+
// fold-in above also covers this branch: a STILL-WORKING unarmed worker (a silent
|
|
1313
|
+
// tool, no reply yet) keeps the pane-log fresh via the TUI render, so it is no longer
|
|
1314
|
+
// reaped mid-task — only a genuinely idle/wedged unarmed session crosses this bound.
|
|
1315
|
+
if (ephemeral && ageSecs > cfg.ephemeralUnarmedIdleSecs) {
|
|
1279
1316
|
killSession(sock, s.identity)
|
|
1280
1317
|
clearEphemeralArmed(cfg, s.identity)
|
|
1281
1318
|
removeSessionState(cfg, s.identity)
|