@agfpd/iapeer 0.4.13 → 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 ADDED
@@ -0,0 +1,254 @@
1
+ # Changelog
2
+
3
+ All notable changes to **@agfpd/iapeer** are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ > The public repository begins at the **Initial public release (0.2.98, 2026-06-18)**.
9
+ > Versions before 0.2.98 predate the public repo (private development) and are not itemized here.
10
+
11
+ ## [Unreleased]
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
+
38
+ ### Changed
39
+
40
+ - Fact-actualized the repo docs against the current code: pty-only (tmux references removed),
41
+ no canary/forensics in postmortem diagnostics, and the legacy `runtime`-mirror drop.
42
+ - Unified the README to the fleet documentation style.
43
+
44
+ ### Added
45
+
46
+ - This changelog (Keep a Changelog), shipped in the npm package.
47
+ - HD hero GIF in the README (1440×726 — sharper terminal text).
48
+
49
+ ## [0.4.13] - 2026-06-20
50
+
51
+ ### Fixed
52
+
53
+ - `verify --fix` now strips the legacy `runtime` mirror from **every** profile that still
54
+ carries it, not only the ones that triggered a warning — completes the fleet-wide cleanup.
55
+
56
+ ## [0.4.12] - 2026-06-20
57
+
58
+ ### Fixed
59
+
60
+ - `migrateProfileRuntimeField` strips any legacy `runtime` field, so `verify --fix` cleans
61
+ the whole fleet.
62
+
63
+ ## [0.4.11] - 2026-06-20
64
+
65
+ ### Changed
66
+
67
+ - Local peer profiles no longer write the legacy `runtime` mirror — `default_runtime` is the
68
+ single field (Phase-3, local half). A read-fallback still understands older profiles that
69
+ carry only `runtime`.
70
+
71
+ ## [0.4.10] - 2026-06-20
72
+
73
+ ### Changed
74
+
75
+ - The registry (`peers-profiles.json`) no longer writes the legacy `runtime` mirror
76
+ (Phase-3, registry half).
77
+
78
+ ### Removed
79
+
80
+ - Two proven-dead internal symbols (audit safe-cuts).
81
+
82
+ ## [0.4.9] - 2026-06-20
83
+
84
+ ### Removed
85
+
86
+ - **tmux removed entirely — the foundation is now pty-only.** The tmux launch / delivery /
87
+ occupancy paths and the retired tmux→pty burn-in observer are gone; session hosting runs
88
+ through the pty supervisor (which owns the pty, the pane-log, and the delivery socket).
89
+ `render.ts` is kept as a shared leaf.
90
+
91
+ ## [0.4.8] - 2026-06-20
92
+
93
+ ### Fixed
94
+
95
+ - The supervisor self-heals a pane-log that was unlinked out from under a live session
96
+ (Defect 2).
97
+
98
+ ## [0.4.7] - 2026-06-20
99
+
100
+ ### Fixed
101
+
102
+ - An unreadable hosted pane-log is no longer treated as composer-busy — fixes an IAP
103
+ delivery stall.
104
+
105
+ ### Added
106
+
107
+ - Ping-pong hero demo GIF in the README.
108
+
109
+ ## [0.4.6] - 2026-06-19
110
+
111
+ ### Fixed
112
+
113
+ - Unified omitted-runtime resolution across `new` / `attach` / `compact`.
114
+
115
+ ## [0.4.5] - 2026-06-19
116
+
117
+ ### Fixed
118
+
119
+ - `init` declares all installed agentic runtimes (parity with `create`).
120
+
121
+ ### Changed
122
+
123
+ - README: moved "What makes it different" above Quick start; polished the Bypass Permissions
124
+ security disclosure.
125
+
126
+ ## [0.4.4] - 2026-06-19
127
+
128
+ ### Fixed
129
+
130
+ - Clear the virgin-Claude bypass-accept + project-MCP first-run dialogs (unblocks a
131
+ fresh-machine boot).
132
+ - Release scripts push explicitly with `git push origin main --follow-tags`.
133
+
134
+ ### Changed
135
+
136
+ - Alias namespace `/alias-*` → `/alias_*` (Telegram slash-command constraint).
137
+
138
+ ### Added
139
+
140
+ - Clean-install first-run dialog reference in docs/07 (EN + RU).
141
+
142
+ ## [0.4.3] - 2026-06-19
143
+
144
+ ### Added
145
+
146
+ - FU13: targeted CLI argument errors — no more generic help-wall dump.
147
+
148
+ ### Fixed
149
+
150
+ - onboard: marketplace add no longer shows a bare red "failed" on a transient first run.
151
+
152
+ ### Changed
153
+
154
+ - docs/07: note the one-time double-run when upgrading from a pre-cascade (<0.4.2) version.
155
+
156
+ ## [0.4.2] - 2026-06-19
157
+
158
+ ### Added
159
+
160
+ - **FU12: `iapeer update` cascades the whole stack** — foundation + every installed runtime +
161
+ the memory provider in one command. The foundation goes first and is health-checked; a hard
162
+ foundation failure aborts the cascade.
163
+
164
+ ## [0.4.1] - 2026-06-19
165
+
166
+ ### Added
167
+
168
+ - Per-package ecosystem docs scaffolded to a stable host path `~/.iapeer/docs/<pkg>/`, with a
169
+ host-context docs pointer in the system prompt (FU6).
170
+ - Global host-doctrine stub `~/.iapeer/IAPEER.md` scaffolded on install (FU11).
171
+ - CI-status + dynamic license badges in the README (FU10).
172
+ - A no-empty-acks instruction on the agent tool surface.
173
+
174
+ ### Changed
175
+
176
+ - Polish batch: onboard summary ANSI, advisory install commands, help/list formatting, the
177
+ create-runtime prompt.
178
+ - Dropped `bot_username` from the telegram-interface example and fixtures.
179
+
180
+ ## [0.4.0] - 2026-06-18
181
+
182
+ ### Added
183
+
184
+ - `iapeer uninstall` — symmetric, namespace-safe foundation removal (S4).
185
+ - Onboard wizard hardening (S1–S3): host-runtime detection threading `--runtime` to memory
186
+ init, the notifier scheduler and Telegram as default steps, visual polish with honest
187
+ slow-step labels.
188
+
189
+ ### Fixed
190
+
191
+ - install: exact-pin cache-bust + keychain heads-up.
192
+
193
+ ## [0.3.0] - 2026-06-18
194
+
195
+ ### Added
196
+
197
+ - **Public one-line installer** (`curl -fsSL … | sh`) with GitHub Pages hosting — `install.sh`
198
+ is deployed from `main` so the served script is identical to source.
199
+ - **Onboard wizard (Ink TUI)** — first slice behind an opt-in flag, then feature-complete and
200
+ default; `install.sh` stops launching onboard directly.
201
+
202
+ ### Changed
203
+
204
+ - README documents both install paths (one-liner + manual).
205
+
206
+ ## [0.2.99] - 2026-06-18
207
+
208
+ ### Added
209
+
210
+ - Ship `docs/` in the npm package (excluding `docs/internals`).
211
+
212
+ ### Changed
213
+
214
+ - README: humans also take part in the terminal via `iapeer attach`.
215
+
216
+ ## [0.2.98] - 2026-06-18
217
+
218
+ Initial public release — the foundation core of the iapeer multi-agent ecosystem.
219
+
220
+ ### Added
221
+
222
+ - **Data layer** — identity (`<runtime>-<personality>`), registry (`peers-profiles.json`,
223
+ single-writer), storage (the `~/.iapeer` layout with atomic writes), codec (the IAP envelope).
224
+ - **Always-on router daemon** — a canonical HTTP-MCP Streamable router exposing the single
225
+ agent tool `send_to_peer`; dual-listen — a `0600` unix socket for same-host local callers and
226
+ a TCP loopback for real MCP clients.
227
+ - **Warm-on-demand lifecycle** — wake-on-miss, idle-reap, zombie-sweep, fresh-vs-resume by
228
+ death cause, a crash-loop guard, and a durable decision log.
229
+ - **Launch primitive + runtime adapters** — Claude, Codex, Telegram, and notifier; layered
230
+ system-prompt composition; launchd plist rendering.
231
+ - **Install / onboard chain** and the `iapeer` CLI (create, init, list, verify, status, send,
232
+ attach, update, and more).
233
+ - **Heterogeneous peers** — AI agents (Claude, Codex), humans (Telegram), and services
234
+ (timer = cron, watcher = event) are all first-class and addressed the same way.
235
+
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
239
+ [0.4.13]: https://github.com/agfpd/iapeer/compare/v0.4.12...v0.4.13
240
+ [0.4.12]: https://github.com/agfpd/iapeer/compare/v0.4.11...v0.4.12
241
+ [0.4.11]: https://github.com/agfpd/iapeer/compare/v0.4.10...v0.4.11
242
+ [0.4.10]: https://github.com/agfpd/iapeer/compare/v0.4.9...v0.4.10
243
+ [0.4.9]: https://github.com/agfpd/iapeer/compare/v0.4.8...v0.4.9
244
+ [0.4.8]: https://github.com/agfpd/iapeer/compare/v0.4.7...v0.4.8
245
+ [0.4.7]: https://github.com/agfpd/iapeer/compare/v0.4.6...v0.4.7
246
+ [0.4.6]: https://github.com/agfpd/iapeer/compare/v0.4.5...v0.4.6
247
+ [0.4.5]: https://github.com/agfpd/iapeer/compare/v0.4.4...v0.4.5
248
+ [0.4.4]: https://github.com/agfpd/iapeer/compare/v0.4.3...v0.4.4
249
+ [0.4.3]: https://github.com/agfpd/iapeer/compare/v0.4.2...v0.4.3
250
+ [0.4.2]: https://github.com/agfpd/iapeer/compare/v0.4.1...v0.4.2
251
+ [0.4.1]: https://github.com/agfpd/iapeer/compare/v0.4.0...v0.4.1
252
+ [0.4.0]: https://github.com/agfpd/iapeer/compare/v0.3.0...v0.4.0
253
+ [0.3.0]: https://github.com/agfpd/iapeer/compare/v0.2.99...v0.3.0
254
+ [0.2.99]: https://github.com/agfpd/iapeer/releases/tag/v0.2.99
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # iapeer
2
2
 
