-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add routed ping and structured targeting #399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,12 @@ export interface InboxMessage { | |
| id: string; | ||
| ts_ms: number; | ||
| from: string; | ||
| /** Authoritative agent id to use for replies. Never infer this from pane focus. */ | ||
| reply_to: string; | ||
| /** Optional observed surface ref. Stale-able hint only; never a routing address. */ | ||
| via?: string; | ||
| /** Observation timestamp paired with via. */ | ||
| observed_at?: string; | ||
|
Comment on lines
+42
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find all readers of InboxMessage.reply_to and check for unguarded access.
set -euo pipefail
rg -n --type=ts -C4 '\breply_to\b' src/ | sed -n '1,200p'
# Locate readInbox/replay helpers to confirm whether any validation/backfill exists.
ast-grep run --pattern 'export function $NAME($$$): InboxMessage[] { $$$ }' --lang typescript src/inbox.tsRepository: EtanHey/cmuxlayer Length of output: 5902 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- inbox.ts readers and deserialization ---'
sed -n '1,125p' src/inbox.ts
sed -n '300,390p' src/inbox.ts
sed -n '450,515p' src/inbox.ts
printf '%s\n' '--- all reply_to references ---'
rg -n --hidden -g '!node_modules' -g '!dist' -g '!build' '\breply_to\b' .
printf '%s\n' '--- InboxMessage type and readInbox consumers ---'
rg -n --hidden -g '!node_modules' -g '!dist' -g '!build' 'InboxMessage|readInbox\(|replayUndelivered\(|pendingDispatches\(' src test tests 2>/dev/null || trueRepository: EtanHey/cmuxlayer Length of output: 16608 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- exact JSONL parser and public readers ---'
nl -ba src/inbox.ts | sed -n '115,175p'
nl -ba src/inbox.ts | sed -n '375,440p'
printf '%s\n' '--- source imports and property reads ---'
rg -n --hidden -g '!node_modules' -g '!dist' -g '!build' \
'from ["'\'']\./inbox|from ["'\''][^"'\'']*inbox|\.reply_to|reply_to\s*:' src
printf '%s\n' '--- legacy-shaped inbox fixtures or writes ---'
rg -n --hidden -g '!node_modules' -g '!dist' -g '!build' \
'inbox\.jsonl|JSON\.stringify\([^)]*from|from:.*task:|task:.*from:' tests src docs README.md 2>/dev/null || trueRepository: EtanHey/cmuxlayer Length of output: 241 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- exact JSONL parser and public readers ---'
cat -n src/inbox.ts | sed -n '115,175p'
cat -n src/inbox.ts | sed -n '375,440p'
printf '%s\n' '--- source imports and property reads ---'
rg -n --hidden -g '!node_modules' -g '!dist' -g '!build' \
'from ["'\'']\./inbox|from ["'\''][^"'\'']*inbox|\.reply_to|reply_to[[:space:]]*:' src
printf '%s\n' '--- legacy-shaped inbox fixtures or writes ---'
rg -n --hidden -g '!node_modules' -g '!dist' -g '!build' \
'inbox\.jsonl|JSON\.stringify\([^)]*from|from:.*task:|task:.*from:' tests src docs README.md 2>/dev/null || trueRepository: EtanHey/cmuxlayer Length of output: 9750 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- JSONL parser implementation ---'
rg -n -A24 -B8 'function readJsonl|const readJsonl' src/inbox.ts
printf '%s\n' '--- history of reply_to in inbox.ts ---'
git log --oneline -S'reply_to' -- src/inbox.ts | sed -n '1,10p'Repository: EtanHey/cmuxlayer Length of output: 1265 Normalize legacy inbox records before returning them
🤖 Prompt for AI Agents |
||
| /** Recipient agent id (own-tag) or "orc". Each agent monitors only its own inbox. */ | ||
| to: string; | ||
| tag: string; | ||
|
|
@@ -90,6 +96,12 @@ export type InboxMonitorState = "never-armed" | "alive" | "stale"; | |
|
|
||
| export interface DispatchInput { | ||
| from: string; | ||
| /** Resolved sender agent id. Defaults to from for non-engine/internal callers. */ | ||
| reply_to?: string; | ||
| /** Optional sender surface ref hint. Routing must continue to use reply_to. */ | ||
| via?: string; | ||
| /** Optional ISO timestamp for the via observation. */ | ||
| observed_at?: string; | ||
| to?: string; | ||
| tag?: string; | ||
| task: string; | ||
|
|
@@ -325,6 +337,13 @@ export function dispatch( | |
| id: input.id ?? genId(ts), | ||
| ts_ms: ts, | ||
| from: input.from, | ||
| reply_to: input.reply_to ?? input.from, | ||
| ...(input.via | ||
| ? { | ||
| via: input.via, | ||
| observed_at: input.observed_at ?? new Date(ts).toISOString(), | ||
| } | ||
| : {}), | ||
|
Comment on lines
+341
to
+346
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High When ...(input.via
? {
via: input.via,
- observed_at: input.observed_at ?? new Date(ts).toISOString(),
+ observed_at:
+ input.observed_at ??
+ (Number.isFinite(ts) &&
+ ts >= -8_640_000_000_000_000 &&
+ ts <= 8_640_000_000_000_000
+ ? new Date(ts).toISOString()
+ : undefined),
}
: {}),🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||
| to: input.to ?? agentId, | ||
| tag: input.tag ?? "dispatch", | ||
| task: input.task, | ||
|
|
@@ -334,6 +353,16 @@ export function dispatch( | |
| return msg; | ||
| } | ||
|
|
||
| /** One connector-authored composer shape for all inbox wakes. */ | ||
| export function formatInboxPing(message: InboxMessage, path: string): string { | ||
| const replyTo = message.reply_to || message.from; | ||
| const viaHint = | ||
| message.via && message.observed_at | ||
| ? ` via:${message.via} observed_at:${message.observed_at}` | ||
| : ""; | ||
| return `[inbox] ${message.id} — reply_to: ${replyTo}${viaHint} — read ${path}`; | ||
| } | ||
|
|
||
| export function dispatchOnce( | ||
| agentId: string, | ||
| input: DispatchInput & { id: string }, | ||
|
|
||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ import { tmpdir } from "node:os"; | |
| import { createServer } from "../src/server.js"; | ||
| import { | ||
| agentDir, | ||
| inboxPath, | ||
| writeHeartbeat, | ||
| readInbox, | ||
| } from "../src/inbox.js"; | ||
|
|
@@ -287,8 +288,10 @@ describe("dispatch_to_agent nudge (state-independent inbox wake)", () => { | |
| const nudgeCall = after.at(-1)!; | ||
| const nudgeText = String(nudgeCall.at(-1) ?? ""); | ||
| expect(nudgeCall.join(" ")).toContain("surface:new"); | ||
| expect(nudgeCall.join(" ")).toContain("inbox"); | ||
| expect(nudgeText).not.toMatch(/[\n\r]/); | ||
| const message = readInbox(agentId, { baseDir: inboxDir }).at(-1)!; | ||
| expect(nudgeText).toBe( | ||
| `[inbox] ${message.id} — reply_to: ${message.reply_to} — read ${inboxPath(agentId, { baseDir: inboxDir })}`, | ||
| ); | ||
| // And the message itself is durably in the inbox file. | ||
| expect( | ||
| readInbox(agentId, { baseDir: inboxDir }).map((m) => m.task), | ||
|
|
@@ -351,6 +354,120 @@ describe("dispatch_to_agent nudge (state-independent inbox wake)", () => { | |
| expect(sendCalls(exec).length).toBe(before); | ||
| }); | ||
|
|
||
| it("wakes an idle live agent exactly once on enqueue even when its monitor is fresh", async () => { | ||
| const agentId = await spawnTestAgent(server); | ||
| const engine = server._registeredTools["interact"]._engine; | ||
| const idle = engine.stateMgr.updateRecord(agentId, { state: "idle" }); | ||
| engine.getRegistry().set(agentId, idle); | ||
| writeHeartbeat(agentId, { baseDir: inboxDir }); | ||
|
|
||
| const before = sendCalls(exec).length; | ||
| const result = await server._registeredTools["dispatch_to_agent"].handler( | ||
| { | ||
| agent_id: agentId, | ||
| task: "GO", | ||
| from: "orc", | ||
| tag: "dispatch", | ||
| persist: false, | ||
| nudge: "auto", | ||
| }, | ||
| {} as any, | ||
| ); | ||
| const parsed = | ||
| result.structuredContent ?? JSON.parse(result.content[0].text); | ||
| const message = readInbox(agentId, { baseDir: inboxDir }).at(-1)!; | ||
| const after = sendCalls(exec); | ||
|
|
||
| expect(parsed.ok).toBe(true); | ||
| expect(parsed.monitor_state).toBe("alive"); | ||
| expect(parsed.nudge).toMatchObject({ attempted: true, sent: true }); | ||
| expect(after).toHaveLength(before + 1); | ||
| expect(String(after.at(-1)?.at(-1) ?? "")).toBe( | ||
| `[inbox] ${message.id} — reply_to: ${message.reply_to} — read ${inboxPath(agentId, { baseDir: inboxDir })}`, | ||
| ); | ||
| }); | ||
|
|
||
| it("puts the resolved caller agent id in the envelope and ping reply address", async () => { | ||
| const agentId = await spawnTestAgent(server); | ||
| const engine = server._registeredTools["interact"]._engine; | ||
| const target = engine.getRegistry().get(agentId)!; | ||
| const idle = engine.stateMgr.updateRecord(agentId, { state: "idle" }); | ||
| engine.getRegistry().set(agentId, idle); | ||
| const caller = { | ||
| ...target, | ||
| agent_id: "golems-caller", | ||
| surface_id: "surface:golems", | ||
| surface_uuid: "22222222-2222-4222-8222-222222222222", | ||
| state: "ready", | ||
| }; | ||
| engine.stateMgr.writeState(caller); | ||
| engine.getRegistry().set(caller.agent_id, caller); | ||
| writeHeartbeat(agentId, { baseDir: inboxDir }); | ||
|
|
||
| const before = sendCalls(exec).length; | ||
| const result = await runWithCallerContext( | ||
| { surfaceId: caller.surface_uuid }, | ||
| () => | ||
| server._registeredTools["dispatch_to_agent"].handler( | ||
| { | ||
| agent_id: agentId, | ||
| task: "Reply to the sender, not your own pane", | ||
| from: "ambiguous-human-label", | ||
| nudge: "auto", | ||
| }, | ||
| {} as any, | ||
| ), | ||
| ); | ||
| const parsed = | ||
| result.structuredContent ?? JSON.parse(result.content[0].text); | ||
| const message = readInbox(agentId, { baseDir: inboxDir }).at(-1)!; | ||
| const after = sendCalls(exec); | ||
|
|
||
| expect(parsed.ok).toBe(true); | ||
| expect(message).toMatchObject({ | ||
| from: "ambiguous-human-label", | ||
| reply_to: caller.agent_id, | ||
| via: caller.surface_id, | ||
| observed_at: expect.any(String), | ||
| }); | ||
| expect(after).toHaveLength(before + 1); | ||
| expect(String(after.at(-1)?.at(-1) ?? "")).toBe( | ||
| `[inbox] ${message.id} — reply_to: ${caller.agent_id} via:${caller.surface_id} observed_at:${message.observed_at} — read ${inboxPath(agentId, { baseDir: inboxDir })}`, | ||
| ); | ||
| expect(String(after.at(-1)?.at(-1) ?? "")).not.toContain("workspace:"); | ||
| }); | ||
|
Comment on lines
+390
to
+438
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Add coverage for the unresolvable caller surface and for the ref-based fallback. This test exercises only the UUID branch of
Add one case for each so a later refactor cannot silently drop the guard or the fallback. 🧪 Sketch of the missing casesit("refuses dispatch when the caller surface resolves to no live agent", async () => {
const agentId = await spawnTestAgent(server);
const before = readInbox(agentId, { baseDir: inboxDir }).length;
const result = await runWithCallerContext(
{ surfaceId: "99999999-9999-4999-8999-999999999999" },
() =>
server._registeredTools["dispatch_to_agent"].handler(
{ agent_id: agentId, task: "GO", from: "orc", nudge: "never" },
{} as any,
),
);
const parsed =
result.structuredContent ?? JSON.parse(result.content[0].text);
expect(parsed.ok).toBe(false);
expect(String(parsed.error)).toContain("could not resolve caller surface");
// The durable append must not have happened.
expect(readInbox(agentId, { baseDir: inboxDir })).toHaveLength(before);
});🤖 Prompt for AI Agents |
||
|
|
||
| it("durably appends with the supplied sender id when caller surface is unresolved", async () => { | ||
| const agentId = await spawnTestAgent(server); | ||
|
|
||
| const result = await runWithCallerContext( | ||
| { surfaceId: "surface:missing-caller" }, | ||
| () => | ||
| server._registeredTools["dispatch_to_agent"].handler( | ||
| { | ||
| agent_id: agentId, | ||
| task: "Recovery message must survive stale caller state", | ||
| from: "cmuxlayerClaude-recovery", | ||
| nudge: "never", | ||
| }, | ||
| {} as any, | ||
| ), | ||
| ); | ||
| const parsed = | ||
| result.structuredContent ?? JSON.parse(result.content[0].text); | ||
| const message = readInbox(agentId, { baseDir: inboxDir }).at(-1)!; | ||
|
|
||
| expect(parsed.ok).toBe(false); | ||
| expect(parsed.durable).toBe(true); | ||
| expect(parsed.error_code).toBe("inbox_monitor_never_armed"); | ||
| expect(message).toMatchObject({ | ||
| from: "cmuxlayerClaude-recovery", | ||
| reply_to: "cmuxlayerClaude-recovery", | ||
| task: "Recovery message must survive stale caller state", | ||
| }); | ||
| expect(message).not.toHaveProperty("via"); | ||
| }); | ||
|
|
||
| it("republishes a Claude idle-to-working transition without shrinking any lane", async () => { | ||
| await server.close(); | ||
| const idleScreen = [ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -154,6 +154,7 @@ describe("inbox write-channel", () => { | |
| it("dispatch appends a message with defaults (to=agent, tag=dispatch) and id", () => { | ||
| const m = dispatch("a1", { from: "orc", task: "do X" }, opts); | ||
| expect(m.to).toBe("a1"); | ||
| expect(m.reply_to).toBe("orc"); | ||
| expect(m.tag).toBe("dispatch"); | ||
| expect(m.id).toBeTruthy(); | ||
| expect(m.ts_ms).toBe(1_000_000); | ||
|
|
@@ -162,6 +163,28 @@ describe("inbox write-channel", () => { | |
| expect(all[0].task).toBe("do X"); | ||
| }); | ||
|
|
||
| it("stores an optional stale-able surface hint only beside the durable reply id", () => { | ||
| const m = dispatch( | ||
| "coach", | ||
| { | ||
| from: "golems", | ||
| reply_to: "golems-agent-id", | ||
| via: "surface:golems", | ||
| observed_at: "2026-08-12T18:00:00.000Z", | ||
| task: "reply through the registry", | ||
| }, | ||
| opts, | ||
| ); | ||
|
|
||
| expect(m).toMatchObject({ | ||
| reply_to: "golems-agent-id", | ||
| via: "surface:golems", | ||
| observed_at: "2026-08-12T18:00:00.000Z", | ||
| }); | ||
| expect(m).not.toHaveProperty("tab"); | ||
| expect(m).not.toHaveProperty("tab_name"); | ||
| }); | ||
|
Comment on lines
+166
to
+186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Cover the two conditional branches of the This test supplies
The second outcome is silent. A caller that passes only 🧪 Proposed additional assertionsit("generates observed_at when via is supplied without it", () => {
const m = dispatch(
"coach",
{ from: "golems", via: "surface:golems", task: "t" },
opts,
);
expect(m.via).toBe("surface:golems");
expect(m.observed_at).toBe(new Date(m.ts_ms).toISOString());
});
it("drops observed_at when via is absent", () => {
const m = dispatch(
"coach",
{ from: "golems", observed_at: "2026-08-12T18:00:00.000Z", task: "t" },
opts,
);
expect(m).not.toHaveProperty("via");
expect(m).not.toHaveProperty("observed_at");
});🤖 Prompt for AI Agents |
||
|
|
||
| it("dispatchOnce keeps one durable message for a stable recovery id", () => { | ||
| const first = dispatchOnce( | ||
| "a1-once", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Existing unacknowledged
inbox.jsonlentries written before this commit have noreply_to, butreadInbox()only casts parsed JSON to the newly required interface. After an upgrade or resume,replayUndelivered()therefore returns messages whose advertised authoritative reply address is actuallyundefined; the fallback informatInboxPing()does not repair the durable envelope consumed by the agent. Normalize old records while reading, at least by deriving the compatibility value fromfrom.AGENTS.md reference: AGENTS.md:L27-L34
Useful? React with 👍 / 👎.