@agfpd/iapeer 0.4.15 → 0.4.16
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 +14 -1
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/core/constants.ts +9 -7
- package/src/launch/adapters/voicetalk.ts +95 -0
- package/src/launch/index.ts +2 -0
- package/src/launch/launchdRun.ts +1 -0
- package/src/launch/types.ts +2 -0
- package/src/lifecycle/index.ts +30 -31
- package/src/runtime/deploy.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.4.16] - 2026-06-21
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Idle-reap now uses the pane-log (TUI render-stream) mtime as its activity proxy, not the
|
|
18
|
+
transcript file mtime. A live claude session re-touches its session `.jsonl` periodically
|
|
19
|
+
*without a real turn*, so the transcript proxy never crossed `idleSecs` and **claude peers
|
|
20
|
+
never idle-reaped** — the fleet piled up warm while codex peers (whose session file isn't
|
|
21
|
+
re-touched) reaped normally. The pane-log goes truly quiet at the prompt, so idle peers now
|
|
22
|
+
reap as intended. Pane-log primary; transcript fallback only when the pane-log is missing
|
|
23
|
+
(legacy supervisor); the launch ready-gate still uses the transcript.
|
|
24
|
+
|
|
13
25
|
## [0.4.15] - 2026-06-21
|
|
14
26
|
|
|
15
27
|
### Fixed
|
|
@@ -233,7 +245,8 @@ Initial public release — the foundation core of the iapeer multi-agent ecosyst
|
|
|
233
245
|
- **Heterogeneous peers** — AI agents (Claude, Codex), humans (Telegram), and services
|
|
234
246
|
(timer = cron, watcher = event) are all first-class and addressed the same way.
|
|
235
247
|
|
|
236
|
-
[Unreleased]: https://github.com/agfpd/iapeer/compare/v0.4.
|
|
248
|
+
[Unreleased]: https://github.com/agfpd/iapeer/compare/v0.4.16...HEAD
|
|
249
|
+
[0.4.16]: https://github.com/agfpd/iapeer/compare/v0.4.15...v0.4.16
|
|
237
250
|
[0.4.15]: https://github.com/agfpd/iapeer/compare/v0.4.14...v0.4.15
|
|
238
251
|
[0.4.14]: https://github.com/agfpd/iapeer/compare/v0.4.13...v0.4.14
|
|
239
252
|
[0.4.13]: https://github.com/agfpd/iapeer/compare/v0.4.12...v0.4.13
|
package/README.md
CHANGED
|
@@ -20,18 +20,18 @@ Agents on Claude and on Codex are different AI runtimes from different vendors,
|
|
|
20
20
|
## How it works
|
|
21
21
|
|
|
22
22
|
```text
|
|
23
|
-
send_to_peer(name, text)
|
|
23
|
+
send_to_peer(name, text) ← from any peer
|
|
24
24
|
│
|
|
25
25
|
▼
|
|
26
|
-
router daemon
|
|
26
|
+
router daemon — always on; finds the recipient, checks liveness, delivers
|
|
27
27
|
│
|
|
28
28
|
▼
|
|
29
|
-
recipient peer
|
|
29
|
+
recipient peer — asleep? the daemon wakes it and delivers on wake
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## What makes it different
|
|
33
33
|
|
|
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
|
|
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.
|
|
35
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
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.
|
|
37
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`.
|
package/package.json
CHANGED
package/src/core/constants.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function normalizeIntelligenceValue(value: unknown): Intelligence | undef
|
|
|
56
56
|
return LEGACY_INTELLIGENCE[value]
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const NATURAL_RUNTIMES = new Set(['telegram', 'discord', 'matrix', 'email', 'web'])
|
|
59
|
+
const NATURAL_RUNTIMES = new Set(['telegram', 'discord', 'matrix', 'email', 'web', 'voicetalk'])
|
|
60
60
|
const ABSENT_RUNTIMES = new Set(['notifier', 'webhook', 'api', 'cron'])
|
|
61
61
|
|
|
62
62
|
export function defaultIntelligenceForRuntime(runtime: string): Intelligence {
|
|
@@ -67,12 +67,12 @@ export function defaultIntelligenceForRuntime(runtime: string): Intelligence {
|
|
|
67
67
|
|
|
68
68
|
// Infra runtimes are ALWAYS-ON (held live by launchd KeepAlive), as opposed to the
|
|
69
69
|
// warm-on-demand agentic runtimes (claude/codex, woken by the daemon). Liveness is
|
|
70
|
-
// a property of the RUNTIME, not the personality (zone Идентичность).
|
|
71
|
-
//
|
|
72
|
-
// notifier receives send_to_peer(timer|watcher, …)
|
|
73
|
-
//
|
|
74
|
-
// set gates always-on launchd plist generation (src/launch/launchd.ts).
|
|
75
|
-
const INFRA_RUNTIMES = new Set(['notifier', 'telegram'])
|
|
70
|
+
// a property of the RUNTIME, not the personality (zone Идентичность). Infra runtimes
|
|
71
|
+
// are routers hosted by the pty supervisor: telegram/voicetalk carry a human channel
|
|
72
|
+
// (inbound messages / voice), notifier receives send_to_peer(timer|watcher, …)
|
|
73
|
+
// registration/live-reload — outbound to each is deliverHosted → the child's stdin.
|
|
74
|
+
// This set gates always-on launchd plist generation (src/launch/launchd.ts).
|
|
75
|
+
const INFRA_RUNTIMES = new Set(['notifier', 'telegram', 'voicetalk'])
|
|
76
76
|
|
|
77
77
|
export function isInfraRuntime(runtime: string): boolean {
|
|
78
78
|
return INFRA_RUNTIMES.has(runtime)
|
|
@@ -89,6 +89,7 @@ export function isInfraRuntime(runtime: string): boolean {
|
|
|
89
89
|
export const INFRA_RUNTIME_BIN_ENV: Readonly<Record<string, string>> = {
|
|
90
90
|
notifier: 'NOTIFIER_RUNTIME_BIN',
|
|
91
91
|
telegram: 'TELEGRAM_RUNTIME_BIN',
|
|
92
|
+
voicetalk: 'VOICETALK_RUNTIME_BIN',
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
/** The PATH-resolvable default launcher name per infra runtime (when no abs path
|
|
@@ -97,6 +98,7 @@ export const INFRA_RUNTIME_BIN_ENV: Readonly<Record<string, string>> = {
|
|
|
97
98
|
export const INFRA_RUNTIME_DEFAULT_BIN: Readonly<Record<string, string>> = {
|
|
98
99
|
notifier: 'notifier-runtime',
|
|
99
100
|
telegram: 'telegram-runtime',
|
|
101
|
+
voicetalk: 'voicetalk-runtime',
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
// codex MCP token-free import (contract Установка §codex MCP). codex REFUSES to import
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// voicetalk RuntimeAdapter — the "HOW to launch / observe ONE voicetalk session"
|
|
2
|
+
// half of the launch contract (src/launch/types.ts). Like telegram, voicetalk is a
|
|
3
|
+
// long-running ROUTER (voicetalk-runtime run: mic + wake-word listen → IAP envelopes
|
|
4
|
+
// to the fleet; <iap>-marked stdin → TTS to the owner's speaker), NOT an LLM TUI. So
|
|
5
|
+
// kind:'router' tells launch.launch to SKIP every TUI phase — no pane boot dialog, no
|
|
6
|
+
// ready-marker, no first-user-turn delivery, no activity-proxy ready-gate, no
|
|
7
|
+
// permission modal, no transcript to resume. The pane predicates below are therefore
|
|
8
|
+
// trivial constants, present only to satisfy the frozen interface.
|
|
9
|
+
//
|
|
10
|
+
// voicetalk is a presence-runtime for a HUMAN (the owner's voice channel), modelled on
|
|
11
|
+
// telegram per the foundation presence-runtime contract: kind:'router',
|
|
12
|
+
// usesDoctrine:false, requiresIntelligence:'natural', argv = `<bin> run <extra>`. The
|
|
13
|
+
// STT/TTS engines and providers live in a shared voice service (voice-connect), NOT in
|
|
14
|
+
// this adapter and NOT in the peer passport — interfaces.voicetalk carries only the
|
|
15
|
+
// binding ({wake_word, device}), opaque to the foundation.
|
|
16
|
+
//
|
|
17
|
+
// NO currency on this path: no marketplace check, no plugin install/update — that is
|
|
18
|
+
// install-time (the package self-deploys its runtime.json manifest), not session
|
|
19
|
+
// bring-up.
|
|
20
|
+
|
|
21
|
+
import type { ControlCommand, ControlPlan, LaunchAdapterConfig, LaunchSpec, RuntimeAdapter } from '../types.ts'
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// voicetalkAdapter
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export const voicetalkAdapter: RuntimeAdapter = {
|
|
28
|
+
runtime: 'voicetalk',
|
|
29
|
+
kind: 'router',
|
|
30
|
+
usesDoctrine: false,
|
|
31
|
+
|
|
32
|
+
/** No submit surface — a router takes no bracketed-paste-then-Enter turn; delivery
|
|
33
|
+
* is deliverHosted → child stdin (<iap> reader), not submitIntoTui. Empty glyphs. */
|
|
34
|
+
deliveryMarkers: { promptGlyphs: [] },
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* voicetalk is a HUMAN channel — launch REFUSES a non-natural peer (fail-loud).
|
|
38
|
+
* Same nature gate as telegram: a peer with intelligence artificial/absent must never
|
|
39
|
+
* be brought up on voicetalk (it would route the owner's voice channel to an
|
|
40
|
+
* agent/script). The launch primitive enforces this against LaunchSpec.intelligence;
|
|
41
|
+
* source of intelligence → docs/Идентичность.
|
|
42
|
+
*/
|
|
43
|
+
requiresIntelligence: 'natural',
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* argv = voicetalkBin + 'run' + extraArgs.
|
|
47
|
+
* - cfg.voicetalkBin ?? 'voicetalk-runtime' the launch binary (the always-on
|
|
48
|
+
* plist pins an absolute path via VOICETALK_RUNTIME_BIN — launchd-minimal PATH;
|
|
49
|
+
* here we defer to cfg.voicetalkBin when set, else emit the PATH-resolvable
|
|
50
|
+
* literal for launch.launch to resolve).
|
|
51
|
+
* - 'run' the router subcommand: reads the peer-profile binding (interfaces.voicetalk),
|
|
52
|
+
* brings up mic + wake-word listening, and pumps IAP envelopes both directions
|
|
53
|
+
* through the session's stdio (outbound <iap> stdin → TTS; inbound mic → fleet via
|
|
54
|
+
* the daemon send_to_peer).
|
|
55
|
+
* - ...extraArgs LaunchSpec.extraArgs passthrough.
|
|
56
|
+
*
|
|
57
|
+
* NO --system-prompt-file: usesDoctrine:false — a router is not an LLM, no doctrine to
|
|
58
|
+
* merge. NO --resume: a router has no transcript. NO currency on this path.
|
|
59
|
+
*/
|
|
60
|
+
buildArgv(spec: LaunchSpec, cfg: LaunchAdapterConfig): string[] {
|
|
61
|
+
return [cfg.voicetalkBin ?? 'voicetalk-runtime', 'run', ...(spec.extraArgs ?? [])]
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/** No startup dialogs — a router has no pane TUI to answer. launch.launch skips the
|
|
65
|
+
* boot phase for kind:'router', so this is never consulted; null = "no dialog". */
|
|
66
|
+
bootDialogKeys(_pane: string): string[] | null {
|
|
67
|
+
return null
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/** Always ready: a router has no input surface waiting for a first message — it is
|
|
71
|
+
* "up" the instant `voicetalk-runtime run` is launched. launch.launch skips the
|
|
72
|
+
* ready-gate for kind:'router'; true = "nothing to wait for". */
|
|
73
|
+
isInputReady(_pane: string): boolean {
|
|
74
|
+
return true
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
/** No activity proxy: a router writes no transcript/session jsonl to mtime as a
|
|
78
|
+
* ready-gate or idle signal. launch.launch skips the activity-proxy gate for a
|
|
79
|
+
* router; null per the contract ("null for a router (no proxy)"). */
|
|
80
|
+
newestActivityMtime(_cwd: string): number | null {
|
|
81
|
+
return null
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/** Nothing to resume: a router does not replay a transcript, so resume cannot
|
|
85
|
+
* fail-loud the way claude/codex do. Always {ok:true}. */
|
|
86
|
+
resolveResume(_cwd: string): { ok: boolean; ref?: string; reason?: string } {
|
|
87
|
+
return { ok: true }
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/** No in-session control: a router has no TUI turn to interrupt/compact. null for
|
|
91
|
+
* every command → the daemon/CLI surfaces an explicit "unsupported" refusal. */
|
|
92
|
+
executeControl(_command: ControlCommand): ControlPlan | null {
|
|
93
|
+
return null
|
|
94
|
+
},
|
|
95
|
+
}
|
package/src/launch/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { claudeAdapter } from './adapters/claude.ts'
|
|
|
23
23
|
import { codexAdapter } from './adapters/codex.ts'
|
|
24
24
|
import { telegramAdapter } from './adapters/telegram.ts'
|
|
25
25
|
import { notifierAdapter } from './adapters/notifier.ts'
|
|
26
|
+
import { voicetalkAdapter } from './adapters/voicetalk.ts'
|
|
26
27
|
import type {
|
|
27
28
|
LaunchConfig,
|
|
28
29
|
LaunchFn,
|
|
@@ -88,6 +89,7 @@ export function getAdapter(runtime: Runtime): RuntimeAdapter {
|
|
|
88
89
|
if (runtime === 'codex') return codexAdapter
|
|
89
90
|
if (runtime === 'telegram') return telegramAdapter
|
|
90
91
|
if (runtime === 'notifier') return notifierAdapter
|
|
92
|
+
if (runtime === 'voicetalk') return voicetalkAdapter
|
|
91
93
|
throw new Error(`no launch adapter for runtime "${runtime}"`)
|
|
92
94
|
}
|
|
93
95
|
|
package/src/launch/launchdRun.ts
CHANGED
|
@@ -96,6 +96,7 @@ export async function runAlwaysOn(personality: string, runtime: string, cwd: str
|
|
|
96
96
|
// (installAlwaysOnPlist) uses — so the pin and the read can never drift.
|
|
97
97
|
telegramBin: env[INFRA_RUNTIME_BIN_ENV.telegram],
|
|
98
98
|
notifierBin: env[INFRA_RUNTIME_BIN_ENV.notifier],
|
|
99
|
+
voicetalkBin: env[INFRA_RUNTIME_BIN_ENV.voicetalk],
|
|
99
100
|
sockDir,
|
|
100
101
|
bootDeadlineSecs: 30,
|
|
101
102
|
readyGateSecs: 30,
|
package/src/launch/types.ts
CHANGED
|
@@ -134,6 +134,8 @@ export interface LaunchAdapterConfig {
|
|
|
134
134
|
telegramBin?: string
|
|
135
135
|
/** notifier-runtime launch binary (router runtime, infra/always-on). */
|
|
136
136
|
notifierBin?: string
|
|
137
|
+
/** voicetalk-runtime launch binary (router runtime, infra/always-on — the owner's voice channel). */
|
|
138
|
+
voicetalkBin?: string
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
export interface LaunchSpec {
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -1246,42 +1246,41 @@ export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): S
|
|
|
1246
1246
|
trace({ identity: s.identity, action: 'reaped-gone', death: gone.death, reason: gone.reason, outcome: 'fresh-next-msg' })
|
|
1247
1247
|
continue
|
|
1248
1248
|
}
|
|
1249
|
-
// Idle accounting via the
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
//
|
|
1255
|
-
//
|
|
1256
|
-
//
|
|
1257
|
-
//
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
} catch {
|
|
1262
|
-
/* no adapter for this runtime yet → wokeAt baseline */
|
|
1263
|
-
}
|
|
1249
|
+
// Idle accounting via the PANE-LOG (TUI render-stream) mtime — the RELIABLE idle
|
|
1250
|
+
// signal: it advances ~1s while the TUI renders ANY turn (model generation OR a tool
|
|
1251
|
+
// run — the "esc to interrupt" elapsed counter ticks) and goes quiet ONLY at the
|
|
1252
|
+
// prompt. NOT the transcript file mtime: a live claude session RE-TOUCHES its session
|
|
1253
|
+
// .jsonl ~every 30-45min WITHOUT a real turn (a system/bridge re-save), so the
|
|
1254
|
+
// transcript proxy is perpetually < idleSecs and a claude peer NEVER idle-reaps — the
|
|
1255
|
+
// fleet-reap incident: a warm claude pile, last real turn hours ago, never reaped;
|
|
1256
|
+
// codex's session file is NOT re-touched, so codex reaps (the claude-vs-codex split).
|
|
1257
|
+
// The transcript stays the launch ready-gate's signal (it needs "a real turn produced
|
|
1258
|
+
// output"); only this idle proxy moves to the pane-log. FLOORED at wokeAt: a session
|
|
1259
|
+
// cannot be idle longer than it has been awake (a wake that produced no turn →
|
|
1260
|
+
// mt=wokeAt → reaped idleSecs after the wake, never out from under an attaching op).
|
|
1264
1261
|
const ephemeral = isEphemeralPeer(s.cwd)
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1276
|
-
|
|
1277
|
-
|
|
1262
|
+
const plMt = paneLogMt(s.identity)
|
|
1263
|
+
let mt: number
|
|
1264
|
+
if (plMt !== null) {
|
|
1265
|
+
mt = Math.max(plMt, s.wokeAt)
|
|
1266
|
+
} else {
|
|
1267
|
+
// Pane-log MISSING (a legacy pre-pane-log supervisor) → fall back to the transcript
|
|
1268
|
+
// proxy, NEVER to wokeAt-as-activity: a wokeAt-only age would read an ACTIVE legacy
|
|
1269
|
+
// session's idle from its hours-old wokeAt and reap it MID-WORK. The transcript is
|
|
1270
|
+
// false-fresh at idle (so an IDLE legacy peer lingers warm until its next respawn
|
|
1271
|
+
// earns a pane-log — bounded, self-heals), but an ACTIVE legacy peer keeps a
|
|
1272
|
+
// genuinely-fresh transcript and is correctly protected. Safe direction preserved.
|
|
1273
|
+
let tx = 0
|
|
1274
|
+
try { tx = activityMtime(s.runtime, s.cwd) ?? 0 } catch { /* no adapter → wokeAt */ }
|
|
1275
|
+
mt = Math.max(tx, s.wokeAt)
|
|
1278
1276
|
}
|
|
1279
1277
|
const ageSecs = Math.floor((nowMs - mt) / 1000)
|
|
1280
1278
|
// wake_policy:ephemeral M2 — die-after-reply: an ARMED ephemeral session (the
|
|
1281
1279
|
// daemon routed its outbound reply) is reaped after a QUIET window, checked
|
|
1282
|
-
// BEFORE the idle branch (quiet ≪ idle). Quiet =
|
|
1283
|
-
//
|
|
1284
|
-
// so the worker finishes its turn first; post-reply housekeeping
|
|
1280
|
+
// BEFORE the idle branch (quiet ≪ idle). Quiet = the pane-log silent for
|
|
1281
|
+
// ephemeralQuietSecs (the reliable render signal above) — a still-rendering turn
|
|
1282
|
+
// keeps it fresh, so the worker finishes its turn first; post-reply housekeeping
|
|
1283
|
+
// (operative-note writes) also advances it.
|
|
1285
1284
|
// NOT armed (still mid-task) → the ordinary idle bound below is its only reaper.
|
|
1286
1285
|
// Deliberate, policy-driven death: NO .idle-reaped (an ephemeral peer never resumes)
|
|
1287
1286
|
// and NO recordDeath (the crash-loop ring counts faults, not policy reaps).
|
package/src/runtime/deploy.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { readRuntimeManifest, type RuntimeManifest, type RuntimePeerDecl } from
|
|
|
32
32
|
export const RUNTIME_PACKAGES: Readonly<Record<string, string>> = {
|
|
33
33
|
telegram: '@agfpd/telegram-runtime',
|
|
34
34
|
notifier: '@agfpd/notifier-runtime',
|
|
35
|
+
voicetalk: '@agfpd/voicetalk-runtime',
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
/** Resolve the npm package for a runtime: explicit override wins, else the built-in
|