@agfpd/iapeer 0.4.86 → 0.4.87
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/package.json +1 -1
- package/src/codec/index.ts +23 -13
- package/src/core/constants.ts +7 -4
- package/src/identity/index.ts +25 -8
- package/src/launch/adapters/web.ts +107 -0
- package/src/launch/index.ts +3 -0
- package/src/launch/launchd.ts +30 -0
- package/src/launch/launchdRun.ts +1 -0
- package/src/launch/types.ts +2 -0
- package/src/lifecycle/index.ts +10 -3
- package/src/runtime/deploy.ts +1 -0
- package/src/transport/index.ts +7 -5
package/package.json
CHANGED
package/src/codec/index.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
// IAP envelope codec — encoder +
|
|
1
|
+
// IAP envelope codec — wire encoder + read-both decoder + agent-facing render.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Three halves (envelope-compaction F, 0.4.86):
|
|
4
|
+
// buildEnvelope → the WIRE form (legacy attr names, always-CDATA, full-ISO ts) —
|
|
5
|
+
// what bridges parse and durable stores hold; never slims.
|
|
6
|
+
// decodeEnvelope → READ-BOTH (legacy + compact names, <message>/<msg>, ts),
|
|
7
|
+
// CDATA-AWARE so a body containing `</iap>`, `</message>`,
|
|
8
|
+
// `]]>` or a literal CR round-trips by FIELD-semantic
|
|
9
|
+
// equivalence, not byte-equality.
|
|
10
|
+
// renderEnvelopeForAgent → the compact presentation for LLM-context hops (bottom).
|
|
11
|
+
//
|
|
12
|
+
// Historic origin: encoder from inter-agent-protocol preamble.ts; decoder from
|
|
13
|
+
// telegram-runtime parseIapEnvelope/extractIapEnvelopes. The sibling runtimes run
|
|
14
|
+
// their own copies of the decoder, SYNCED to this canon 14.07.2026 (telegram-runtime
|
|
15
|
+
// 0.26.0, notifier-runtime 0.3.6, voicetalk-runtime 0.1.1 — anchored attrValue, В37/В38,
|
|
16
|
+
// read-both); a decoder-behavior change here should be mirrored there (or flagged to
|
|
17
|
+
// their implementers). The wire form stays compatible with all of them regardless.
|
|
8
18
|
//
|
|
9
19
|
// Deliberately NO CR→LF fold here. The old telegram decoder did
|
|
10
20
|
// `xml.replace(/\r\n?/g, '\n')` to repair tmux paste (which rewrites LF→CR).
|
|
@@ -45,19 +55,20 @@ export interface EnvelopeInput {
|
|
|
45
55
|
message: string
|
|
46
56
|
}
|
|
47
57
|
|
|
58
|
+
const pad2 = (n: number): string => String(n).padStart(2, '0')
|
|
59
|
+
|
|
48
60
|
/** Format a Date as ISO-8601 LOCAL time with numeric offset, second precision:
|
|
49
61
|
* `2026-07-14T01:23:45+03:00`. Local (not UTC-Z) on purpose: the fleet's agents
|
|
50
62
|
* compare envelope timestamps against their LOCAL statusline clock — a Z-form
|
|
51
63
|
* invites silent off-by-offset misreads by an LLM reader. */
|
|
52
64
|
export function formatSentAt(date: Date): string {
|
|
53
|
-
const pad = (n: number): string => String(n).padStart(2, '0')
|
|
54
65
|
const offMin = -date.getTimezoneOffset() // minutes EAST of UTC
|
|
55
66
|
const sign = offMin >= 0 ? '+' : '-'
|
|
56
67
|
const abs = Math.abs(offMin)
|
|
57
68
|
return (
|
|
58
|
-
`${date.getFullYear()}-${
|
|
59
|
-
`T${
|
|
60
|
-
`${sign}${
|
|
69
|
+
`${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}` +
|
|
70
|
+
`T${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}` +
|
|
71
|
+
`${sign}${pad2(Math.floor(abs / 60))}:${pad2(abs % 60)}`
|
|
61
72
|
)
|
|
62
73
|
}
|
|
63
74
|
|
|
@@ -351,11 +362,10 @@ function presentBody(value: string): string {
|
|
|
351
362
|
function renderTsForAgent(sentAt: string, now: Date): string {
|
|
352
363
|
const d = new Date(sentAt)
|
|
353
364
|
if (Number.isNaN(d.getTime())) return sentAt
|
|
354
|
-
const
|
|
355
|
-
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
365
|
+
const time = `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
|
|
356
366
|
const sameLocalDay =
|
|
357
367
|
d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()
|
|
358
|
-
return sameLocalDay ? time : `${d.getFullYear()}-${
|
|
368
|
+
return sameLocalDay ? time : `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${time}`
|
|
359
369
|
}
|
|
360
370
|
|
|
361
371
|
/**
|
package/src/core/constants.ts
CHANGED
|
@@ -89,11 +89,12 @@ export function isIntelligenceAllowedForRuntime(runtime: string, intelligence: I
|
|
|
89
89
|
// Infra runtimes are ALWAYS-ON (held live by launchd KeepAlive), as opposed to the
|
|
90
90
|
// warm-on-demand agentic runtimes (claude/codex, woken by the daemon). Liveness is
|
|
91
91
|
// a property of the RUNTIME, not the personality (zone Идентичность). Infra runtimes
|
|
92
|
-
// are routers hosted by the pty supervisor: telegram/voicetalk carry a human
|
|
93
|
-
// (inbound messages / voice), notifier receives
|
|
94
|
-
// registration/live-reload — outbound to each is
|
|
92
|
+
// are routers hosted by the pty supervisor: telegram/voicetalk/web carry a human
|
|
93
|
+
// channel (inbound messages / voice / browser console), notifier receives
|
|
94
|
+
// send_to_peer(timer|watcher, …) registration/live-reload — outbound to each is
|
|
95
|
+
// deliverHosted → the child's stdin.
|
|
95
96
|
// This set gates always-on launchd plist generation (src/launch/launchd.ts).
|
|
96
|
-
const INFRA_RUNTIMES = new Set(['notifier', 'telegram', 'voicetalk'])
|
|
97
|
+
const INFRA_RUNTIMES = new Set(['notifier', 'telegram', 'voicetalk', 'web'])
|
|
97
98
|
|
|
98
99
|
export function isInfraRuntime(runtime: string): boolean {
|
|
99
100
|
return INFRA_RUNTIMES.has(runtime)
|
|
@@ -111,6 +112,7 @@ export const INFRA_RUNTIME_BIN_ENV: Readonly<Record<string, string>> = {
|
|
|
111
112
|
notifier: 'NOTIFIER_RUNTIME_BIN',
|
|
112
113
|
telegram: 'TELEGRAM_RUNTIME_BIN',
|
|
113
114
|
voicetalk: 'VOICETALK_RUNTIME_BIN',
|
|
115
|
+
web: 'WEB_RUNTIME_BIN',
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
/** The PATH-resolvable default launcher name per infra runtime (when no abs path
|
|
@@ -120,6 +122,7 @@ export const INFRA_RUNTIME_DEFAULT_BIN: Readonly<Record<string, string>> = {
|
|
|
120
122
|
notifier: 'notifier-runtime',
|
|
121
123
|
telegram: 'telegram-runtime',
|
|
122
124
|
voicetalk: 'voicetalk-runtime',
|
|
125
|
+
web: 'web-runtime',
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
// codex MCP token-free import (contract Установка §codex MCP). codex REFUSES to import
|
package/src/identity/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
// KeepAlive, so creating one installs its always-on plist. Imported from the
|
|
27
27
|
// launchd module directly (not the launch barrel) to keep the dependency surface
|
|
28
28
|
// minimal — launchd.ts pulls only core/*, so identity → launch introduces no cycle.
|
|
29
|
-
import { installAlwaysOnPlist } from '../launch/launchd.ts'
|
|
29
|
+
import { installAlwaysOnPlist, installedPlistRuntime } from '../launch/launchd.ts'
|
|
30
30
|
import { buildProcessAddress } from '../core/socket.ts'
|
|
31
31
|
import { IapError } from '../core/errors.ts'
|
|
32
32
|
import {
|
|
@@ -623,13 +623,30 @@ export function ensurePeerProfile(options: EnsurePeerProfileOptions): PeerProfil
|
|
|
623
623
|
// and leaves no half-updated profile. Idempotent for our own (sentinel) plist.
|
|
624
624
|
let intelligence = existing.intelligence
|
|
625
625
|
if (isInfraRuntime(options.runtime)) {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
626
|
+
// MULTI-INFRA BRIDGE: the always-on plist scheme is ONE per personality, so when the
|
|
627
|
+
// personality's plist is already OCCUPIED by ANOTHER infra runtime (arthur: telegram,
|
|
628
|
+
// now declaring web), installing here would clobber the live channel — and the
|
|
629
|
+
// installAlwaysOnPlist collision guard would fail the whole declaration. Instead:
|
|
630
|
+
// DECLARE the runtime (runtimes += <rt> below — identity `<rt>-<p>` resolves, delivery
|
|
631
|
+
// to a LIVE hosted session works) but leave the first channel's plist untouched. The
|
|
632
|
+
// new runtime's session is NOT launchd-owned until per-runtime plists (multi-infra)
|
|
633
|
+
// land — bring it up manually via `iapeer run-infra <p> <rt>` meanwhile.
|
|
634
|
+
const occupied = installedPlistRuntime(existing.personality, options.env)
|
|
635
|
+
if (occupied && occupied !== options.runtime) {
|
|
636
|
+
options.warn?.(
|
|
637
|
+
`always-on plist for "${existing.personality}" already holds runtime "${occupied}" — ` +
|
|
638
|
+
`"${options.runtime}" is DECLARED but gets no launchd plist (one plist per personality); ` +
|
|
639
|
+
`until per-runtime plists land, bring the session up manually: iapeer run-infra ${existing.personality} ${options.runtime}`,
|
|
640
|
+
)
|
|
641
|
+
} else {
|
|
642
|
+
installAlwaysOnPlist({
|
|
643
|
+
personality: existing.personality,
|
|
644
|
+
runtime: options.runtime,
|
|
645
|
+
cwd,
|
|
646
|
+
runtimeBin: options.runtimeBin,
|
|
647
|
+
env: options.env,
|
|
648
|
+
})
|
|
649
|
+
}
|
|
633
650
|
// Re-provision honors an EXPLICIT nature (a manifest-declared absent service bot survives
|
|
634
651
|
// update-runtime re-provision), else the runtime default (the legacy vocab-flip this fix
|
|
635
652
|
// preserved). resolvedIntelligence validates the explicit against the runtime's allowed set.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// web RuntimeAdapter — the "HOW to launch / observe ONE web session" half of the
|
|
2
|
+
// launch contract (src/launch/types.ts). Like telegram/voicetalk, web is a
|
|
3
|
+
// long-running ROUTER (`web-runtime run`: the owner's browser fleet-console —
|
|
4
|
+
// local HTTP/WebSocket server the browser client connects to → IAP envelopes to
|
|
5
|
+
// the fleet; <iap>-marked stdin → rendered into the console chat), NOT an LLM TUI.
|
|
6
|
+
// So kind:'router' tells launch.launch to SKIP every TUI phase — no pane boot
|
|
7
|
+
// dialog, no ready-marker, no first-user-turn delivery, no activity-proxy
|
|
8
|
+
// ready-gate, no permission modal, no transcript to resume. The pane predicates
|
|
9
|
+
// below are therefore trivial constants, present only to satisfy the frozen
|
|
10
|
+
// interface.
|
|
11
|
+
//
|
|
12
|
+
// web is a presence-runtime for a HUMAN (the owner's browser console), modelled on
|
|
13
|
+
// telegram/voicetalk per the foundation presence-runtime contract: kind:'router',
|
|
14
|
+
// usesDoctrine:false, allowedIntelligences:['natural','absent'], argv = `<bin> run
|
|
15
|
+
// <extra>`. The server/client mechanics (port, tunnel, auth) live in the
|
|
16
|
+
// @agfpd/web-runtime package, NOT in this adapter and NOT interpreted by the
|
|
17
|
+
// foundation — interfaces.web (if the package declares one) is opaque here.
|
|
18
|
+
//
|
|
19
|
+
// NO sender-guard for web (the voicetalk precedent): telegramSenderGuard is
|
|
20
|
+
// telegram-specific (the bridge picks the SENDING bot from the sender's
|
|
21
|
+
// bot_username); web is a single local owner endpoint — the console renders the
|
|
22
|
+
// sender from the envelope's `from`, so any peer may write to it freely.
|
|
23
|
+
//
|
|
24
|
+
// NO currency on this path: no marketplace check, no plugin install/update — that
|
|
25
|
+
// is install-time (the package self-deploys its runtime.json manifest), not
|
|
26
|
+
// session bring-up.
|
|
27
|
+
|
|
28
|
+
import type { ControlCommand, ControlPlan, LaunchAdapterConfig, LaunchSpec, RuntimeAdapter } from '../types.ts'
|
|
29
|
+
import { allowedIntelligencesForRuntime } from '../../core/constants.ts'
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// webAdapter
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export const webAdapter: RuntimeAdapter = {
|
|
36
|
+
runtime: 'web',
|
|
37
|
+
kind: 'router',
|
|
38
|
+
usesDoctrine: false,
|
|
39
|
+
|
|
40
|
+
/** No submit surface — a router takes no bracketed-paste-then-Enter turn; delivery
|
|
41
|
+
* is deliverHosted → child stdin (<iap> reader), not submitIntoTui. Empty glyphs. */
|
|
42
|
+
deliveryMarkers: { promptGlyphs: [] },
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* web carries a HUMAN (natural) OR a FACELESS SERVICE bot (absent) — never an LLM
|
|
46
|
+
* agent (artificial), which would route the owner's console channel to an agent/script.
|
|
47
|
+
* Same nature gate as telegram/voicetalk, sourced from the single Ф0 truth
|
|
48
|
+
* (allowedIntelligencesForRuntime); enforced by the launch primitive against
|
|
49
|
+
* LaunchSpec.intelligence. Source → docs/Идентичность.
|
|
50
|
+
*/
|
|
51
|
+
allowedIntelligences: allowedIntelligencesForRuntime('web'),
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* argv = webBin + 'run' + extraArgs.
|
|
55
|
+
* - cfg.webBin ?? 'web-runtime' the launch binary (the always-on plist pins an
|
|
56
|
+
* absolute path via WEB_RUNTIME_BIN — launchd-minimal PATH; here we defer to
|
|
57
|
+
* cfg.webBin when set, else emit the PATH-resolvable literal for launch.launch
|
|
58
|
+
* to resolve).
|
|
59
|
+
* - 'run' the router subcommand: brings up the console server (browser client ↔
|
|
60
|
+
* fleet) and pumps IAP envelopes both directions through the session's stdio
|
|
61
|
+
* (outbound <iap> stdin → console chat; inbound browser input → fleet via the
|
|
62
|
+
* daemon send_to_peer).
|
|
63
|
+
* - ...extraArgs LaunchSpec.extraArgs passthrough.
|
|
64
|
+
*
|
|
65
|
+
* NO --system-prompt-file: usesDoctrine:false — a router is not an LLM, no doctrine to
|
|
66
|
+
* merge. NO --resume: a router has no transcript. NO currency on this path.
|
|
67
|
+
*/
|
|
68
|
+
buildArgv(spec: LaunchSpec, cfg: LaunchAdapterConfig): string[] {
|
|
69
|
+
return [cfg.webBin ?? 'web-runtime', 'run', ...(spec.extraArgs ?? [])]
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/** No startup dialogs — a router has no pane TUI to answer. launch.launch skips the
|
|
73
|
+
* boot phase for kind:'router', so this is never consulted; null = "no dialog". */
|
|
74
|
+
bootDialogKeys(_pane: string): string[] | null {
|
|
75
|
+
return null
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/** Always ready: a router has no input surface waiting for a first message — it is
|
|
79
|
+
* "up" the instant `web-runtime run` is launched. launch.launch skips the
|
|
80
|
+
* ready-gate for kind:'router'; true = "nothing to wait for". */
|
|
81
|
+
isInputReady(_pane: string): boolean {
|
|
82
|
+
return true
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/** No activity proxy: a router writes no transcript/session jsonl to mtime as a
|
|
86
|
+
* ready-gate or idle signal. launch.launch skips the activity-proxy gate for a
|
|
87
|
+
* router; null per the contract ("null for a router (no proxy)"). */
|
|
88
|
+
newestActivityMtime(_cwd: string): number | null {
|
|
89
|
+
return null
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
lastTurnMtime(_cwd: string): number | null {
|
|
93
|
+
return null // router — no transcript
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/** Nothing to resume: a router does not replay a transcript, so resume cannot
|
|
97
|
+
* fail-loud the way claude/codex do. Always {ok:true}. */
|
|
98
|
+
resolveResume(_cwd: string): { ok: boolean; ref?: string; reason?: string } {
|
|
99
|
+
return { ok: true }
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/** No in-session control: a router has no TUI turn to interrupt/compact. null for
|
|
103
|
+
* every command → the daemon/CLI surfaces an explicit "unsupported" refusal. */
|
|
104
|
+
executeControl(_command: ControlCommand): ControlPlan | null {
|
|
105
|
+
return null
|
|
106
|
+
},
|
|
107
|
+
}
|
package/src/launch/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { codexAdapter } from './adapters/codex.ts'
|
|
|
24
24
|
import { telegramAdapter } from './adapters/telegram.ts'
|
|
25
25
|
import { notifierAdapter } from './adapters/notifier.ts'
|
|
26
26
|
import { voicetalkAdapter } from './adapters/voicetalk.ts'
|
|
27
|
+
import { webAdapter } from './adapters/web.ts'
|
|
27
28
|
import type {
|
|
28
29
|
LaunchConfig,
|
|
29
30
|
LaunchFn,
|
|
@@ -56,6 +57,7 @@ export {
|
|
|
56
57
|
launchdPlistPath,
|
|
57
58
|
renderLaunchdPlist,
|
|
58
59
|
installAlwaysOnPlist,
|
|
60
|
+
installedPlistRuntime,
|
|
59
61
|
isFoundationOwnedPlist,
|
|
60
62
|
launchctlBootstrap,
|
|
61
63
|
bootstrapJobCore,
|
|
@@ -90,6 +92,7 @@ export function getAdapter(runtime: Runtime): RuntimeAdapter {
|
|
|
90
92
|
if (runtime === 'telegram') return telegramAdapter
|
|
91
93
|
if (runtime === 'notifier') return notifierAdapter
|
|
92
94
|
if (runtime === 'voicetalk') return voicetalkAdapter
|
|
95
|
+
if (runtime === 'web') return webAdapter
|
|
93
96
|
throw new Error(`no launch adapter for runtime "${runtime}"`)
|
|
94
97
|
}
|
|
95
98
|
|
package/src/launch/launchd.ts
CHANGED
|
@@ -81,6 +81,20 @@ function readPlistEnvVar(path: string, key: string): string | undefined {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* The PEER_RUNTIME baked into the personality's EXISTING foundation-owned always-on
|
|
86
|
+
* plist, or undefined when there is no plist / it is foreign-owned / it carries no
|
|
87
|
+
* runtime. The plist scheme is ONE per personality (`com.iapeer.<p>.plist` — the
|
|
88
|
+
* runtime rides only in ProgramArguments/env), so this is how a caller detects that
|
|
89
|
+
* the personality's always-on slot is already OCCUPIED by another infra runtime
|
|
90
|
+
* (e.g. arthur's telegram) before asking for a second one (multi-infra collision).
|
|
91
|
+
*/
|
|
92
|
+
export function installedPlistRuntime(personality: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
93
|
+
const path = launchdPlistPath(personality, env)
|
|
94
|
+
if (!existsSync(path) || !isFoundationOwnedPlist(path)) return undefined
|
|
95
|
+
return readPlistEnvVar(path, 'PEER_RUNTIME')
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
/**
|
|
85
99
|
* Resolve an executable to an ABSOLUTE path against `env.PATH` (the RICH
|
|
86
100
|
* provisioning PATH), so the result can be baked into a launchd plist whose own
|
|
@@ -614,6 +628,22 @@ export function installAlwaysOnPlist(opts: InstallAlwaysOnPlistOptions): string
|
|
|
614
628
|
`or other manager owns it; rename the peer to avoid the com.iapeer.<personality> collision`,
|
|
615
629
|
)
|
|
616
630
|
}
|
|
631
|
+
// MULTI-INFRA COLLISION GUARD (fail-loud, never a silent clobber): the plist scheme
|
|
632
|
+
// is ONE per personality, so installing a SECOND infra runtime for the same
|
|
633
|
+
// personality would OVERWRITE the first channel's plist (e.g. web over arthur's
|
|
634
|
+
// telegram) and kill that channel on the next bootout/reboot. Refuse loudly until
|
|
635
|
+
// per-runtime plists (multi-infra) land. A deliberate runtime SWITCH is an operator
|
|
636
|
+
// act: `iapeer stop <p>` + delete the plist, then re-provision.
|
|
637
|
+
const bakedRuntime = existsSync(path) ? readPlistEnvVar(path, 'PEER_RUNTIME') : undefined
|
|
638
|
+
if (bakedRuntime && bakedRuntime !== opts.runtime) {
|
|
639
|
+
throw new IapError(
|
|
640
|
+
`refusing to overwrite launchd plist ${path}: it already holds the always-on ` +
|
|
641
|
+
`"${bakedRuntime}" session for "${opts.personality}" (the plist scheme is one per ` +
|
|
642
|
+
`personality) — installing "${opts.runtime}" here would silently kill that channel. ` +
|
|
643
|
+
`Multi-infra per personality needs per-runtime plists (not yet supported); for a ` +
|
|
644
|
+
`deliberate runtime switch: iapeer stop ${opts.personality}, delete the plist, re-provision.`,
|
|
645
|
+
)
|
|
646
|
+
}
|
|
617
647
|
// GLOBAL infra logs (Фаза §8): ~/.iapeer/logs/<personality>/, NOT per-peer
|
|
618
648
|
// <cwd>/.iapeer/logs/ — host-service logs live in the global log area.
|
|
619
649
|
const logDir = peerLogsDir(opts.personality, { env })
|
package/src/launch/launchdRun.ts
CHANGED
|
@@ -97,6 +97,7 @@ export async function runAlwaysOn(personality: string, runtime: string, cwd: str
|
|
|
97
97
|
telegramBin: env[INFRA_RUNTIME_BIN_ENV.telegram],
|
|
98
98
|
notifierBin: env[INFRA_RUNTIME_BIN_ENV.notifier],
|
|
99
99
|
voicetalkBin: env[INFRA_RUNTIME_BIN_ENV.voicetalk],
|
|
100
|
+
webBin: env[INFRA_RUNTIME_BIN_ENV.web],
|
|
100
101
|
sockDir,
|
|
101
102
|
bootDeadlineSecs: 30,
|
|
102
103
|
readyGateSecs: 30,
|
package/src/launch/types.ts
CHANGED
|
@@ -141,6 +141,8 @@ export interface LaunchAdapterConfig {
|
|
|
141
141
|
notifierBin?: string
|
|
142
142
|
/** voicetalk-runtime launch binary (router runtime, infra/always-on — the owner's voice channel). */
|
|
143
143
|
voicetalkBin?: string
|
|
144
|
+
/** web-runtime launch binary (router runtime, infra/always-on — the owner's browser fleet-console). */
|
|
145
|
+
webBin?: string
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
export interface LaunchSpec {
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -1320,10 +1320,17 @@ interface SuperviseCtx {
|
|
|
1320
1320
|
*/
|
|
1321
1321
|
function superviseOnePeer(s: SessionState, ctx: SuperviseCtx): SuperviseOutcome {
|
|
1322
1322
|
const { cfg, env, nowMs, lastTurnMt, childActivityMt, isAlive, kill, verbose, trace } = ctx
|
|
1323
|
-
// H4 — FIRST, before any reap. A launchd-managed peer is read-only
|
|
1323
|
+
// H4 — FIRST, before any reap. A launchd-managed peer is read-only: never wake,
|
|
1324
|
+
// never kill, never sweep its PROCESS. Its stale `.session` however is the
|
|
1325
|
+
// DAEMON'S own supervisory record (written back when the peer was daemon-owned),
|
|
1326
|
+
// not the peer's — a peer that moved under launchd would otherwise re-enter this
|
|
1327
|
+
// skip on every tick forever (follow-up 09.06: latent garbage, not a delivery
|
|
1328
|
+
// bug). Drop the record once; if the plist is ever removed again, the next wake
|
|
1329
|
+
// recreates it.
|
|
1324
1330
|
if (isLaunchdManaged(s.personality, env)) {
|
|
1325
|
-
|
|
1326
|
-
|
|
1331
|
+
removeSessionState(cfg, s.identity)
|
|
1332
|
+
if (verbose) trace({ identity: s.identity, action: 'skipped-launchd', outcome: 'read-only-h4-stale-state-dropped' })
|
|
1333
|
+
return { identity: s.identity, action: 'skipped-launchd', reason: 'launchd-managed; stale daemon .session dropped' }
|
|
1327
1334
|
}
|
|
1328
1335
|
const sock = buildSocketPath(s.runtime, s.personality, cfg.sockDir)
|
|
1329
1336
|
if (!isAlive(sock, s.identity)) {
|
package/src/runtime/deploy.ts
CHANGED
|
@@ -33,6 +33,7 @@ export const RUNTIME_PACKAGES: Readonly<Record<string, string>> = {
|
|
|
33
33
|
telegram: '@agfpd/telegram-runtime',
|
|
34
34
|
notifier: '@agfpd/notifier-runtime',
|
|
35
35
|
voicetalk: '@agfpd/voicetalk-runtime',
|
|
36
|
+
web: '@agfpd/web-runtime',
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/** Resolve the npm package for a runtime: explicit override wins, else the built-in
|
package/src/transport/index.ts
CHANGED
|
@@ -308,11 +308,13 @@ async function deliverViaHost(
|
|
|
308
308
|
// codex took the socket-ack short-circuit above). A bare transcript/pane-log mtime BUMP is NOT proof
|
|
309
309
|
// the session took THIS message: a receiver in an active turn advances both mtimes with its OWN
|
|
310
310
|
// rendering even when our paste was swallowed at the turn boundary (incident 2026-06-23: ok=true,
|
|
311
|
-
// message gone). The ONLY proof is a NEW transcript record CARRYING
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
311
|
+
// message gone). The ONLY proof is a NEW transcript record CARRYING the DELIVERED payload (for an
|
|
312
|
+
// agent target that is the compact `<iap from=…>` presentation — deliverWarm rendered it before this
|
|
313
|
+
// point, so the needle matches what was actually pasted) — for claude, a queue-operation `content`
|
|
314
|
+
// (busy → enqueued) or the user-turn message (idle), written sub-second so a short grace covers it.
|
|
315
|
+
// The receiver's own assistant/tool turn never reproduces the full delivered envelope verbatim, so it
|
|
316
|
+
// cannot forge a confirm. No such record within the grace → false-FAIL (the sender retries) — NOT a
|
|
317
|
+
// false-OK (silent loss the contract forbids).
|
|
316
318
|
const confirm = seam.confirmLanded ?? ((payload: string) => transcriptCarriesEnvelope(baseline, payload, { env }))
|
|
317
319
|
const sleep = seam.sleep ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)))
|
|
318
320
|
const graceMs = confirmGraceMs()
|