@agfpd/iapeer 0.2.75 → 0.2.76
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/daemon/daemon.test.ts +10 -3
- package/src/daemon/index.ts +16 -25
- package/src/launch/adapters/claude.ts +0 -20
- package/src/launch/adapters/codex.ts +0 -29
- package/src/launch/adapters/notifier.ts +0 -10
- package/src/launch/adapters/telegram.ts +0 -14
- package/src/launch/bootdeliver.test.ts +0 -2
- package/src/launch/launch.test.ts +0 -9
- package/src/launch/types.ts +0 -6
- package/src/lifecycle/index.ts +16 -5
- package/src/lifecycle/lifecycle.test.ts +9 -0
- package/src/supervisor/protocol.ts +1 -1
- package/src/transport/index.ts +1 -8
package/package.json
CHANGED
|
@@ -73,12 +73,19 @@ describe('caller identity from header', () => {
|
|
|
73
73
|
})
|
|
74
74
|
|
|
75
75
|
describe('listTools', () => {
|
|
76
|
-
test('serves ONLY send_to_peer (list_online_peers deprecated) with alwaysLoad meta and roster', () => {
|
|
77
|
-
const tools = listTools(
|
|
76
|
+
test('serves ONLY send_to_peer (list_online_peers deprecated) with alwaysLoad meta and NO embedded roster', () => {
|
|
77
|
+
const tools = listTools() as any[]
|
|
78
78
|
expect(tools.map(t => t.name).sort()).toEqual(['send_to_peer'])
|
|
79
79
|
expect(tools.every(t => t._meta['anthropic/alwaysLoad'] === true)).toBe(true)
|
|
80
80
|
const sendTool = tools.find(t => t.name === 'send_to_peer')
|
|
81
|
-
|
|
81
|
+
// Regression guard: the peer roster lives ONLY in the system prompt
|
|
82
|
+
// (composeSystemPrompt → "## Known peers"), NEVER in the tool description —
|
|
83
|
+
// embedding it here duplicated the list into every agent's context per turn.
|
|
84
|
+
expect(sendTool.description).not.toMatch(/Known peers/)
|
|
85
|
+
expect(sendTool.description).not.toContain('arthur')
|
|
86
|
+
expect(sendTool.description).not.toMatch(/Runtimes:/)
|
|
87
|
+
// Purpose + parameter semantics stay.
|
|
88
|
+
expect(sendTool.description).toContain('Send a message to a known iapeer peer')
|
|
82
89
|
expect(sendTool.description).toContain('wakes it and delivers there')
|
|
83
90
|
expect(sendTool.description).toContain('parallel to the default runtime')
|
|
84
91
|
expect(sendTool.inputSchema.properties.runtime.description).toContain('wakes it warm-on-demand')
|
package/src/daemon/index.ts
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
import { parseSessionName } from '../core/socket.ts'
|
|
42
42
|
import { IapError } from '../core/errors.ts'
|
|
43
43
|
import { pluginStateDir, writeFileAtomic, type StorageOptions } from '../storage/index.ts'
|
|
44
|
-
import {
|
|
44
|
+
import { readPeersIndex, type PeersIndex } from '../registry/index.ts'
|
|
45
45
|
import { resolveCallerIdentity, type CallerIdentity, type ResolvedCaller } from '../identity/index.ts'
|
|
46
46
|
import { routeSend, type ComposerQueueRouteDeps, type EphemeralRouteDeps, type SendToPeerInput, type WakeFn } from '../transport/index.ts'
|
|
47
47
|
import { appendDeliveryEvent } from './deliverylog.ts'
|
|
@@ -76,31 +76,22 @@ export function resolveCallerFromHeader(value: string | undefined, index: PeersI
|
|
|
76
76
|
// Tool definitions (ported verbatim from IAP server.ts)
|
|
77
77
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// The roster of known peers is DELIBERATELY NOT in this description. It is rendered
|
|
80
|
+
// ONCE, in the peer's system prompt (composeSystemPrompt → Layer 3 "## Known peers").
|
|
81
|
+
// Embedding it here ALSO duplicated the full list into every agent's context on every
|
|
82
|
+
// turn — a live consumer (boris) confirmed seeing the identical roster twice. The
|
|
83
|
+
// description stays light: purpose + parameter semantics only; the model gets the
|
|
84
|
+
// roster from its system prompt. Likewise the MCP server-level `instructions` field is
|
|
85
|
+
// left unset (createMcpServer) — it would re-inject doctrine into the client system
|
|
86
|
+
// prompt, re-creating the very duplication this removed. Static (no index needed).
|
|
87
|
+
export function buildSendDescription(): string {
|
|
88
|
+
return [
|
|
81
89
|
'Send a message to a known iapeer peer through Inter-Agent-Protocol.',
|
|
82
90
|
'Peers can be agents, humans, or services. Runtime is the delivery surface, not the peer type.',
|
|
83
91
|
'Current delivery supports any runtime endpoint that follows the IAP tmux socket convention. Claude/Codex are built-in local runtimes; external runtimes such as telegram can be exposed by runtime-router packages.',
|
|
84
92
|
'',
|
|
85
|
-
|
|
86
|
-
]
|
|
87
|
-
if (index.peers.length === 0) {
|
|
88
|
-
lines.push('- none yet')
|
|
89
|
-
} else {
|
|
90
|
-
for (const peer of index.peers) {
|
|
91
|
-
const p = publicPeerSummary(peer)
|
|
92
|
-
const desc = p.description ? ` — ${p.description}` : ''
|
|
93
|
-
lines.push(`- ${p.personality}${desc}`)
|
|
94
|
-
lines.push(` Runtime: ${p.runtime}`)
|
|
95
|
-
lines.push(` Runtimes: ${p.runtimes.join(', ')}`)
|
|
96
|
-
lines.push(` Intelligence: ${p.intelligence}`)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
lines.push(
|
|
100
|
-
'',
|
|
101
|
-
"Use `personality`, not a runtime-prefixed identity. Set `runtime` to target a specific declared runtime for this send. If that runtime is warm-asleep, the daemon wakes it and delivers there; use this to intentionally bring up a second session of the same peer in parallel to the default runtime (for example codex alongside claude). Descriptions identify the peer's role; runtimes identify reachable delivery endpoints; intelligence identifies the nature of the peer (artificial / natural / absent). Inbound peer messages arrive as <iap from-personality=\"...\" from-runtime=\"...\" from-intelligence=\"...\"> blocks. The topic attribute is optional.",
|
|
102
|
-
)
|
|
103
|
-
return lines.join('\n')
|
|
93
|
+
"Use `personality`, not a runtime-prefixed identity. Set `runtime` to target a specific declared runtime for this send. If that runtime is warm-asleep, the daemon wakes it and delivers there; use this to intentionally bring up a second session of the same peer in parallel to the default runtime (for example codex alongside claude). Inbound peer messages arrive as <iap from-personality=\"...\" from-runtime=\"...\" from-intelligence=\"...\"> blocks. The topic attribute is optional.",
|
|
94
|
+
].join('\n')
|
|
104
95
|
}
|
|
105
96
|
|
|
106
97
|
// The daemon serves the AGENT exactly ONE MCP tool: send_to_peer. list_online_peers
|
|
@@ -110,11 +101,11 @@ export function buildSendDescription(index: PeersIndex): string {
|
|
|
110
101
|
// tool-set is kept minimal. The liveness scan itself (transport.listOnlinePeers) stays
|
|
111
102
|
// — the future `iapeer list` verb and the multi-runtime delivery resolver use it — it
|
|
112
103
|
// is simply no longer exposed to the agent as a tool.
|
|
113
|
-
export function listTools(
|
|
104
|
+
export function listTools(): unknown[] {
|
|
114
105
|
return [
|
|
115
106
|
{
|
|
116
107
|
name: 'send_to_peer',
|
|
117
|
-
description: buildSendDescription(
|
|
108
|
+
description: buildSendDescription(),
|
|
118
109
|
inputSchema: {
|
|
119
110
|
type: 'object',
|
|
120
111
|
properties: {
|
|
@@ -270,7 +261,7 @@ function headerFromRequestInfo(extra: { requestInfo?: { headers?: Record<string,
|
|
|
270
261
|
export function createMcpServer(deps: CallToolDeps = {}): Server {
|
|
271
262
|
const server = new Server(SERVER_INFO, { capabilities: { tools: {} } })
|
|
272
263
|
|
|
273
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: listTools(
|
|
264
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: listTools() }))
|
|
274
265
|
|
|
275
266
|
server.setRequestHandler(CallToolRequestSchema, async (req, extra): Promise<CallToolResult> => {
|
|
276
267
|
// Per-request identity: read the caller from THIS request's HTTP header,
|
|
@@ -223,26 +223,6 @@ export const claudeAdapter: RuntimeAdapter = {
|
|
|
223
223
|
return newest > 0 ? newest : null
|
|
224
224
|
},
|
|
225
225
|
|
|
226
|
-
/**
|
|
227
|
-
* A claude tool-permission/approval modal is open iff the pane shows
|
|
228
|
-
* 'Do you want to proceed?' (Spawned-Peer dialogs.ts:60-61,
|
|
229
|
-
* approve-watch.sh:78). Defensive only: a --dangerously-skip-permissions peer
|
|
230
|
-
* does not normally reach it, but a stray approval (e.g. a connector) must not
|
|
231
|
-
* stall a headless session.
|
|
232
|
-
*/
|
|
233
|
-
permissionDialogActive(pane: string): boolean {
|
|
234
|
-
return pane.includes('Do you want to proceed?')
|
|
235
|
-
},
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Affirm a claude approval with ['Enter']: option 1 ('Yes') is the
|
|
239
|
-
* default-highlighted choice, so a bare Enter accepts (approve-watch.sh:22-25,
|
|
240
|
-
* Spawned-Peer watcher.ts:298).
|
|
241
|
-
*/
|
|
242
|
-
permissionDialogKeys(): string[] {
|
|
243
|
-
return ['Enter']
|
|
244
|
-
},
|
|
245
|
-
|
|
246
226
|
/**
|
|
247
227
|
* Resume preflight (fail-loud — never a silent fresh fallback): resolve the
|
|
248
228
|
* newest transcript uuid for cwd via the claude-slug dir scan (lifecycle.
|
|
@@ -260,35 +260,6 @@ export const codexAdapter: RuntimeAdapter = {
|
|
|
260
260
|
return newestCodexSessionMtime(cwd)
|
|
261
261
|
},
|
|
262
262
|
|
|
263
|
-
/**
|
|
264
|
-
* A codex approval modal is open iff the pane carries the 'enter to submit'
|
|
265
|
-
* footer AND an Allow-to-run title — the MCP tool-approval modal ("Allow the
|
|
266
|
-
* <server> MCP server to run tool …?") or the generic command-exec approval
|
|
267
|
-
* (dialogs.ts codexApprovalActive, 39-45). The footer is the discriminator
|
|
268
|
-
* against boot-phase startup screens (those say 'Press enter to continue'), so
|
|
269
|
-
* a dir-trust/update screen never matches here. Required even under YOLO:
|
|
270
|
-
* connector / consequential MCP-tool approvals are NOT suppressed by
|
|
271
|
-
* --dangerously-bypass-approvals-and-sandbox and would block a headless peer
|
|
272
|
-
* forever (codex-start.sh:229-237).
|
|
273
|
-
*/
|
|
274
|
-
permissionDialogActive(pane: string): boolean {
|
|
275
|
-
if (!pane.includes('enter to submit')) return false
|
|
276
|
-
if (/Allow the .+ MCP server to run tool/.test(pane)) return true
|
|
277
|
-
return /\bAllow\b[\s\S]*\bto run\b/.test(pane)
|
|
278
|
-
},
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Affirm a codex approval with ['Down','Enter'] (watcher.answerPermissionDialog
|
|
282
|
-
* codex branch, 302-306): the modal default-highlights option 1 ("Allow",
|
|
283
|
-
* one-shot); one Down selects option 2 ("Allow for this session") so later tool
|
|
284
|
-
* calls in this same headless session don't re-prompt, then Enter submits. Even
|
|
285
|
-
* partial key delivery is safe — Down-then-nothing leaves option 1 (still an
|
|
286
|
-
* affirmative), and the only destructive option (Cancel) is three Downs away.
|
|
287
|
-
*/
|
|
288
|
-
permissionDialogKeys(): string[] {
|
|
289
|
-
return ['Down', 'Enter']
|
|
290
|
-
},
|
|
291
|
-
|
|
292
263
|
/**
|
|
293
264
|
* Resume preflight (fail-loud — never a silent fresh fallback). Codex resumes
|
|
294
265
|
* via 'resume --last' with NO ref: it matches the newest session for cwd
|
|
@@ -68,16 +68,6 @@ export const notifierAdapter: RuntimeAdapter = {
|
|
|
68
68
|
return null
|
|
69
69
|
},
|
|
70
70
|
|
|
71
|
-
/** No permission/approval modal: a router is not an LLM driving tools. */
|
|
72
|
-
permissionDialogActive(_pane: string): boolean {
|
|
73
|
-
return false
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
/** No permission modal to affirm — empty send-keys args. */
|
|
77
|
-
permissionDialogKeys(): string[] {
|
|
78
|
-
return []
|
|
79
|
-
},
|
|
80
|
-
|
|
81
71
|
/** Nothing to resume: a router does not replay a transcript. */
|
|
82
72
|
resolveResume(_cwd: string): { ok: boolean; ref?: string; reason?: string } {
|
|
83
73
|
return { ok: true }
|
|
@@ -98,20 +98,6 @@ export const telegramAdapter: RuntimeAdapter = {
|
|
|
98
98
|
return null
|
|
99
99
|
},
|
|
100
100
|
|
|
101
|
-
/**
|
|
102
|
-
* No permission/approval modal: a router is not an LLM driving tools behind an
|
|
103
|
-
* autopilot, so there is never a dialog to detect (telegram-start.sh:14, "no
|
|
104
|
-
* trust-dialog auto-accept, no permissions banner").
|
|
105
|
-
*/
|
|
106
|
-
permissionDialogActive(_pane: string): boolean {
|
|
107
|
-
return false
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
/** No permission modal to affirm — empty send-keys args (see above). */
|
|
111
|
-
permissionDialogKeys(): string[] {
|
|
112
|
-
return []
|
|
113
|
-
},
|
|
114
|
-
|
|
115
101
|
/**
|
|
116
102
|
* Nothing to resume: a router does not replay a transcript, so resume cannot
|
|
117
103
|
* fail-loud the way claude/codex do — there is no prior session ref to resolve
|
|
@@ -136,7 +136,6 @@ describe('telegramAdapter (router — no TUI surface)', () => {
|
|
|
136
136
|
expect(telegramAdapter.bootDialogKeys('anything')).toBeNull()
|
|
137
137
|
expect(telegramAdapter.isInputReady('anything')).toBe(true)
|
|
138
138
|
expect(telegramAdapter.newestActivityMtime('/w')).toBeNull()
|
|
139
|
-
expect(telegramAdapter.permissionDialogActive('x')).toBe(false)
|
|
140
139
|
})
|
|
141
140
|
})
|
|
142
141
|
|
|
@@ -156,8 +155,6 @@ describe('notifierAdapter (router — infra/always-on)', () => {
|
|
|
156
155
|
expect(notifierAdapter.bootDialogKeys('anything')).toBeNull()
|
|
157
156
|
expect(notifierAdapter.isInputReady('anything')).toBe(true)
|
|
158
157
|
expect(notifierAdapter.newestActivityMtime('/w')).toBeNull()
|
|
159
|
-
expect(notifierAdapter.permissionDialogActive('x')).toBe(false)
|
|
160
|
-
expect(notifierAdapter.permissionDialogKeys()).toEqual([])
|
|
161
158
|
expect(notifierAdapter.resolveResume('/w')).toEqual({ ok: true })
|
|
162
159
|
})
|
|
163
160
|
})
|
|
@@ -249,12 +246,6 @@ describe('claudeAdapter', () => {
|
|
|
249
246
|
"This session is 7h 3m old and 314.2k tokens.\n❯ 1. Resume from summary (recommended)\n 2. Resume full session as-is\n 3. Don't ask me again\nEnter to confirm · Esc to cancel"
|
|
250
247
|
expect(claudeAdapter.bootDialogKeys(live170)).toEqual(['Down'])
|
|
251
248
|
})
|
|
252
|
-
|
|
253
|
-
test('permissionDialog: proceed prompt → active, [Enter]', () => {
|
|
254
|
-
expect(claudeAdapter.permissionDialogActive('Do you want to proceed?')).toBe(true)
|
|
255
|
-
expect(claudeAdapter.permissionDialogActive('nothing')).toBe(false)
|
|
256
|
-
expect(claudeAdapter.permissionDialogKeys()).toEqual(['Enter'])
|
|
257
|
-
})
|
|
258
249
|
})
|
|
259
250
|
|
|
260
251
|
// ─── exit-cause observability: the pane-died hook builder (pure string) ──────
|
package/src/launch/types.ts
CHANGED
|
@@ -265,12 +265,6 @@ export interface RuntimeAdapter {
|
|
|
265
265
|
* claude transcript / codex session jsonl mtime; null for a router (no proxy). */
|
|
266
266
|
newestActivityMtime(cwd: string): number | null
|
|
267
267
|
|
|
268
|
-
/** Is a permission/approval dialog open in the pane (headless autopilot)? */
|
|
269
|
-
permissionDialogActive(pane: string): boolean
|
|
270
|
-
/** tmux send-keys args to affirm a permission dialog (claude ['Enter'];
|
|
271
|
-
* codex ['Down','Enter'] to pick "allow for session"). */
|
|
272
|
-
permissionDialogKeys(): string[]
|
|
273
|
-
|
|
274
268
|
/** Resume preflight — validate a resume request fail-loud (never silent fresh).
|
|
275
269
|
* Returns the resolved ref (claude uuid) or ok:false with a reason. */
|
|
276
270
|
resolveResume(cwd: string): { ok: boolean; ref?: string; reason?: string }
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
} from '../launch/index.ts'
|
|
49
49
|
import { composeSystemPrompt, gatherPromptInput } from '../launch/composeSystemPrompt.ts'
|
|
50
50
|
import { dismissCanary, ensureServerCanary, signalCanaryClean } from '../launch/canary.ts'
|
|
51
|
-
import { hostSessionAlive, killPtyHost } from '../launch/ptyHost.ts'
|
|
51
|
+
import { hostSessionAlive, killPtyHost, ptyHostEnabled } from '../launch/ptyHost.ts'
|
|
52
52
|
import { appendLifecycleEvent, superviseLogVerbose } from './eventlog.ts'
|
|
53
53
|
|
|
54
54
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -668,10 +668,21 @@ function sessionAlive(sock: string, identity: string): boolean {
|
|
|
668
668
|
* - `server-dead` — the server itself is gone: the socket file is missing, or it
|
|
669
669
|
* exists but nothing serves it (stale socket — the SIGKILL/OOM class). pane-died
|
|
670
670
|
* could never fire; the server-death canary (launch/canary.ts) is the recorder
|
|
671
|
-
* for this class (`ev=server-exit` + forensics), this line is the backstop.
|
|
672
|
-
|
|
671
|
+
* for this class (`ev=server-exit` + forensics), this line is the backstop.
|
|
672
|
+
* `hosted=true` (pty-supervisor peer) reframes ONLY the absent-socket reason: a hosted peer
|
|
673
|
+
* has NO tmux socket, so that branch would otherwise misattribute its death to a tmux server
|
|
674
|
+
* that never existed. Its real cause lives in the supervisor exits.log. A socket that EXISTS
|
|
675
|
+
* is a genuine tmux server, so `hosted` never overrides the session-gone / stale-socket arm. */
|
|
676
|
+
export function classifyGoneSession(sock: string, hosted = false): { death: 'server-dead' | 'session-gone'; reason: string } {
|
|
673
677
|
if (!existsSync(sock)) {
|
|
674
|
-
|
|
678
|
+
// No socket file. For a pty-supervisor-hosted peer this is EXPECTED — it never had a tmux
|
|
679
|
+
// socket; its death is the supervisor session ending (cause in the supervisor exits.log
|
|
680
|
+
// death-EVENT), NOT a tmux server vanishing. For a tmux peer it IS a dead tmux server.
|
|
681
|
+
// The check is gated on the absent socket on purpose: a socket that EXISTS (below) means a
|
|
682
|
+
// real tmux server, so `hosted` (pty-enabled-by-default) must not override genuine tmux.
|
|
683
|
+
return hosted
|
|
684
|
+
? { death: 'server-dead', reason: 'pty host session ended (supervisor) — cause in exits.log (death-EVENT)' }
|
|
685
|
+
: { death: 'server-dead', reason: 'tmux server gone (socket file missing)' }
|
|
675
686
|
}
|
|
676
687
|
// Ask the SERVER, not the session: any server-level command answering (exit 0)
|
|
677
688
|
// proves the server is alive and merely lost this session.
|
|
@@ -1172,7 +1183,7 @@ export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): S
|
|
|
1172
1183
|
// distinguishable in lifecycle.log: `session-gone` (pane died, server alive →
|
|
1173
1184
|
// exits.log should have the cause) vs `server-dead` (whole tmux server died →
|
|
1174
1185
|
// exits.log structurally empty; this line is the only durable trace).
|
|
1175
|
-
const gone = classifyGoneSession(sock)
|
|
1186
|
+
const gone = classifyGoneSession(sock, ptyHostEnabled(cfg.logDir, s.identity))
|
|
1176
1187
|
out.push({ identity: s.identity, action: 'reaped-gone', reason: gone.reason })
|
|
1177
1188
|
trace({ identity: s.identity, action: 'reaped-gone', death: gone.death, reason: gone.reason, outcome: 'fresh-next-msg' })
|
|
1178
1189
|
continue
|
|
@@ -738,6 +738,15 @@ describe('classifyGoneSession (death-class tag)', () => {
|
|
|
738
738
|
expect(r.reason).toContain('socket file missing')
|
|
739
739
|
})
|
|
740
740
|
|
|
741
|
+
test('hosted peer → host-framed reason, not tmux', () => {
|
|
742
|
+
// A pty-supervisor-hosted peer never had a tmux socket; the tmux-framed "socket file
|
|
743
|
+
// missing" would misattribute its death. hosted=true short-circuits to the supervisor framing.
|
|
744
|
+
const r = classifyGoneSession(join(tmpdir(), 'iapeer-no-such-sock-ever.sock'), true)
|
|
745
|
+
expect(r.death).toBe('server-dead')
|
|
746
|
+
expect(r.reason).toContain('pty host')
|
|
747
|
+
expect(r.reason).not.toContain('tmux server')
|
|
748
|
+
})
|
|
749
|
+
|
|
741
750
|
test('stale socket (file exists, nothing serves it) → server-dead', () => {
|
|
742
751
|
// The SIGKILL/OOM class: the killed server never unlinks its socket file.
|
|
743
752
|
const dir = mkdtempSync(join(tmpdir(), 'iapeer-stale-sock-'))
|
|
@@ -78,7 +78,7 @@ export function stripQueries(s: string): string {
|
|
|
78
78
|
// "looks right" table).
|
|
79
79
|
//
|
|
80
80
|
// tmux semantics mirrored for the vocabulary our RuntimeAdapters actually emit (bootDialogKeys /
|
|
81
|
-
//
|
|
81
|
+
// executeControl): a leading `-l` switches to LITERAL mode (every following
|
|
82
82
|
// token sent verbatim, no key-name lookup); otherwise each token is a KEY NAME when known, else
|
|
83
83
|
// literal text. Cursor keys depend on DECCKM application-cursor-keys mode — `\x1b[B` (normal) vs
|
|
84
84
|
// `\x1bOB` (application) — which tmux keys off the terminal mode and the caller reads from the
|
package/src/transport/index.ts
CHANGED
|
@@ -262,13 +262,6 @@ export function hasAttachedTmuxClient(target: DeliveryTarget): boolean {
|
|
|
262
262
|
return r.out.split(/\r?\n/).map(line => line.trim()).includes(target.address)
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
export function targetComposerHasHumanInput(target: DeliveryTarget): boolean {
|
|
266
|
-
if (!isSupportedLocalRuntime(target.runtime)) return false
|
|
267
|
-
const cap = tmux(target.socketPath, 'capture-pane', '-e', '-p', '-t', target.address)
|
|
268
|
-
if (!cap.ok) return false
|
|
269
|
-
return composerCaptureHasHumanInput(cap.out, getAdapter(target.runtime).deliveryMarkers)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
265
|
function targetComposerBlocksDelivery(target: DeliveryTarget): boolean {
|
|
273
266
|
if (!isSupportedLocalRuntime(target.runtime)) return false
|
|
274
267
|
const cap = tmux(target.socketPath, 'capture-pane', '-e', '-p', '-t', target.address)
|
|
@@ -1373,7 +1366,7 @@ function telegramSenderGuard(
|
|
|
1373
1366
|
|
|
1374
1367
|
/**
|
|
1375
1368
|
* Route a send from a request-resolved caller to a target peer and deliver it.
|
|
1376
|
-
* hit (live) → build envelope from the CALLER →
|
|
1369
|
+
* hit (live) → build envelope from the CALLER → deliverWarm (host-aware) → {woke:false}
|
|
1377
1370
|
* miss (dead) → if deps.wake: wake the peer (envelope = boot first-message) →
|
|
1378
1371
|
* verify-before-act re-resolve → {woke:true}; else explicit
|
|
1379
1372
|
* "offline" (Ф1 behaviour). H4 (don't wake launchd peers) and the
|