Skip to content

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:

  1. Per-finding handback. When the Critic emits N findings, the routing engine merges all style → executor findings 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 to askuser.
  2. 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 to askuser.

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:

kindexpects_replyBehaviour
requesttypically truereceiver must produce a response (matched by in_reply_to) before the request can be archived
responsealways falsecarries in_reply_to pointing at the request it answers; messages-archive of the request only succeeds once a response exists
notifyalways falseinformational; 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-reply

The 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 (phase field). At task-commit time, sweepTaskOnCommit moves them out of the active routing surface.
  • Append-only manifestmanifest.jsonl is never truncated; it is the audit-trail.
  • TTL — none in v1. Orphan inboxes (operator-killed mid-task) are surfaced by np:doctor as messaging-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:

RoundStepAction
1Executor edits; verify redround 1 → round 2
1Critic finds ambiguous deletion + emits request to executorexpects_reply: true, subject style
2Build-Fixer reads inbox, sees the requestresolves the deletion intent in the patch
2Build-Fixer sends response, archives the original requestinbox empty
2Critic re-runs, zero findingsrouting → commit
commit_runCommit checks pendingReplies(taskId) === 0passes
commitsweepTaskOnCommit(taskId) moves all 2 messages to archive/by-task/M001-S001-T0001/messages_swept: 2

A stalled dialogue:

RoundStepAction
2Critic asks request ApendingReplies = 1
3Build-Fixer's response misses the pointrequest A still in inbox; new request B emitted
commit_runCommit refuses with pending-replies-clearedpending_subjects: ['style', 'unmet-criterion']
  • 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.