3
- **Host-level core for a team of AI agents on one machine.**
3
+ **The foundation for a team of AI agents on one machine.**
4
4
 
5
5
  [![CI](https://github.com/agfpd/iapeer/actions/workflows/ci.yml/badge.svg)](https://github.com/agfpd/iapeer/actions/workflows/ci.yml)
6
6
  [![npm](https://img.shields.io/npm/v/@agfpd/iapeer)](https://www.npmjs.com/package/@agfpd/iapeer)
7
7
  [![license](https://img.shields.io/npm/l/@agfpd/iapeer)](./LICENSE)
8
8
  [![platform](https://img.shields.io/badge/platform-macOS-lightgrey)](#quick-start)
9
9
 
10
- iapeer keeps a team of AI agents, humans, and services on one host **always reachable — every one of them a single message away.** Each participant is a **peer**: a Claude Code or Codex CLI agent, a person on Telegram, or a scheduler/watcher service. Any agent reaches any other with one call — `send_to_peer(<name>, <text>)` — and a peer that is asleep is still reachable: the daemon wakes it and delivers. Agents on Claude and on Codex — different AI runtimes from different vendors — talk as equal peers; humans take part over Telegram, or directly in the terminal — attached to a peer's live session with `iapeer attach`.
10
+ iapeer keeps a team of AI agents, humans, and services on one host always reachable, every one a single message away. Each participant is a **peer**: a Claude Code or Codex CLI agent, a person on Telegram, or a scheduler/watcher service. Any agent reaches any other with one call, `send_to_peer(<name>, <text>)`; if the recipient is asleep, the daemon wakes it and delivers.
11
+
12
+ Agents on Claude and on Codex are different AI runtimes from different vendors, yet they talk as equal peers. Humans take part over Telegram, or directly in the terminal by attaching to a peer's live session with `iapeer attach`.
11
13
 
12
14
  <p align="center">
13
15
  <img src=".github/assets/iapeer-pingpong.gif" width="800" alt="A person messages one peer on Telegram; heterogeneous peers (Claude + Codex) relay a baton over one protocol and report back to Telegram.">
@@ -30,8 +32,8 @@ recipient peer — asleep? the daemon wakes it and delivers on wake
30
32
  ## What makes it different
31
33
 
32
34
  - **Real interactive CLI sessions, not headless one-shots.** Each AI peer is a full Claude Code or Codex CLI session, not a disposable `claude -p` / `codex exec` call (as of 2026-06-15, `claude -p` no longer counts against the subscription's main usage limits).
33
- - **Warm-on-demand lifecycle.** A live session answers fast; after about an hour idle the daemon closes it; the next message brings it back with its context resumed. The message flow drives the lifecycle — not a human's presence.
34
- - **One identity across runtimes, with one memory.** A peer's `personality` is decoupled from its runtime — the same peer runs on Claude or Codex, switched with a command. By default (set up by `iapeer onboard`) it carries one shared memory (iapeer-memory) keyed to the personality, not the runtime: one personality, one memory, whatever runtime it runs on.
35
+ - **Warm-on-demand lifecycle.** A live session answers fast; after about an hour idle the daemon closes it; the next message brings it back with its context resumed. The message flow drives the lifecycle, not a human's presence.
36
+ - **One identity across runtimes, with one memory.** A peer's `personality` is decoupled from its runtime: the same peer runs on Claude or Codex, switched with a command. By default (set up by `iapeer onboard`) it carries one shared memory (iapeer-memory) keyed to the personality, not the runtime — one personality, one memory, whatever runtime it runs on.
35
37
  - **Heterogeneous peers, one protocol.** AI agents, humans (Telegram), and services (timer = cron, watcher = event) are all first-class, addressed the same way over an always-on MCP daemon that exposes exactly one tool, `send_to_peer`.
36
38
 
37
39
  ## Quick start
@@ -91,9 +93,9 @@ Agents reach each other with `send_to_peer`; you can also message any peer over
91
93
 
92
94
  ## Bypass Permissions mode
93
95
 
94
- ⚠️ **iapeer runs your Claude and Codex peers in bypass-permissions / no-sandbox mode — they execute tools (shell commands, file edits, network calls) without asking you to approve each action.** This is by design: a peer is a background, headless session with no human watching its terminal, so there is no one to answer a per-tool permission prompt. The launcher starts Claude with `--dangerously-skip-permissions` and Codex with `--dangerously-bypass-approvals-and-sandbox`, and **auto-accepts the one-time "accept bypass mode" screen on your behalf** the first time a peer boots on a fresh machine.
96
+ **Warning: iapeer runs your Claude and Codex peers in bypass-permissions / no-sandbox mode — they execute tools (shell commands, file edits, network calls) without asking you to approve each action.** This is by design: a peer is a background, headless session with no human watching its terminal, so there is no one to answer a per-tool permission prompt. The launcher starts Claude with `--dangerously-skip-permissions` and Codex with `--dangerously-bypass-approvals-and-sandbox`, and **auto-accepts the one-time "accept bypass mode" screen on your behalf** the first time a peer boots on a fresh machine.
95
97
 
96
- What this means for you, stated plainly:
98
+ What this means for you:
97
99
 
98
100
  - A peer can run any command its runtime can run — read and write files anywhere in your home directory, install software, make network calls — with **no confirmation step**. There is no sandbox between an agent and your machine.
99
101
  - You are trusting the model's judgment, and the doctrine you give each peer, the same way you would trust a developer with a shell on your account.
@@ -118,4 +120,4 @@ Planned, not yet built:
118
120
 
119
121
  ## License
120
122
 
121
- Apache-2.0. Platform: macOS / launchd. iapeer is the foundation core of a larger ecosystem; runtime and capability plugins build on top of it.
123
+ Apache-2.0. Platform: macOS / launchd. iapeer is the foundation of a larger ecosystem; runtime and capability plugins build on top of it.
@@ -102,7 +102,7 @@ Identity has two storage places, and it matters which is authoritative.
102
102
 
103
103
  The profile fields split into three classes by who owns them:
104
104
 
105
- - **Identity core** — `personality`, `default_runtime`, `runtimes`, `description`, `intelligence`, `initial_prompt`. Owned by the core and the lifecycle. The `initial_prompt` field is the seed for a fresh session's first turn. The `runtime` field beside `default_runtime` is a synchronized copy for compatibility with old readers; both are written consistently.
105
+ - **Identity core** — `personality`, `default_runtime`, `runtimes`, `description`, `intelligence`, `initial_prompt`. Owned by the core and the lifecycle. The `initial_prompt` field is the seed for a fresh session's first turn. The legacy `runtime` field beside `default_runtime` is no longer written — a read-fallback still understands older profiles that carry only `runtime`.
106
106
  - **Private plugin config** — sections like `expansion` (shortcut aliases), `notifier` (the scheduler peer's settings), and others, one per plugin. Read only from the local profile, never projected into the registry.
107
107
  - **Public passport** — the `interfaces` section (e.g. `interfaces.telegram.bot`). These are routing attributes, and they're projected into the registry.
108
108
 
@@ -81,7 +81,7 @@ An agent with no activity for longer than an hour is slept on the next superviso
81
81
 
82
82
  **Wake serialization.** Several messages may hit a sleeping agent at once; one attempt should bring it up, or two competing sessions would be born. The daemon takes a lock on waking a given peer — the rest wait for the first to be ready.
83
83
 
84
- **Postmortem diagnostics.** At the moment a session dies the supervisor records its cause (exit code / signal) — what an ordinary "is the process alive" check misses. For this a separate canary holds a blocking connection to the supervisor and, on a session's death, writes the circumstances — memory, swap, process list, diagnostic reports — to a log.
84
+ **Postmortem diagnostics.** At the moment a session dies the supervisor records its cause (exit code / signal) to a log — what an ordinary "is the process alive" check misses.
85
85
 
86
86
  ## The boundary: claude/codex vs infrastructure
87
87
 
@@ -29,7 +29,7 @@ The adapter's key properties:
29
29
 
30
30
  Routers skip all the TUI phases: they have no boot dialogs, no readiness markers, no delivery confirmation, no control commands. Their liveness is held by launchd, not a wake by the daemon.
31
31
 
32
- The production mechanism for hosting TUI sessions is the **pty supervisor**: a supervisor daemon owns the session's pty and its pane log, and delivery onto input goes through its socket. tmux remains only an internal dev branch (the shadow branch) and doesn't figure at the user level.
32
+ The production mechanism for hosting TUI sessions is the **pty supervisor**: a supervisor daemon owns the session's pty and its pane log, and delivery onto input goes through its socket.
33
33
 
34
34
  ## The runtime-package contract
35
35
 
@@ -71,8 +71,6 @@ The daemon's supervisory tick is fixed — once a minute, not configurable.
71
71
  A few more variables the system reads but that aren't set in normal operation:
72
72
 
73
73
  - `NOTIFIER_FALLBACK_TARGET` — a fallback recipient for notifier signals; the core resolves it into the peer's plist at onboarding (no hardcode), the operator doesn't set it by hand.
74
- - `IAP_TMUX_PASTE_SETTLE_MS`, `IAP_TMUX_SUBMIT_TIMEOUT_MS` — delivery timings in the internal tmux dev branch; production uses the pty supervisor ([13 — Architecture and contract](13-architecture-and-contract.md)), so they aren't needed outside development.
75
- - `IAPEER_SHADOW_PATH` — the `PATH` for a shadow-fidelity run (an internal development tool).
76
74
 
77
75
  ## Logs and diagnostics
78
76
 
@@ -84,8 +82,8 @@ The daemon keeps three durable logs in `~/.iapeer/logs/iapeer/`:
84
82
 
85
83
  ### Postmortem diagnostics
86
84
 
87
- The cause of a session's death (exit code / signal) the supervisor records at the moment of death — what an ordinary "is the process alive" check misses. A separate canary holds a connection to the supervisor and, on a session's death, writes the circumstances to `exits.log`: memory, swap, top processes by consumption, system diagnostic reports. It's the material for answering "why did the agent die".
85
+ The cause of a session's death (exit code / signal) the supervisor records at the moment of death to `exits.log` — what an ordinary "is the process alive" check misses. It's the material for answering "why did the agent die".
88
86
 
89
87
  ### Full Disk Access
90
88
 
91
- To read system diagnostic reports, the `iapeer` binary needs Full Disk Access (FDA) — a macOS permission. `iapeer status` shows whether FDA is granted and hints at what to do if not. Without FDA, peers will hit TCC prompts when accessing protected folders and third-party app containers.
89
+ The `iapeer` binary needs Full Disk Access (FDA) — a macOS permission. `iapeer status` shows whether FDA is granted and hints at what to do if not. Without FDA, peers will hit TCC prompts when accessing protected folders and third-party app containers.
package/docs/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [Русский](ru/README.md) · **English**
4
4
 
5
- iapeer is the core of a multi-agent system on one host. It maintains a registry of participants, a single router daemon for messaging between them, and a set of commands to manage participants: create, start, sleep, restart, connect to external channels.
5
+ iapeer is the foundation of a multi-agent system on one host. It maintains a registry of participants, a single router daemon for messaging between them, and a set of commands to manage participants: create, start, sleep, restart, connect to external channels.
6
6
 
7
7
  A participant in the system is called a **peer**. A peer can be an AI agent (Claude, Codex), a human (over Telegram), or a service process (a scheduler, a watcher). iapeer doesn't distinguish them by nature at the transport level — they're all addressed the same way and exchange messages over one protocol.
8
8
 
@@ -81,7 +81,7 @@ The sections are layered: from "install and use" to "understand the architecture
81
81
 
82
82
  Built and working: the single router daemon, the messaging protocol (IAP), the peer registry, identity and its ABI, the lifecycle (wake, sleep, restart, supervision), runtime adapters for Claude, Codex, Telegram, and notifier, host install and onboarding, Telegram connection, the memory slot, and capability plugins.
83
83
 
84
- Planned on top of the core: a package-compatibility graph, a whole-team health doctor, and an interactive coordination interface. Until those exist, coordination operations live in the `iapeer` CLI.
84
+ Planned on top of the foundation: a package-compatibility graph, a whole-team health doctor, and an interactive coordination interface. Until those exist, coordination operations live in the `iapeer` CLI.
85
85
 
86
86
  ## Roadmap
87
87
 
@@ -102,7 +102,7 @@ PEER_IDENTITY = PEER_RUNTIME + "-" + PEER_PERSONALITY
102
102
 
103
103
  Поля профиля делятся на три класса по тому, кто ими владеет:
104
104
 
105
- - **Ядро идентичности** — `personality`, `default_runtime`, `runtimes`, `description`, `intelligence`, `initial_prompt`. Этим владеет ядро и жизненный цикл. Поле `initial_prompt` — затравка первого хода свежей сессии. Поле `runtime` рядом с `default_runtime` — это синхронная копия для совместимости со старыми читателями; оба пишутся согласованно.
105
+ - **Ядро идентичности** — `personality`, `default_runtime`, `runtimes`, `description`, `intelligence`, `initial_prompt`. Этим владеет ядро и жизненный цикл. Поле `initial_prompt` — затравка первого хода свежей сессии. Легаси-поле `runtime` рядом с `default_runtime` больше не пишется — фолбэк при чтении всё ещё понимает старые профили, где есть только `runtime`.
106
106
  - **Приватная конфигурация плагина** — секции вроде `expansion` (шорткаты-алиасы), `notifier` (настройка пира-планировщика) и прочие, по одной на плагин. Читаются только из локального профиля, в реестр не попадают.
107
107
  - **Публичный паспорт** — секция `interfaces` (например `interfaces.telegram.bot`). Это атрибуты маршрутизации, и они проецируются в реестр.
108
108
 
@@ -81,7 +81,7 @@
81
81
 
82
82
  **Сериализация пробуждений.** На спящего агента может прилететь несколько сообщений сразу; поднять его должна одна попытка, иначе родятся две конкурирующие сессии. Демон берёт блокировку на пробуждение конкретного пира — остальные ждут готовности первой.
83
83
 
84
- **Посмертная диагностика.** Супервизор в момент смерти сессии фиксирует её причину (код выхода / сигнал) — это то, что обычная проверка «жив ли процесс» не ловит. На этот случай отдельный сторож (canary) держит блокирующее соединение с супервизором и при смерти сессии записывает обстоятельства — память, swap, список процессов, отчёты диагностики — в журнал.
84
+ **Посмертная диагностика.** Супервизор в момент смерти сессии фиксирует её причину (код выхода / сигнал) в журнал — это то, что обычная проверка «жив ли процесс» не ловит.
85
85
 
86
86
  ## Граница: claude/codex против инфраструктурных
87
87
 
@@ -29,7 +29,7 @@
29
29
 
30
30
  Роутеры пропускают все TUI-фазы: у них нет загрузочных диалогов, маркеров готовности, подтверждения доставки и контрольных команд. Их живость держит launchd, а не пробуждение демоном.
31
31
 
32
- Продакшен-механизм хостинга TUI-сессий — **pty-супервизор**: супервизор-демон владеет pty сессии и её pane-логом, через его сокет идёт доставка на ввод. tmux остаётся только внутренней dev-ветвью (shadow-branch), на пользовательском уровне не фигурирует.
32
+ Продакшен-механизм хостинга TUI-сессий — **pty-супервизор**: супервизор-демон владеет pty сессии и её pane-логом, через его сокет идёт доставка на ввод.
33
33
 
34
34
  ## Контракт рантайм-пакета
35
35
 
@@ -71,8 +71,6 @@
71
71
  Ещё несколько переменных система читает, но в обычной эксплуатации их не задают:
72
72
 
73
73
  - `NOTIFIER_FALLBACK_TARGET` — запасной адресат для сигналов notifier; ядро резолвит его в плист пира при онбординге (без хардкода), вручную оператор его не выставляет.
74
- - `IAP_TMUX_PASTE_SETTLE_MS`, `IAP_TMUX_SUBMIT_TIMEOUT_MS` — тайминги доставки во внутренней tmux dev-ветви; на проде используется pty-супервизор ([13 — Архитектура и контракт](13-архитектура-и-контракт.md)), вне разработки не нужны.
75
- - `IAPEER_SHADOW_PATH` — `PATH` для shadow-fidelity прогона (внутренний инструмент разработки).
76
74
 
77
75
  ## Журналы и диагностика
78
76
 
@@ -84,8 +82,8 @@
84
82
 
85
83
  ### Посмертная диагностика
86
84
 
87
- Причину смерти сессии (код выхода / сигнал) супервизор фиксирует в момент смерти — это то, что обычная проверка «жив ли процесс» не ловит. Отдельный сторож (canary) держит соединение с супервизором и при смерти сессии записывает в `exits.log` обстоятельства: память, swap, процессы по потреблению, системные отчёты диагностики. Это материал для ответа на вопрос «почему агент умер».
85
+ Причину смерти сессии (код выхода / сигнал) супервизор фиксирует в момент смерти в `exits.log` — это то, что обычная проверка «жив ли процесс» не ловит. Это материал для ответа на вопрос «почему агент умер».
88
86
 
89
87
  ### Full Disk Access
90
88
 
91
- Чтобы читать системные отчёты диагностики, бинарю `iapeer` нужен Full Disk Access (FDA) — разрешение macOS. `iapeer status` показывает, выдан ли FDA, и подсказывает, что делать, если нет. Без FDA пиры будут натыкаться на запросы TCC при доступе к защищённым папкам и контейнерам сторонних приложений.
89
+ Бинарю `iapeer` нужен Full Disk Access (FDA) — разрешение macOS. `iapeer status` показывает, выдан ли FDA, и подсказывает, что делать, если нет. Без FDA пиры будут натыкаться на запросы TCC при доступе к защищённым папкам и контейнерам сторонних приложений.
package/docs/ru/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Русский** · [English](../README.md)
4
4
 
5
- iapeer — ядро мультиагентной системы на одном хосте. Он держит реестр участников, единый демон-роутер для обмена сообщениями между ними и набор команд, которыми участниками управляют: создают, запускают, усыпляют, перезапускают, подключают к внешним каналам.
5
+ iapeer — основа мультиагентной системы на одном хосте. Он держит реестр участников, единый демон-роутер для обмена сообщениями между ними и набор команд, которыми участниками управляют: создают, запускают, усыпляют, перезапускают, подключают к внешним каналам.
6
6
 
7
7
  Участник системы называется **пиром**. Пиром может быть AI-агент (Claude, Codex), человек (через Telegram) или служебный процесс (планировщик, наблюдатель). iapeer не различает их по природе на уровне транспорта — все они адресуются одинаково и обмениваются сообщениями через один протокол.
8
8
 
@@ -81,7 +81,7 @@ iapeer send assistant --message "привет, ты на связи?"
81
81
 
82
82
  Построено и работает: единый демон-роутер, протокол обмена сообщениями (IAP), реестр пиров, идентичность и её ABI, жизненный цикл (пробуждение, усыпление, перезапуск, надзор), рантайм-адаптеры для Claude, Codex, Telegram и notifier, установка и онбординг хоста, подключение Telegram, слот памяти и capability-плагины.
83
83
 
84
- Запланировано поверх ядра: граф совместимости пакетов, доктор здоровья всей команды и интерактивный интерфейс координации. Пока этих надстроек нет, координационные операции живут в CLI `iapeer`.
84
+ Запланировано поверх основы: граф совместимости пакетов, доктор здоровья всей команды и интерактивный интерфейс координации. Пока этих надстроек нет, координационные операции живут в CLI `iapeer`.
85
85
 
86
86
  ## Планы
87
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
@@ -28,6 +28,7 @@
28
28
  "src",
29
29
  "bin",
30
30
  "docs",
31
+ "CHANGELOG.md",
31
32
  "tsconfig.json",
32
33
  "!src/**/*.test.ts",
33
34
  "!docs/internals/**"
@@ -217,6 +217,23 @@ export function projectProfileToRecord(cwd: string): PeerRecord | null {
217
217
  }
218
218
  }
219
219
 
220
+ /** Order-insensitive structural serialization for comparing the projected OWNER
221
+ * passport (`interfaces`) — a nested object whose key ORDER is not semantically
222
+ * meaningful. A plain JSON.stringify would flag an order-only difference as drift
223
+ * (a legacy upsertPeer write may have stored keys in a different order); canonicalize
224
+ * first so only real content divergence is reported — a key added, removed, or changed,
225
+ * e.g. a field dropped from the source of truth after a cutover that the derived index
226
+ * never re-projected. */
227
+ function canonicalJson(value: unknown): string {
228
+ if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'
229
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
230
+ const obj = value as Record<string, unknown>
231
+ return `{${Object.keys(obj)
232
+ .sort()
233
+ .map(k => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`)
234
+ .join(',')}}`
235
+ }
236
+
220
237
  /** The index↔local drift for one peer: the fields where the live registry record
221
238
  * disagrees with the projection from the local profile (the self-heal source). Empty
222
239
  * array = in sync. cwd is the registry pointer, never compared. */
@@ -227,6 +244,12 @@ export function recordDrift(indexRecord: PeerRecord, projected: PeerRecord): str
227
244
  if (indexRecord.runtimes.join(',') !== projected.runtimes.join(',')) drift.push('runtimes')
228
245
  if (indexRecord.description !== projected.description) drift.push('description')
229
246
  if (indexRecord.intelligence !== projected.intelligence) drift.push('intelligence')
247
+ // The OWNER passport (`interfaces`) is projected into the registry, so a divergence
248
+ // here is real drift — the class that hid the bot_username cutover (source dropped
249
+ // `interfaces.telegram.bot`, the derived index kept it because nothing compared it).
250
+ // projectProfileToRecord already replaces interfaces wholesale; comparing it here makes
251
+ // the desync VISIBLE to verify/reconcile so it can never silently persist again.
252
+ if (canonicalJson(indexRecord.interfaces) !== canonicalJson(projected.interfaces)) drift.push('interfaces')
230
253
  return drift
231
254
  }
232
255
 
@@ -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. Long enough for the post-reply housekeeping writes (operative notes)
78
- * to keep resetting it; far below idleSecs. ASSUMPTION (by design): the
79
- * transcript mtime is a LIVENESS proxy — "no longer writing" — not a semantic
80
- * "done" signal. */
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 activity-proxy silence. Live case: a worker that
85
- * ended silently without its final outbound stalled its M3 FIFO for the FULL
86
- * generic idleSecs (1 h) — with serial drain that blocks the whole conveyor per
87
- * silent worker. Bound chosen ≫ the legitimate silent-tool case
88
- * (sleep-180) and ≪ idleSecs. DOCUMENTED RISK: an ephemeral worker whose tool
89
- * stays silent longer than this MID-TASK is reaped and its consumed queue item
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 = the activity proxy stayed
1248
- // silent for ephemeralQuietSecs — post-reply housekeeping (operative-note
1249
- // writes) keeps resetting it, so the worker finishes its bookkeeping first.
1250
- // NOT armed (still mid-task, e.g. a long silent tool run) → the ordinary
1251
- // idle bound below is its only reaper. Deliberate, policy-driven death:
1252
- // NO .idle-reaped (an ephemeral peer never resumes) and NO recordDeath
1253
- // (the crash-loop ring counts faults, not policy reaps).
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
- if (isEphemeralPeer(s.cwd) && ageSecs > cfg.ephemeralUnarmedIdleSecs) {
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)
@@ -966,19 +966,21 @@ function validateAttachments(value: readonly string[] = []): Result<string[]> {
966
966
 
967
967
  // ─── Telegram-domain sender policy ─
968
968
 
969
- /** A SENDER face in the telegram domain: a non-empty interfaces.telegram.bot (the
970
- * bot machine-key) in the peer's registry passport. Deliberately bot-ONLY, not
971
- * bot|user_id — the bridge picks the SENDING bot from the sender's bot key:
972
- * a user_id grants a receive/operator
973
- * identity but no ability to send, so a user_id-only sender passing the guard
974
- * would still die at the bridge — a late asynchronous failure where the guard
975
- * exists to give a synchronous one. The guard passes exactly what the bridge can
976
- * deliver. */
969
+ /** A SENDER face in the telegram domain: a non-empty interfaces.telegram.bot_username
970
+ * (the bot binding key SINCE the bot_username-cutover) in the peer's registry passport.
971
+ * Deliberately bot_username-ONLY, not bot_username|user_id — the bridge picks the SENDING
972
+ * bot from the sender's bot_username key: a user_id grants a receive/operator identity but
973
+ * no ability to send, so a user_id-only sender passing the guard would still die at the
974
+ * bridge — a late asynchronous failure where the guard exists to give a synchronous one.
975
+ * The guard passes exactly what the bridge can deliver. (Legacy `bot` was the pre-cutover
976
+ * key; it is NO LONGER read here — the source of truth dropped it and telegram-runtime
977
+ * resolves the sending bot on bot_username. This guard is the foundation's only consumer
978
+ * of the telegram passport, so it moves with the cutover.) */
977
979
  export function hasTelegramPresence(record: PeerRecord): boolean {
978
980
  const tg = record.interfaces?.telegram
979
981
  if (!tg || typeof tg !== 'object' || Array.isArray(tg)) return false
980
982
  const t = tg as Record<string, unknown>
981
- return typeof t.bot === 'string' && t.bot.trim() !== ''
983
+ return typeof t.bot_username === 'string' && t.bot_username.trim() !== ''
982
984
  }
983
985
 
984
986
  /** Sender policy for the telegram domain: a peer with NO declared telegram presence
@@ -1000,7 +1002,7 @@ function telegramSenderGuard(
1000
1002
  if (targetRuntime !== 'telegram') return ok(undefined)
1001
1003
  if (hasTelegramPresence(caller.record)) return ok(undefined)
1002
1004
  return err(
1003
- `telegram policy: sender "${caller.personality}" has no telegram face (no interfaces.telegram.bot in its passport) — message to "${targetPersonality}" NOT delivered; route via a peer that has a telegram bot in its passport, or provision a bot for this sender`,
1005
+ `telegram policy: sender "${caller.personality}" has no telegram face (no interfaces.telegram.bot_username in its passport) — message to "${targetPersonality}" NOT delivered; route via a peer that has a telegram bot in its passport, or provision a bot for this sender`,
1004
1006
  )
1005
1007
  }
1006
1008