@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 +254 -0
- package/README.md +9 -7
- package/docs/03-peers-runtimes-identity.md +1 -1
- package/docs/04-lifecycle-and-daemon.md +1 -1
- package/docs/13-architecture-and-contract.md +1 -1
- package/docs/14-configuration-and-logs.md +2 -4
- package/docs/README.md +2 -2
- package/docs/ru/03-/320/277/320/270/321/200/321/213-/321/200/320/260/320/275/321/202/320/260/320/271/320/274/321/213-/320/270/320/264/320/265/320/275/321/202/320/270/321/207/320/275/320/276/321/201/321/202/321/214.md +1 -1
- package/docs/ru/04-/320/266/320/270/320/267/320/275/320/265/320/275/320/275/321/213/320/271-/321/206/320/270/320/272/320/273-/320/270-/320/264/320/265/320/274/320/276/320/275.md +1 -1
- package/docs/ru/13-/320/260/321/200/321/205/320/270/321/202/320/265/320/272/321/202/321/203/321/200/320/260-/320/270-/320/272/320/276/320/275/321/202/321/200/320/260/320/272/321/202.md +1 -1
- package/docs/ru/14-/320/272/320/276/320/275/321/204/320/270/320/263/321/203/321/200/320/260/321/206/320/270/321/217-/320/270-/320/266/321/203/321/200/320/275/320/260/320/273/321/213.md +2 -4
- package/docs/ru/README.md +2 -2
- package/package.json +2 -1
- package/src/identity/profileStandard.ts +23 -0
- package/src/lifecycle/index.ts +59 -22
- package/src/transport/index.ts +12 -10
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
|
-
**
|
|
3
|
+
**The foundation for a team of AI agents on one machine.**
|
|
4
4
|
|
|
5
5
|
[](https://github.com/agfpd/iapeer/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/@agfpd/iapeer)
|
|
7
7
|
[](./LICENSE)
|
|
8
8
|
[](#quick-start)
|
|
9
9
|
|
|
10
|
-
iapeer keeps a team of AI agents, humans, and services on one host
|
|
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
|
|
34
|
-
- **One identity across runtimes, with one memory.** A peer's `personality` is decoupled from its runtime
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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` — затравка первого хода свежей сессии.
|
|
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
|
-
**Посмертная диагностика.** Супервизор в момент смерти сессии фиксирует её причину (код выхода / сигнал) — это то, что обычная проверка «жив ли процесс» не ловит.
|
|
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-логом, через его сокет идёт доставка на ввод.
|
|
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
|
-
Причину смерти сессии (код выхода / сигнал) супервизор фиксирует в момент смерти — это то, что обычная проверка «жив ли процесс» не ловит.
|
|
85
|
+
Причину смерти сессии (код выхода / сигнал) супервизор фиксирует в момент смерти в `exits.log` — это то, что обычная проверка «жив ли процесс» не ловит. Это материал для ответа на вопрос «почему агент умер».
|
|
88
86
|
|
|
89
87
|
### Full Disk Access
|
|
90
88
|
|
|
91
|
-
|
|
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
|
-
Запланировано поверх
|
|
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.
|
|
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
|
|
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)
|
package/src/transport/index.ts
CHANGED
|
@@ -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.
|
|
970
|
-
* bot
|
|
971
|
-
*
|
|
972
|
-
* a user_id grants a receive/operator
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
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.
|
|
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.
|
|
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
|
|