feat(messaging): route messages to live runs via pi steer
Incoming messages (human → agent, agent → agent, channel broadcast) persisted to the inbox and were only projected into the recipient's transcript on the next `prepareRun`. When the recipient was already running, every message waited for the next wake — a Slack-like system operating at heartbeat cadence, not in real time. `RuntimeMessageInjector` closes the gap: it looks up the recipient's active run through `RuntimeBridge.findActiveRunForAgent`, projects the persisted `AgentMessageRecord` via the existing inbox-projection helper, and posts a `steer-run` ParentMessage into the worker. The worker's runner enqueues the projection on the Agent's steering queue; pi drains it at the next turn boundary and emits the usual `message_end` → `persistMessage` sequence, so the session store shadow is preserved without double-writing. `dispatchAgentMessage` gains an optional `injectRuntimeMessage` hook used by every API-side dispatch. Injection succeeds → we mark the inbox row delivered and skip `wakeAgentIfPossible`. Injection fails (no active run) → the legacy inbox + wake path still fires, so sleeping agents continue to receive their messages on the next turn. The injector is owned by the service graph alongside `workerService` and `piRuntimeRef`, which gives it both the worker postMessage channel and the RuntimeBridge active-run map without additional coupling. Internal dispatch call sites (sweepStaleReviews, automation actions) leave the hook unset; those system messages target idle agents by design and do not need the runtime path.
This commit is contained in:
1 parent
795008f56b
commit
5ee3d1394a
7 files changed
+151
-2
No files matched your search
@@ -64,6 +64,14 @@ export interface DispatchAgentMessageDependencies {
|
||||
db: AppDatabase;
|
||||
publishDomainChanged?: () => void;
|
||||
wakeAgentIfPossible?: (agentId: string | null | undefined, companyId: string, trigger: string) => string | null;
|
||||
/**
|
||||
* Attempts to route a persisted message into the recipient's live pi
|
||||
* run via `agent.steer()`. Returns true when delivery happened, false
|
||||
* when the recipient has no active run. When true, dispatch skips
|
||||
* `wakeAgentIfPossible` for that recipient because the message is
|
||||
* already in-flight inside the agent loop.
|
||||
*/
|
||||
injectRuntimeMessage?: (recipientAgentId: string, companyId: string, message: AgentMessageRecord) => boolean;
|
||||
emitEvent?: (event: DesktopEvent) => void;
|
||||
notify?: (options: { title: string; body: string; urgency: "critical" | "informational"; navigation?: { section: SectionId; entityId?: string }; batchKey?: string }) => void;
|
||||
/** Called when a human user sends a message. Resolves thread wait states scoped by waitStateId, threadId, or recipient. */
|
||||
@@ -303,6 +311,18 @@ export function dispatchAgentMessage(
|
||||
const agentNameMap = new Map(agentNameRows.map((a) => [a.id, a.name]));
|
||||
for (const wakeTarget of wakeTargets) {
|
||||
const agentName = agentNameMap.get(wakeTarget) ?? wakeTarget;
|
||||
// Prefer live-loop injection: if the recipient is currently running
|
||||
// a pi turn, the message lands in its steering queue and is
|
||||
// observed within one turn. When no active run exists, fall back to
|
||||
// the inbox + wake pipeline so a sleeping agent is woken with the
|
||||
// message queued for its next `prepareRun` cycle.
|
||||
const injected = message && deps.injectRuntimeMessage
|
||||
? deps.injectRuntimeMessage(wakeTarget, normalized.companyId, message)
|
||||
: false;
|
||||
if (injected) {
|
||||
mentionResults.push({ agentId: wakeTarget, agentName, outcome: "woken" });
|
||||
continue;
|
||||
}
|
||||
const runId = deps.wakeAgentIfPossible(wakeTarget, normalized.companyId, wakeTrigger);
|
||||
const hasQueuedWake = !runId && deps.db.listWakeupRequests(normalized.companyId, {
|
||||
agentId: wakeTarget,
|
||||
|
||||
Reference in new issue
Block a user