@agfpd/iapeer 0.4.23 → 0.4.25
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 +39 -1
- package/package.json +1 -1
- package/src/connect/index.ts +60 -15
- package/src/daemon/index.ts +9 -0
- package/src/daemon/main.ts +5 -0
- package/src/transport/index.ts +58 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.4.25] - 2026-06-22
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **`connect telegram` now eager-reindexes the registry** (#1). `interface bot` merges
|
|
18
|
+
interfaces.telegram into the per-cwd profile (source of truth), but the derived registry
|
|
19
|
+
(peers-profiles.json) is a projection that only refreshed on specific verbs — so until
|
|
20
|
+
then the peer's telegram FACE was invisible to the sender-guard (`hasTelegramPresence`
|
|
21
|
+
reads the REGISTRY passport), **blocking the peer's outbound telegram** (observed live:
|
|
22
|
+
impact-finder "no telegram face"). connect now calls `reindexFromLocals` after a
|
|
23
|
+
successful `interface bot` (best-effort), so the face lands in the registry immediately —
|
|
24
|
+
no manual `iapeer verify --fix`.
|
|
25
|
+
- **Launchd-revive delivery retry** (#2): a MISS on a launchd-managed target — which the
|
|
26
|
+
daemon can't wake (H4) but launchd KeepAlive revives in ~1s — no longer fails terminally
|
|
27
|
+
in ~16ms. During a router restart (on `connect` / `iapeer update`) an in-flight delivery
|
|
28
|
+
was lost (observed: `natalya→arthur ok=false ms=16` mid-restart — a message to a HUMAN).
|
|
29
|
+
routeSend now retries-resolve for a bounded window (`IAP_LAUNCHD_REVIVE_GRACE_MS`, ~4s)
|
|
30
|
+
and delivers the instant the target revives; it re-resolves each poll (verify-before-act,
|
|
31
|
+
so the ok reflects a CONFIRMED delivery — no false-OK), and a genuinely-down peer still
|
|
32
|
+
fails LOUD and retryable (never silent). `isLaunchdManaged` injected via deps (no
|
|
33
|
+
transport→lifecycle coupling).
|
|
34
|
+
|
|
35
|
+
## [0.4.24] - 2026-06-22
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **`iapeer connect telegram <peer>` (and the `enable telegram` alias) no longer fails for a
|
|
40
|
+
peer whose personality isn't a valid Telegram @username** (e.g. a hyphen: "impact-finder").
|
|
41
|
+
connect passed the PERSONALITY as the bot @username to telegram-runtime (which keys bots by
|
|
42
|
+
`<bot-username>`), so a hyphenated personality was rejected ("must be a telegram @username").
|
|
43
|
+
The bot @username is TOKEN-derived (set in @BotFather), never the personality. connect now
|
|
44
|
+
resolves it from the token via Telegram **getMe** and uses that for `bot add` / `interface
|
|
45
|
+
bot` / storage; the human still owes only the token. An invalid/unreachable token fails
|
|
46
|
+
early with a getMe-rejected message. (The getMe lives in the telegram-glue module
|
|
47
|
+
`connect/index.ts` — the foundation core stays Telegram-API-free.)
|
|
48
|
+
|
|
13
49
|
## [0.4.23] - 2026-06-22
|
|
14
50
|
|
|
15
51
|
### Added
|
|
@@ -352,7 +388,9 @@ Initial public release — the foundation core of the iapeer multi-agent ecosyst
|
|
|
352
388
|
- **Heterogeneous peers** — AI agents (Claude, Codex), humans (Telegram), and services
|
|
353
389
|
(timer = cron, watcher = event) are all first-class and addressed the same way.
|
|
354
390
|
|
|
355
|
-
[Unreleased]: https://github.com/agfpd/iapeer/compare/v0.4.
|
|
391
|
+
[Unreleased]: https://github.com/agfpd/iapeer/compare/v0.4.25...HEAD
|
|
392
|
+
[0.4.25]: https://github.com/agfpd/iapeer/compare/v0.4.24...v0.4.25
|
|
393
|
+
[0.4.24]: https://github.com/agfpd/iapeer/compare/v0.4.23...v0.4.24
|
|
356
394
|
[0.4.23]: https://github.com/agfpd/iapeer/compare/v0.4.22...v0.4.23
|
|
357
395
|
[0.4.22]: https://github.com/agfpd/iapeer/compare/v0.4.21...v0.4.22
|
|
358
396
|
[0.4.21]: https://github.com/agfpd/iapeer/compare/v0.4.20...v0.4.21
|
package/package.json
CHANGED
package/src/connect/index.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
//
|
|
6
6
|
// The human owes EXACTLY ONE external fact: the bot token (prompt walks them
|
|
7
7
|
// through @BotFather → /newbot). Everything else the system resolves: the bot
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// @username via Telegram getMe(token) — the bot key is TOKEN-derived, NOT the peer
|
|
9
|
+
// personality (a personality may contain a hyphen, structurally invalid as a
|
|
10
|
+
// @username; telegram-runtime keys bots by <bot-username>), `telegram-runtime bot add`
|
|
11
|
+
// (also validates the token), `telegram-runtime interface bot` (profile merge;
|
|
12
|
+
// precondition: the peer is registered), then the MANDATORY
|
|
11
13
|
// router-session restart (the live poller reads bots/ ONCE at start, no fs-watch —
|
|
12
14
|
// without the restart the channel is dead both ways), and the activation hint:
|
|
13
15
|
// the FIRST message from the human to the bot opens the chat (a Telegram platform
|
|
@@ -23,6 +25,7 @@ import { spawnSync } from 'child_process'
|
|
|
23
25
|
import { normalizeIntelligenceValue } from '../core/constants.ts'
|
|
24
26
|
import { runtimeRoot } from '../storage/index.ts'
|
|
25
27
|
import { findPeer, readPeersIndex } from '../registry/index.ts'
|
|
28
|
+
import { reindexFromLocals } from '../identity/profileStandard.ts'
|
|
26
29
|
import { readRuntimeManifest } from '../runtime/index.ts'
|
|
27
30
|
|
|
28
31
|
export interface TgRunResult {
|
|
@@ -51,6 +54,9 @@ export interface ConnectTelegramOptions {
|
|
|
51
54
|
runTg?: TgRunner
|
|
52
55
|
/** Injectable router restart (tests). Default: stopPeer→startPeer strictly in order. */
|
|
53
56
|
restart?: RestartFn
|
|
57
|
+
/** Injectable bot-@username resolver (tests). Default: Telegram getMe(token). Returns the
|
|
58
|
+
* bare @username (no leading @) or null on a rejected/unreachable token. */
|
|
59
|
+
resolveUsername?: (token: string) => Promise<string | null>
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
export interface ConnectTelegramResult {
|
|
@@ -60,6 +66,7 @@ export interface ConnectTelegramResult {
|
|
|
60
66
|
| 'refused-no-token' // non-tty and no --token (or an empty tty answer)
|
|
61
67
|
| 'unregistered-peer' // precondition: the peer must be in the registry
|
|
62
68
|
| 'runtime-missing' // no telegram runtime manifest — install it first
|
|
69
|
+
| 'bad-token' // Telegram getMe rejected the token (invalid/revoked) or was unreachable
|
|
63
70
|
| 'bot-add-failed' // telegram-runtime bot add exited non-zero (incl. getMe refusal)
|
|
64
71
|
| 'interface-failed' // telegram-runtime interface bot exited non-zero
|
|
65
72
|
peer: string
|
|
@@ -93,6 +100,26 @@ function readBotEnv(alias: string, env: NodeJS.ProcessEnv): string | null {
|
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the bot's REAL @username from the token via Telegram getMe — the bot identity
|
|
105
|
+
* is TOKEN-derived (BotFather sets it), NOT the peer personality. telegram-runtime keys
|
|
106
|
+
* bots by `<bot-username>` (`bot add <bot-username>`, `interface bot <bot-username>`), and a
|
|
107
|
+
* personality can contain a hyphen which is structurally invalid as a @username — so the
|
|
108
|
+
* personality can NEVER be the bot-username. Returns the bare username (no leading @) or
|
|
109
|
+
* null on a rejected/unreachable token. The token goes literally in the URL path
|
|
110
|
+
* (Telegram tokens are `<digits>:<urlsafe>`; no encoding).
|
|
111
|
+
*/
|
|
112
|
+
async function defaultResolveBotUsername(token: string): Promise<string | null> {
|
|
113
|
+
try {
|
|
114
|
+
const r = await fetch(`https://api.telegram.org/bot${token}/getMe`)
|
|
115
|
+
const j = (await r.json()) as { ok?: boolean; result?: { username?: unknown } }
|
|
116
|
+
const u = j?.ok === true && typeof j.result?.username === 'string' ? j.result.username.trim().replace(/^@/, '') : ''
|
|
117
|
+
return u || null
|
|
118
|
+
} catch {
|
|
119
|
+
return null // network error / non-JSON → treat as unresolvable (bad-token)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
96
123
|
/** Resolve the telegram-runtime CLI bin from the manifest's selfConfig command (the
|
|
97
124
|
* package's own absolute bin — PATH-independent, launchd-safe). */
|
|
98
125
|
function resolveTgBin(env: NodeJS.ProcessEnv): string | null {
|
|
@@ -173,8 +200,17 @@ export async function connectTelegram(opts: ConnectTelegramOptions): Promise<Con
|
|
|
173
200
|
if (!token) return { state: 'refused-no-token', peer, detail: 'no answer — nothing connected' }
|
|
174
201
|
}
|
|
175
202
|
|
|
203
|
+
// Resolve the bot's REAL @username from the token (getMe) — NOT the personality. The
|
|
204
|
+
// bot-username is token-derived; a personality with a hyphen (e.g. "impact-finder") is
|
|
205
|
+
// structurally invalid as a @username and telegram-runtime (which keys bots by
|
|
206
|
+
// <bot-username>) rejects it. getMe also validates the token early (bad token → fail here).
|
|
207
|
+
const resolveUsername = opts.resolveUsername ?? defaultResolveBotUsername
|
|
208
|
+
const botUsername = await resolveUsername(token)
|
|
209
|
+
if (!botUsername) {
|
|
210
|
+
return { state: 'bad-token', peer, detail: 'Telegram getMe rejected the token (invalid/revoked) or was unreachable — recheck the token with @BotFather' }
|
|
211
|
+
}
|
|
176
212
|
const runTg = opts.runTg ?? defaultRunTg(tgBin)
|
|
177
|
-
const alias =
|
|
213
|
+
const alias = botUsername // bot key = the REAL @username from getMe (telegram-runtime keys bots by <bot-username>)
|
|
178
214
|
const before = readBotEnv(alias, env)
|
|
179
215
|
|
|
180
216
|
// (1) bot add — token → bots/<alias>/.env. Owner adds getMe validation here: an
|
|
@@ -183,17 +219,10 @@ export async function connectTelegram(opts: ConnectTelegramOptions): Promise<Con
|
|
|
183
219
|
if (add.status !== 0) {
|
|
184
220
|
return { state: 'bot-add-failed', peer, detail: (add.stderr || add.stdout || `exit ${add.status}`).trim() }
|
|
185
221
|
}
|
|
186
|
-
// @username: the
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
const envAfterAdd = readBotEnv(alias, env)
|
|
191
|
-
const envUser = envAfterAdd?.match(/^TELEGRAM_BOT_USERNAME=(.+)$/m)?.[1]?.trim()
|
|
192
|
-
const username = envUser
|
|
193
|
-
? envUser.startsWith('@')
|
|
194
|
-
? envUser
|
|
195
|
-
: `@${envUser}`
|
|
196
|
-
: add.stdout.match(/@[A-Za-z0-9_]{3,}/)?.[0]
|
|
222
|
+
// @username for display / the activation hint: the bot key (alias) IS the
|
|
223
|
+
// getMe-resolved username, so the display form is simply `@<alias>` — no need to
|
|
224
|
+
// re-parse bots/<alias>/.env (telegram-runtime writes the same value there).
|
|
225
|
+
const username = `@${alias}`
|
|
197
226
|
|
|
198
227
|
// (2) interface bot — merge the channel binding into the peer's profile.
|
|
199
228
|
const iface = runTg(['interface', 'bot', alias, '--peer', peer], env)
|
|
@@ -201,6 +230,22 @@ export async function connectTelegram(opts: ConnectTelegramOptions): Promise<Con
|
|
|
201
230
|
return { state: 'interface-failed', peer, username, detail: (iface.stderr || iface.stdout || `exit ${iface.status}`).trim() }
|
|
202
231
|
}
|
|
203
232
|
|
|
233
|
+
// (2b) EAGER reindex — `interface bot` merged interfaces.telegram into the per-cwd
|
|
234
|
+
// profile (source of truth), but the derived registry (peers-profiles.json) is a
|
|
235
|
+
// PROJECTION that only refreshes on specific verbs (verify --fix, default-runtime…).
|
|
236
|
+
// Until then the peer's telegram FACE is INVISIBLE to the sender-guard
|
|
237
|
+
// (hasTelegramPresence reads the REGISTRY passport) — so the peer's OUTBOUND telegram
|
|
238
|
+
// is blocked ("no telegram face") + list/attach are stale. Project it now (same
|
|
239
|
+
// reindexFromLocals the default-runtime verb uses). Best-effort: a reindex hiccup must
|
|
240
|
+
// not fail a connect whose profile + bot are already in place (registry catches up on
|
|
241
|
+
// the next reindex verb). Runs on EVERY connect, incl. the idempotent no-op below — so
|
|
242
|
+
// a registry that drifted on a prior (pre-fix) connect self-heals here too.
|
|
243
|
+
try {
|
|
244
|
+
await reindexFromLocals({ env })
|
|
245
|
+
} catch {
|
|
246
|
+
/* registry lazily catches up on the next reindex verb; profile + bot are correct */
|
|
247
|
+
}
|
|
248
|
+
|
|
204
249
|
// (3) Idempotency gate: the SAME token leaves bots/<alias>/.env byte-stable →
|
|
205
250
|
// the live poller already loaded these credentials → NO restart, clean no-op.
|
|
206
251
|
const after = readBotEnv(alias, env)
|
package/src/daemon/index.ts
CHANGED
|
@@ -194,6 +194,10 @@ export interface CallToolDeps {
|
|
|
194
194
|
noteLiveTopic?: (identity: string, topic: string) => void
|
|
195
195
|
/** Busy-human-composer queue — see transport.createComposerDeliveryQueue. */
|
|
196
196
|
composerQueue?: ComposerQueueRouteDeps
|
|
197
|
+
/** H4 launchd-managed detector — see transport.RouteDeps.isLaunchdManaged (the
|
|
198
|
+
* launchd-revive delivery retry). Injected so the daemon never imports lifecycle;
|
|
199
|
+
* the production main wires the real isLaunchdManaged. */
|
|
200
|
+
isLaunchdManaged?: (personality: string) => boolean
|
|
197
201
|
}
|
|
198
202
|
|
|
199
203
|
export async function callTool(
|
|
@@ -216,6 +220,7 @@ export async function callTool(
|
|
|
216
220
|
ephemeral: deps.ephemeral,
|
|
217
221
|
noteLiveTopic: deps.noteLiveTopic,
|
|
218
222
|
composerQueue: deps.composerQueue,
|
|
223
|
+
isLaunchdManaged: deps.isLaunchdManaged,
|
|
219
224
|
})
|
|
220
225
|
// Ф-#8a: ONE durable outcome line per delivery attempt (delivery.log, sibling
|
|
221
226
|
// to lifecycle.log) — metadata only, never the body. Both branches of the
|
|
@@ -392,6 +397,9 @@ export interface StartDaemonOptions extends StorageOptions {
|
|
|
392
397
|
* senders on shutdown/restart.
|
|
393
398
|
*/
|
|
394
399
|
composerQueue?: ComposerQueueRouteDeps
|
|
400
|
+
/** H4 launchd-managed detector — see CallToolDeps.isLaunchdManaged / the launchd-revive
|
|
401
|
+
* delivery retry. OFF by default (library/tests); production main wires the real one. */
|
|
402
|
+
isLaunchdManaged?: (personality: string) => boolean
|
|
395
403
|
/**
|
|
396
404
|
* Write the discovery file (router.json) at <root>/state/iapeer/router.json with
|
|
397
405
|
* the active addresses `{sock, tcp}` — atomically on listen, removed on close.
|
|
@@ -426,6 +434,7 @@ export async function startDaemon(opts: StartDaemonOptions = {}): Promise<Daemon
|
|
|
426
434
|
ephemeral: opts.ephemeral,
|
|
427
435
|
noteLiveTopic: opts.noteLiveTopic,
|
|
428
436
|
composerQueue: opts.composerQueue,
|
|
437
|
+
isLaunchdManaged: opts.isLaunchdManaged,
|
|
429
438
|
})
|
|
430
439
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })
|
|
431
440
|
res.on('close', () => {
|
package/src/daemon/main.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
drainEphemeralQueue,
|
|
36
36
|
enqueueEphemeralTask,
|
|
37
37
|
isEphemeralPeer,
|
|
38
|
+
isLaunchdManaged,
|
|
38
39
|
addTopic,
|
|
39
40
|
loadLifecycleConfig,
|
|
40
41
|
processEagerRelaunches,
|
|
@@ -332,6 +333,10 @@ export async function startConfiguredDaemon(opts: ConfiguredDaemonOptions = {}):
|
|
|
332
333
|
// Busy-human-composer queue: fast queued ack, async drain, fail all pending
|
|
333
334
|
// on daemon close/update so a queued envelope is never silently lost.
|
|
334
335
|
composerQueue,
|
|
336
|
+
// H4 launchd-revive delivery retry (see RouteDeps.isLaunchdManaged): a MISS on a
|
|
337
|
+
// launchd-managed target (the daemon can't wake it, but KeepAlive revives it) → retry-
|
|
338
|
+
// resolve for a bounded window instead of failing in ~16ms — bridges a router restart.
|
|
339
|
+
isLaunchdManaged: (personality: string) => isLaunchdManaged(personality, env),
|
|
335
340
|
supervise: {
|
|
336
341
|
intervalMs: opts.superviseIntervalMs ?? DEFAULT_SUPERVISE_INTERVAL_MS,
|
|
337
342
|
// idle-reap / zombie-sweep, THEN the eager fresh re-launch for any peer whose
|
package/src/transport/index.ts
CHANGED
|
@@ -176,6 +176,17 @@ function hostLivenessGraceMs(): number {
|
|
|
176
176
|
return Number.isFinite(n) && n >= 0 ? n : livenessGraceMs()
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// Launchd-revive retry — a MISS on a launchd-managed target the daemon can't wake (H4)
|
|
180
|
+
// but launchd KeepAlive WILL revive (router restart / crash-revive window). routeSend
|
|
181
|
+
// re-resolves for up to this window before failing, bridging the gap (silent-loss guard).
|
|
182
|
+
// Env-tunable; default ~4s (a bootout→bootstrap router restart settles well under it).
|
|
183
|
+
const LAUNCHD_REVIVE_POLL_MS = 300
|
|
184
|
+
function launchdReviveGraceMs(): number {
|
|
185
|
+
const raw = process.env.IAP_LAUNCHD_REVIVE_GRACE_MS
|
|
186
|
+
const n = raw === undefined ? NaN : Number(raw)
|
|
187
|
+
return Number.isFinite(n) && n >= 0 ? n : 4000
|
|
188
|
+
}
|
|
189
|
+
|
|
179
190
|
// ─── Warm delivery (pty-only — supervisor socket) ────────────────────────────────────────────────
|
|
180
191
|
|
|
181
192
|
/** Test seam for `deliverWarm`. Production defaults route on the real supervisor; tests inject a
|
|
@@ -793,6 +804,17 @@ export interface RouteDeps {
|
|
|
793
804
|
* imports lifecycle (layering — main.ts is where the two meet); call sites
|
|
794
805
|
* swallow hook errors (a hook must never fail a delivered message). */
|
|
795
806
|
noteLiveTopic?: (identity: string, topic: string) => void
|
|
807
|
+
/** H4 launchd-managed detector (injected so transport never imports lifecycle —
|
|
808
|
+
* main.ts wires the real `isLaunchdManaged`). A MISS on a launchd-managed target is
|
|
809
|
+
* almost always a TRANSIENT restart window (router restart on connect / `iapeer
|
|
810
|
+
* update`, or a crash that KeepAlive is reviving), NOT a real outage: the daemon
|
|
811
|
+
* can't wake it (H4) but launchd WILL revive it in ~1s. So routeSend retries the
|
|
812
|
+
* RESOLVE for a bounded window before failing, bridging the gap (prevents the
|
|
813
|
+
* silent-loss class: an in-flight delivery — esp. to a HUMAN over a telegram router —
|
|
814
|
+
* dropped during a restart). Absent → no retry (legacy behaviour). */
|
|
815
|
+
isLaunchdManaged?: (personality: string) => boolean
|
|
816
|
+
/** Injectable async sleep for the launchd-revive retry poll (tests). Default: setTimeout. */
|
|
817
|
+
sleep?: (ms: number) => Promise<void>
|
|
796
818
|
}
|
|
797
819
|
|
|
798
820
|
/** Best-effort noteLiveTopic invocation — only on a non-empty topic, never throws. */
|
|
@@ -1159,6 +1181,42 @@ export async function routeSend(
|
|
|
1159
1181
|
|
|
1160
1182
|
// MISS — peer offline.
|
|
1161
1183
|
if (!deps.wake) return target // Ф1: explicit offline, no wake
|
|
1184
|
+
// LAUNCHD-REVIVE RETRY: a launchd-managed target the daemon can't wake (H4) but launchd
|
|
1185
|
+
// KeepAlive WILL revive — a MISS here is almost always a transient RESTART WINDOW (router
|
|
1186
|
+
// restart on connect / `iapeer update`, or a crash-revive), not a real outage. Wake would
|
|
1187
|
+
// just refuse (launchd-managed) and the delivery would fail in ~16ms — losing an in-flight
|
|
1188
|
+
// message (observed: natalya→arthur ok=false ms=16 during a connect router restart; a
|
|
1189
|
+
// message to a HUMAN, the silent-loss-adjacent class). Bridge it: re-resolve for a bounded
|
|
1190
|
+
// window and deliver the instant it revives. Each poll RE-RESOLVES the live target
|
|
1191
|
+
// (verify-before-act) → the ok reflects a CONFIRMED delivery (no false-OK); a genuinely
|
|
1192
|
+
// down peer still fails LOUD after the window (retryable, never silent).
|
|
1193
|
+
if (deps.isLaunchdManaged?.(personality)) {
|
|
1194
|
+
const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)))
|
|
1195
|
+
const reviveDeadline = monotonicMs() + launchdReviveGraceMs()
|
|
1196
|
+
while (monotonicMs() < reviveDeadline) {
|
|
1197
|
+
await sleep(LAUNCHD_REVIVE_POLL_MS)
|
|
1198
|
+
const revived = resolvePeerDeliveryTarget(personality, runtime, peer)
|
|
1199
|
+
if (!revived.ok) continue // not back yet — keep polling until the deadline
|
|
1200
|
+
if (revived.value.address === caller.address) return err('cannot send to self')
|
|
1201
|
+
const reviveGuard = telegramSenderGuard(caller, revived.value.runtime, personality)
|
|
1202
|
+
if (!reviveGuard.ok) return reviveGuard
|
|
1203
|
+
const delivered = await deliverWarm(revived.value, envelope, peer.cwd)
|
|
1204
|
+
if (delivered.ok) {
|
|
1205
|
+
noteTopic(deps, revived.value.address, topic)
|
|
1206
|
+
return ok({
|
|
1207
|
+
ok: true,
|
|
1208
|
+
delivered_to: { personality: revived.value.personality, runtime: revived.value.runtime },
|
|
1209
|
+
woke: false,
|
|
1210
|
+
ts: new Date().toISOString(),
|
|
1211
|
+
})
|
|
1212
|
+
}
|
|
1213
|
+
// resolved but the deliver didn't land (session still settling) → keep polling
|
|
1214
|
+
}
|
|
1215
|
+
return err(
|
|
1216
|
+
`peer "${personality}" (launchd-managed) offline and did not revive within ${launchdReviveGraceMs()}ms ` +
|
|
1217
|
+
`(restart window outlasted the grace?) — message NOT delivered; retry`,
|
|
1218
|
+
)
|
|
1219
|
+
}
|
|
1162
1220
|
const woke = await deps.wake({ personality, runtime, topic, task: envelope })
|
|
1163
1221
|
if (woke.status === 'FAILED') {
|
|
1164
1222
|
// C1 — a durably STOPPED peer is a deliberate halt, not a transient miss:
|