Appearance
Named-Agent-Messaging
Named-Agent-Messaging is the file-based addressed-dialogue channel between agents inside a single np:execute-phase task. Critics can ask Executors per-finding clarifications; Executors / Build-Fixers read their inbox before editing; the Nubosloop's commit-phase refuses while any expects_reply: true request is unarchived.
ADR-0015 ratifies the design. The orchestration lives in lib/messaging.cjs; CLI verbs in bin/np-tools/messages-{send,inbox,archive,thread}.cjs. It adds no new runtime dependencies: just fs plus crypto.randomUUID.
Why an addressed channel
Two coordination patterns are not addressable through the existing checkpoint / handoff artefacts:
- Per-finding handback. When the Critic emits N findings, the routing engine merges all
style → executorfindings into one Build-Fixer prompt. The Executor cannot acknowledge per-finding ("understood; fixed; ready for re-check"); the next round re-evaluates from scratch and ambiguous findings cannot be deferred or re-routed without falling out toaskuser. - Critic and Executor dialogue inside a round. The round structure is fixed: Executor → Critic → Routing → either commit or re-loop. There is no mechanism for the Critic to ask the Executor a clarifying question ("did you intend to delete
FoobarService, or was that a side-effect?") without falling out toaskuser.
A persistent, addressed, file-based channel inside .nubos-pilot/messages/ lets agents within a round (and within a task across rounds) carry on a structured dialogue. It is append-only filesystem state, the same shape as lib/handoff.cjs and the per-task audit log: no daemon, no network bus.
Layout
.nubos-pilot/messages/ # Project-State sub-tree
inbox/<agent-name>/<msg-id>.json # ungelesen, addressed at <agent-name>
archive/<msg-id>.json # processed (acked / replied)
archive/by-task/<task-id>/<msg-id>.json # post-task historical archive (sweep target)
manifest.jsonl # append-only audit log: sent / archived / task-swept events<msg-id> is <unix-ms>-<uuid>: a millisecond timestamp prefix for sort-stability, a UUID suffix for parallel-spawn collision resistance.
Message schema
json
{
"id": "1730000000123-9b3e...",
"from": "np-critic",
"to": "np-executor",
"phase": "M005-S007-T0002",
"round": 2,
"kind": "request|response|notify",
"subject": "filament-resource-policy-missing",
"body": "...",
"expects_reply": true,
"in_reply_to": "1729999999999-...|null",
"created_at": "2026-05-08T..."
}kind semantics:
| kind | expects_reply | Behaviour |
|---|---|---|
request | typically true | receiver must produce a response (matched by in_reply_to) before the request can be archived |
response | always false | carries in_reply_to pointing at the request it answers; messages-archive of the request only succeeds once a response exists |
notify | always false | informational; archived by receiver after read |
subject is a kebab-case finding-category or topic id, matching the Critic finding-category taxonomy where applicable (style, dead-code, missing-test, weak-assertion, unmet-criterion, scope-creep, …) so the routing engine can index by it.
Where it plugs in
Critic → Executor / Build-Fixer
np-critic's prompt (Inter-Agent Messaging section, ADR-0015) authorises emitting an addressed request message when a per-finding clarification belongs in the dialogue layer, not the findings JSON:
bash
node .nubos-pilot/bin/np-tools.cjs messages-send \
--from np-critic --to np-executor \
--phase <task-id> --round <current-round> \
--kind request --subject <finding-category> \
--body "<question>" --expects-replyThe findings JSON (written to <report_path> per the Verdict-Only Contract, ADR-0010 §L5) remains canonical for routing. Messages are dialogue, not findings. A Critic that has nothing to ask sends nothing.
Executor / Build-Fixer round-2+ inbox-read
Both agents check their inbox before editing:
bash
node .nubos-pilot/bin/np-tools.cjs messages-inbox --agent np-executor --task <task-id>For each kind=request with expects_reply=true, the edit must resolve the question and the agent sends a response:
bash
node .nubos-pilot/bin/np-tools.cjs messages-send \
--from np-executor --to np-critic \
--phase <task-id> --round <round> \
--kind response --subject <same-subject> \
--body "<resolution>" --in-reply-to <request-id>The response unblocks messages-archive(<request-id>); only then can the request leave the inbox and the Layer-B gate open.
Layer-B termination predicate
bin/np-tools/loop-run-round.cjs::_runCommit extends the existing Layer-B preconditions (verify-green, findings empty) with a new check:
js
if (messaging.pendingReplies(taskId, cwd).length > 0) {
throw new NubosPilotError('loop-commit-precondition-missing', '...', {
missing: 'pending-replies-cleared',
observed_pending_replies: <n>,
pending_subjects: ['fix-x', ...],
});
}Unanswered requests block the commit. The error envelope surfaces the open subjects so the orchestrator can surface them to the user. --force-commit-phase bypasses the gate (same shape as the existing Layer-A/B overrides).
Phase-completion sweep
After a successful commit, _runCommit calls messaging.sweepTaskOnCommit(taskId, cwd), which moves every message with phase === taskId from inbox/ and archive/ into archive/by-task/<taskId>/. The manifest.jsonl gets a task-swept event with moved: <count>. Future tasks see clean inboxes; the per-task audit-trail stays historically accessible.
The _runCommit response now includes:
json
{ "messages_swept": <count> }Audit-trail interplay with Layer-C
Messaging events do not replace the ADR-0010 §Trust Layer Layer-C audit; they augment it. A Critic that emits messages-send --kind request from inside its spawn still has its loop-audit-tool-use --agent np-critic stamp applied by the orchestrator after spawn-return. The message itself is an additional artefact, not a substitute for the audit entry.
A hostile orchestrator that wants to fake messaging can write inbox files directly, since the filesystem is unguarded. This is the same threat model as Layer-C today (the audit log is appended by the orchestrator, not the runtime). The same Stufe-2 runtime instrumentation that closes the audit class will close the messaging class.
Lifecycle and cleanup
- Per-task scope — messages are scoped to the task that produced them (
phasefield). At task-commit time,sweepTaskOnCommitmoves them out of the active routing surface. - Append-only manifest —
manifest.jsonlis never truncated; it is the audit-trail. - TTL — none in v1. Orphan inboxes (operator-killed mid-task) are surfaced by
np:doctorasmessaging-orphan-inbox.
Messaging is never committed
Per the same User-Vorgabe as Vector-Memory: .nubos-pilot/messages/ is runtime-state, not source-of-truth. The directory belongs in the consumer-project's .gitignore. Replay from manifest.jsonl is possible but not load-bearing.
CLI
bash
# Send (messaging is always available — no config gate)
node .nubos-pilot/bin/np-tools.cjs messages-send \
--from np-critic --to np-executor \
--phase M001-S001-T0001 --round 2 \
--kind request --subject style \
--body "did you mean to drop FoobarService?" --expects-reply
# Inbox listing (filterable)
node .nubos-pilot/bin/np-tools.cjs messages-inbox --agent np-executor \
--task M001-S001-T0001 --kind request
# Archive (refuses request+expects_reply with no prior response)
node .nubos-pilot/bin/np-tools.cjs messages-archive <msg-id>
# Reply-chain in causal order
node .nubos-pilot/bin/np-tools.cjs messages-thread <msg-id>Each verb returns JSON on stdout. Errors flow through the NubosPilotError envelope with codes including messages-archive-without-reply, messages-already-archived, messages-recipient-unknown, messages-orphan-inbox.
Worked trace
A round-2 dialogue that resolves before commit:
| Round | Step | Action |
|---|---|---|
| 1 | Executor edits; verify red | round 1 → round 2 |
| 1 | Critic finds ambiguous deletion + emits request to executor | expects_reply: true, subject style |
| 2 | Build-Fixer reads inbox, sees the request | resolves the deletion intent in the patch |
| 2 | Build-Fixer sends response, archives the original request | inbox empty |
| 2 | Critic re-runs, zero findings | routing → commit |
| commit | _runCommit checks pendingReplies(taskId) === 0 | passes |
| commit | sweepTaskOnCommit(taskId) moves all 2 messages to archive/by-task/M001-S001-T0001/ | messages_swept: 2 |
A stalled dialogue:
| Round | Step | Action |
|---|---|---|
| 2 | Critic asks request A | pendingReplies = 1 |
| 3 | Build-Fixer's response misses the point | request A still in inbox; new request B emitted |
| commit | _runCommit refuses with pending-replies-cleared | pending_subjects: ['style', 'unmet-criterion'] |
Related
- Nubosloop — Layer-B precondition and the commit-phase sweep hook.
- Findings Routing — messages are dialogue, findings are routing; this concept page covers the dialogue layer only.
- ADR-0015 — full architectural decision record.
