@agfpd/iapeer 0.4.6 → 0.4.8
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/README.md +6 -0
- package/package.json +1 -1
- package/src/launch/readyGateModel.ts +10 -4
- package/src/supervisor/daemon.ts +34 -3
package/README.md
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
|
|
10
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`.
|
|
11
11
|
|
|
12
|
+
<p align="center">
|
|
13
|
+
<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.">
|
|
14
|
+
<br>
|
|
15
|
+
<em>One Telegram message in → Claude and Codex peers pass a baton over the same protocol → a report back in Telegram.</em>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
12
18
|
## How it works
|
|
13
19
|
|
|
14
20
|
```text
|
package/package.json
CHANGED
|
@@ -134,9 +134,15 @@ export async function paneLogViewport(log: string, cols: number, rows: number, s
|
|
|
134
134
|
* validated `composerOccupancyFromModel` (REUSED, not reimplemented). The hosted equivalent of
|
|
135
135
|
* `targetComposerBlocksDelivery`'s capture-pane occupancy. @xterm loads dynamically here (flag-on
|
|
136
136
|
* hosted only; the leaf `composerOccupancyFromModel` import is type-only at runtime, so the warm path
|
|
137
|
-
* stays @xterm-free with the flip off).
|
|
138
|
-
*
|
|
139
|
-
*
|
|
137
|
+
* stays @xterm-free with the flip off). UNREADABLE PANE-LOG → NOT busy (false): the hold exists ONLY to
|
|
138
|
+
* avoid clobbering VISIBLE human composer text; if the model can't be built there is NO observed text to
|
|
139
|
+
* protect. This deliberately does NOT mirror the tmux predicate's capture-fail-is-busy stance: tmux
|
|
140
|
+
* `capture-pane` fails ~only when the session is GONE (then the queue's session-token/alive check fails
|
|
141
|
+
* the job loudly), but a hosted pane-log can be absent/unreadable while the session is ALIVE (e.g. a
|
|
142
|
+
* long-lived session whose lifecycle pane-log was rotated/removed) — so "uncertainty → busy" would stall
|
|
143
|
+
* EVERY delivery to such a peer until the 120 s force-timeout, starving the core addressed-delivery
|
|
144
|
+
* feature. The narrow cost (no composer protection while the log is unreadable) is acceptable: a session
|
|
145
|
+
* actively being typed into is precisely one whose pane-log IS being written.
|
|
140
146
|
*
|
|
141
147
|
* NB (sequence): this is the DETECTION primitive. The end-to-end gate (attached-supervisor-
|
|
142
148
|
* client + enqueue + host-aware drain) lands with the attach client in Ф0b-3 — until a human can
|
|
@@ -145,7 +151,7 @@ export async function paneLogViewport(log: string, cols: number, rows: number, s
|
|
|
145
151
|
*/
|
|
146
152
|
export async function paneLogComposerOccupied(log: string, cols: number, rows: number, runtime: string): Promise<boolean> {
|
|
147
153
|
const term = await loadPaneLogModel(log, cols, rows)
|
|
148
|
-
if (!term) return
|
|
154
|
+
if (!term) return false // unreadable pane-log → no observed human composer text → deliver, never stall (see JSDoc)
|
|
149
155
|
const { composerOccupancyFromModel } = await import('../shadow/render.ts')
|
|
150
156
|
return composerOccupancyFromModel(term, cols, rows, runtime)
|
|
151
157
|
}
|
package/src/supervisor/daemon.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// @xterm/headless bundles under `bun build --compile` (pure JS; the Bun-native pty has no native addon).
|
|
13
13
|
import { Terminal } from '@xterm/headless'
|
|
14
14
|
import { SerializeAddon } from '@xterm/addon-serialize'
|
|
15
|
-
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, unlinkSync, writeFileSync, writeSync } from 'node:fs'
|
|
15
|
+
import { appendFileSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, statSync, unlinkSync, writeFileSync, writeSync } from 'node:fs'
|
|
16
16
|
import { dirname } from 'node:path'
|
|
17
17
|
import {
|
|
18
18
|
BackpressureWriter,
|
|
@@ -157,14 +157,43 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
157
157
|
// corrupts multibyte glyphs like ❯/›; the canonical latin1/Buffer grabli). Parent dir 0700, matching
|
|
158
158
|
// launch's mkdir before pipe-pane. Unset paneLogPath → no log (throwaway/tick).
|
|
159
159
|
let paneLogFd = -1
|
|
160
|
-
|
|
160
|
+
let paneLogIno = -1 // inode the fd points at — to detect an out-from-under-us unlink/replace
|
|
161
|
+
let paneLogCheckedMs = 0
|
|
162
|
+
const PANELOG_HEAL_MS = 1000 // throttle the path-existence re-check to ≤1/sec regardless of output volume
|
|
163
|
+
const openPaneLog = (): void => {
|
|
164
|
+
if (!opts.paneLogPath) return
|
|
161
165
|
try {
|
|
162
166
|
mkdirSync(dirname(opts.paneLogPath), { recursive: true, mode: 0o700 })
|
|
167
|
+
if (paneLogFd >= 0) try { closeSync(paneLogFd) } catch { /* */ }
|
|
163
168
|
paneLogFd = openSync(opts.paneLogPath, 'a')
|
|
169
|
+
paneLogIno = fstatSync(paneLogFd).ino
|
|
164
170
|
} catch {
|
|
165
171
|
paneLogFd = -1 // best-effort: a log open hiccup never blocks serving the session
|
|
172
|
+
paneLogIno = -1
|
|
166
173
|
}
|
|
167
174
|
}
|
|
175
|
+
// SELF-HEAL (Defect 2): the pane-log path can be unlinked out from under a live session (observed on
|
|
176
|
+
// the fleet: long-lived sessions' lifecycle pane-logs vanished while the supervisor ran). The fd then
|
|
177
|
+
// keeps writing to a now-unlinked inode — the path stays gone and every occupancy reader of
|
|
178
|
+
// `<logDir>/<identity>.log` goes blind: composer-busy delivery detection AND telegram-runtime's
|
|
179
|
+
// mtime-based typing/activity indicator (shared file). Throttled re-check before a write: if the path
|
|
180
|
+
// vanished or now maps to a DIFFERENT inode than the fd, reopen 'a'. capPaneLogs truncates IN PLACE
|
|
181
|
+
// (same inode) so a normal cap never triggers a spurious reopen. Tied to output (no idle wakeups) —
|
|
182
|
+
// exactly when the log matters.
|
|
183
|
+
const healPaneLog = (): void => {
|
|
184
|
+
if (!opts.paneLogPath || paneLogFd < 0) return
|
|
185
|
+
const now = Date.now()
|
|
186
|
+
if (now - paneLogCheckedMs < PANELOG_HEAL_MS) return
|
|
187
|
+
paneLogCheckedMs = now
|
|
188
|
+
let ino = -1
|
|
189
|
+
try {
|
|
190
|
+
ino = statSync(opts.paneLogPath).ino
|
|
191
|
+
} catch {
|
|
192
|
+
ino = -1 // path gone
|
|
193
|
+
}
|
|
194
|
+
if (ino !== paneLogIno) openPaneLog()
|
|
195
|
+
}
|
|
196
|
+
openPaneLog()
|
|
168
197
|
|
|
169
198
|
const clients = new Set<unknown>()
|
|
170
199
|
const writers = new WeakMap<object, BackpressureWriter>()
|
|
@@ -234,12 +263,14 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
234
263
|
const buf = Buffer.isBuffer(d) ? d : typeof d === 'string' ? Buffer.from(d, 'utf8') : Buffer.from(d)
|
|
235
264
|
// PANE-LOG: the RAW child bytes, exactly as pipe-pane writes them (before any query-stripping
|
|
236
265
|
// for clients) — written as a Buffer, never a string. The `s` below is for query DETECTION only.
|
|
237
|
-
if (paneLogFd >= 0)
|
|
266
|
+
if (paneLogFd >= 0) {
|
|
267
|
+
healPaneLog() // reopen if the path was unlinked/replaced out from under us (Defect 2)
|
|
238
268
|
try {
|
|
239
269
|
writeSync(paneLogFd, buf)
|
|
240
270
|
} catch {
|
|
241
271
|
/* best-effort */
|
|
242
272
|
}
|
|
273
|
+
}
|
|
243
274
|
const s = buf.toString('latin1')
|
|
244
275
|
writeSeq++ // a new write arrived (lets the attach drain detect bytes that land mid-await)
|
|
245
276
|
writeChain = new Promise<void>(resolve => xterm.write(buf, () => resolve())) // authoritative model (+ parse barrier)
|