nxsflow
chat (nxc)

Commands

The complete nxc surface: fourteen commands, in the order a session tends to reach for them. Run nxc <command> --help for the generated reference (flags, types, defaults); this guide adds the narrative and the --json shapes. Two flags are global — --json on every command, and --db <path> (also NXC_DB) to point at a workspace database instead of discovering .nxs/ from the working directory.

The surface is small on purpose. There are exactly two ways to say somethingsend starts a conversation, reply answers one — plus withdraw, which takes a commission back before it starts, and release, which hands the working copy on when the chain holding it is over. Everything else reads. If you are looking for a verb that creates a channel, registers an agent, or advances a workflow step, see what is deliberately absent at the end.

Set up

nxc init

Activate chat in the current directory: ensure a .nxs/ workspace, register the chat module, then hand the shared agent file and the SessionStart hooks to the nxs assembler, which re-assembles them for every active module. --quiet does the same and prints no banner — that is the seam the umbrella drives, and the non-interactive entry for an agent.

nxc init                 # human, with the banner
nxc init --json          # the machine-readable receipt

--quiet is silent success — it prints nothing at all, which is what makes it drivable:

$ nxc init --quiet

nxc agent-manifest

What chat declares it contributes to the shared agent file — the prime command and the hook — as data rather than as prose:

$ nxc agent-manifest
nexus-chat agent manifest
  prime command:  nxc prime
  hook:           SessionStart → nxc prime

Run with --json for the machine contract the `nxs` umbrella assembles from.

--json is that contract; the umbrella reads it from all three blocks and assembles one AGENTS.md and one hook, so activating a second module never overwrites the first one's block.

Who is here

nxc list

The directory: who can be addressed in this workspace, and what for. It is a read over .nxs-personas/ — nothing else — so it shows exactly what is declared.

$ NXC_ACTOR=alice nxc list
## Who you can address

Address any of them the same way: `nxc send --to <handle> "<your message>"`.

**Coder** (handle: `coder`) — Implements a work order on a branch and merges it.

**Build-And-Ship** (handle: `build-and-ship`, members: coder, review) — the declared order a work order runs through

**Review** (handle: `review`, members: general, integrity) — the review quorum — one round asks both reviewers and hands back one verdict

Without --persona a human sees the whole declared team, while a persona running under its own session sees its own address book, recognised from that session. --persona <handle> projects that persona's view whoever is asking — useful for checking what an agent will actually see before you start it.

--json carries personas (handle, job_title, job_description, direct), channels (name, members, description), a public_channels array of the front doors it can see (omitted when there are none, and absent from the human rendering on purpose — a door no declaration here names is discoverable, not addressable), and a declarations block naming the folder it read and how many files were in it. That last block is the one to check when the list is emptier than you expected.

Say something

nxc send --to <persona|channel> <BODY>

Open a conversation. --to is required, and its target must be declared: a persona handle or a channel name from .nxs-personas/. The declaration decides what happens next — a persona is started on a fresh session, a channel fans out under its own policy — and the call returns the thread id either way.

$ NXC_ACTOR=alice nxc send --to coder --ref nxf_ids=ab12.0007 "Add a --since flag to the export command."
opened thread m-00000000000000000000000001 in dm:9445dbc93dfdad401585c42b — reply with `nxc reply --thread m-00000000000000000000000001 "…"`

Flags:

  • --ref k=v (repeatable) — what this conversation is about: nxf_ids (a flow item; repeatable because a first message may name several), branch, pr, session_id.
  • --no-ref — the explicit answer that there is genuinely no subject. Saying "I looked" where saying nothing says nothing.
  • --stream — watch the thread and the personas' transcripts until somebody answers, instead of returning immediately. For a human at a keyboard; refused inside a running persona.

Naming neither --ref nor --no-ref still sends, and warns — as a line on stderr for a reader and as a refs_warning field for an app, so the reminder cannot be lost between the two:

$ NXC_ACTOR=alice nxc send --to coder "One more thing." --json
{"thread_id":"m-00000000000000000000000002","message_id":"m-00000000000000000000000003","to":"coder","target":"persona","channel":"dm:9445dbc93dfdad401585c42b","session":"m-00000000000000000000000002","expects":["ab12/coder"],"spawned":true,"warnings":[],"refs_warning":"nothing was named as the subject of this conversation. Say what it is about — `--ref nxf_ids=<id>`, repeatable — or `--no-ref` if there really is nothing."}
warning: nothing was named as the subject of this conversation. Say what it is about — `--ref nxf_ids=<id>`, repeatable — or `--no-ref` if there really is nothing.

Sending to a channel returns the same receipt with target: "channel", the declared channel's id (decl:<name>), and the channel supervisor as the expected replier:

$ NXC_ACTOR=alice nxc send --to build-and-ship --ref nxf_ids=ab12.0008 "Ship the export change." --json
{"thread_id":"m-00000000000000000000000003","message_id":"m-00000000000000000000000004","to":"build-and-ship","target":"channel","channel":"decl:build-and-ship","expects":["ab12/__channel__"],"spawned":true,"warnings":[],"refs_warning":null}

The receipt's fields, in the order they are emitted: thread_id, message_id, to, target (persona or channel), channel, session (the internal session id, when a persona was started), queued_behind / queue_position (present only when the trigger is parked behind a working-copy lease), expects, deadline (when the channel declares a timeout:), spawned, warnings, refs_warning. warnings and refs_warning are always present — even empty and even null — so a reader never has to tell "no key" from "nothing to report".

A target no declaration names is refused, and the refusal says what was searched:

$ NXC_ACTOR=alice nxc send --to nobody --ref nxf_ids=ab12.0008 "Anyone there?" --json
? 1
{"error":{"kind":"not_found","msg":"no such target: nobody (not a declared channel, not a declared persona, not a channel in this workspace) — `nxc list` shows what can be addressed"}}

So is a persona that declared it is reachable only through a channel — for everyone alike, human or agent, named or anonymous:

$ NXC_ACTOR=alice nxc send --to general --ref nxf_ids=ab12.0008 "Have a look?" --json
? 1
{"error":{"kind":"validation","msg":"general is addressable only through review — send --to review instead"}}

nxc reply --thread <THREAD> <BODY>

Answer in a thread. --thread is required: a thread id identifies a conversation on its own, with no channel context, and the reply is routed back to whoever is on the other side of it.

$ NXC_ACTOR=coder nxc reply --thread m-00000000000000000000000001 "Added the flag and a test; branch feat/export-since." --json
{"posted":true,"message_id":"m-00000000000000000000000002","thread_id":"m-00000000000000000000000001","resumed":false,"warnings":[]}

resumed is true only when the reply woke the target's own return-address session — the direct one-to-one path. false covers two different things (there was no session to resume, or there was and the resume did not land), and wake_skipped beside it is what separates them. A quorum board's completion wake is not a "resume" in this sense and never sets it. posted is false only in the --if-unanswered no-op below.

  • --escalate — "I cannot carry out this task." One of the three things an agent may say with reply; the others are "I am finished" (a bare reply) and --needs-rework below. It is a declared signal, which is what makes it usable: a channel's supervisor branches on a bit whose meaning is written down, not on a phrase it has to interpret. The turn is discharged either way — what differs is the outcome.

    On an ordered channel the outcome is that the chain ends. The step after yours is not started, the round goes up as it stands, and whoever commissioned it decides. That is the point of the flag: a task that could not be carried out must not commission the work that was to follow it.

    It is not a way to say "not yet". There is no spelling for that on reply — every answer it has ends the turn, and a caller reads this one as work that will not arrive. If your step has commissioned a round of its own and is waiting for it, wait: answer once you have its result.

  • --needs-rework — "what you handed me does not meet the standard." The third answer, and the one about somebody else's work rather than your own. Name what must be put right in the body: those words are handed to the party that produced the work, and they are its next task.

    It is offered only where the channel declares somewhere for it to go (steps:on_needs_rework:) — that is what makes it a signal rather than a token you invent. A step without that edge is told about two endings, not three; set on such a step the bit falls away, with a verdict_dropped warning on the reply's own receipt. It may not be combined with --escalate: the two are statements about different work with opposite consequences. See nxc guide channels.

  • --if-unanswered — post only if the caller still owes a reply on this thread; otherwise a deliberate no-op (exit 0, posted: false, nothing written), never an error. It exists for one caller: the agent sidecar's teardown, which says something when an SDK session ends without ever answering the thread it owed, so the thread does not go quiet forever. You do not need it.

    It is the fallback, not the first move. Before speaking in the agent's name, the teardown gives the session its turn back once and tells it — naming the thread, both ways to end a turn, and any round it commissioned that is still open. Most sessions answer then. One that is reminded and ends silent again is handed back as an escalation: at that point no result exists and somebody has to decide what happens to the round, which is a different fact from a turn that simply produced nothing, and escalated: true on nxc status is where the caller reads it.

    Whatever it posts is marked as the runtime's. Any reply written through this door carries substituted: true on nxc status, so an app never has to read a message body to tell an answer the agent wrote from one written in its name. See limits and safety for the pairing with escalated.

  • --stream — as on send: watch the answer arrive.

There is no --kind, no --priority, no --disposition and no --ref on reply. A reply inherits its subject: the thread already says what the conversation is about, which is why the --ref obligation sits on send and nowhere else.

nxc withdraw --thread <THREAD>

Take back a commission that is still waiting for the working copy — one that never started. It prints one line per commission it took back (withdrew <role> on thread <id> (never started)), and --json carries the same as withdrawn / started_meanwhile.

A persona or channel that declares working_tree: exclusive runs one chain at a time; a commission that arrives while another chain is holding is parked in a queue and starts when the holder lets go. Until then nothing has happened: no session, no transcript, no model call. That is the whole of what this verb takes back, and the reason it is safe to.

Name the thread send handed you. Every parked commission under it is withdrawn, and each waiting thread is discharged with a message saying who took it back — so it is a record in the conversation, not an absence.

If every commission of the round was taken back, the thread you named is discharged the same way and stops counting as an open operation in nxc status. That is not a consolidation — nothing is delivered, because there is no answer — it is the register saying there is no round left. Without it, a send --to <channel> you withdrew went on being reported as running, since the channel thread was still waiting for a supervisor whose round had just been taken away.

What it does not do: stop a round that has started. That one has work in flight and a session mid-turn, and it is a different question with different answers. If a commission starts between this command reading the queue and writing to it, it is reported as started_meanwhile and left strictly alone — there is no window in which this can stop something that had begun.

A thread with nothing parked under it is a not_found saying so.

nxc release --thread <THREAD>

Give this workspace's working copy back when the chain holding it is over and nothing is going to give it back on its own. The other end of withdraw: that one takes back the commission that is waiting, this one moves the one that is in the way.

An escalation holds the working copy on purpose — the task is mid-flight and the answer is still travelling upward. When every session in that chain has ended, nothing is left that could ever say it is finished, and whatever is queued behind it waits forever. (Answering the escalation clears the hold inside a channel; on a direct thread it does not — see limits and safety, which used to claim otherwise.)

Name a thread inside the holding chainnxc status marks the operation that holds one with holds working tree, and nxc threads show <id> says working tree: holding on the thread itself. Naming the thread that is waiting is refused by name: that is the chain which wants the copy, not the one that has it.

It refuses while any session in the holding chain still has a live process, and says which. Taking the checkout from a chain that is still working is the collision the lease exists to prevent; this verb is for the other case. It asks nothing about whether the register looks tidy — a dead chain's register can say anything at all, and "nothing is running" is the fact that decides.

Whoever was parked starts, and the receipt says so (started, plus failed for a promotion that could not be started — its message is persisted and its session is minted, so it names what to go and look at). expired: true means the lease was already past its two-hour bound, so what this call actually did was drain the queue behind it.

Nothing is written to the conversation, unlike withdraw. The lease is a claim on files on one machine's disk — one row in this workspace's own database, never synced — so a message about it would sync to replicas where the fact is not true. What is visible afterwards is the consequence: the operation stops reporting holds working tree, and the chain that was waiting is running.

Read

None of these write anything, and none of them consume a message. --consumer <handle> reads as another qualified handle; without it, a command reads as the caller's own.

nxc inbox and nxc read were here, and both are gone (nxf 6j6v.1gm9). There is no verb for asking after your own messages, and none for acking them, because people pull and agents get pushed: an agent's message arrives in the prompt that starts its session, or in the turn that resumes it, so a pull verb was the duplicate of what had already been delivered — measured at 0 uses in 66 role sessions for each of the two. What a person reads instead is the CONVERSATION, which is what the two verbs below are for. The unread record itself is untouched on the app seam (prime --json carries it); its own future is nxf 6j6v.4d2z.

nxc threads list / nxc threads show <THREAD>

The quorum boards. list is every board the caller is a member of, each with its bulk state; --channel narrows it.

$ NXC_ACTOR=alice nxc threads list
m-00000000000000000000000003  decl:build-and-ship  0/1 in  [waiting]  · waiting for working tree (#1)
m-00000000000000000000000004  decl:build-and-ship  0/1 in  [waiting]  · waiting for working tree (#1)
m-00000000000000000000000001  dm:9445dbc93dfdad401585c42b  1/1 in  [complete]
m-00000000000000000000000002  dm:9445dbc93dfdad401585c42b  0/1 in  [waiting]  · holding working tree

show is one board in full — the quorum state plus the replies in order:

$ NXC_ACTOR=alice nxc threads show m-00000000000000000000000001
thread m-00000000000000000000000001 in dm:9445dbc93dfdad401585c42b
  expects:     ab12/coder
  replied:     
  outstanding: ab12/coder
  complete:    false
  working tree: holding
  · ab12/alice Add a --since flag to the export command.

The --json records carry thread_id, channel_id, opener, expects, replied, outstanding, complete, stale, working_tree and working_tree_queue_position. Reading many boards costs the same number of queries as reading two — that property is measured by a test, not merely intended, because a coordination UI reads all of them at once.

nxc status

Where an operation stands: the whole thread tree from its root down, across channel borders. A board tells you about one conversation; status tells you about the whole piece of work it started.

$ NXC_ACTOR=alice nxc status
operation m-00000000000000000000000001  dm:9445dbc93dfdad401585c42b  1 thread(s), 0 open
  m-00000000000000000000000001  dm:9445dbc93dfdad401585c42b  awaiting you (answered by ab12/coder)
$ NXC_ACTOR=alice nxc status --json
{"operations":[{"root":"m-00000000000000000000000001","channel_id":"dm:9445dbc93dfdad401585c42b","live":true,"open":0,"needs_decision":false,"holds_working_tree":false,"threads":[{"thread_id":"m-00000000000000000000000001","channel_id":"dm:9445dbc93dfdad401585c42b","depth":0,"state":"answered","awaiting_human":true,"escalated":false,"substituted":false,"opener":"ab12/alice","expects":["ab12/coder"],"stale":false}]}]}
  • --thread <id> — the one operation that thread belongs to, shown from its root, whether it is still running or already finished.
  • --channel <name> — the live operations whose root sits in that channel; a declared channel name or a raw channel id. An entry point, not an anchor: an operation crosses channels, and the tree follows it.
  • neither — everything still going on here.

Each thread carries a state of open / answered / stale, plus awaiting_human, which is true only at the root of a finished operation. That distinction is the point of the field: a stalled chain and a finished one that is waiting for a person look identical to a counter, and they mean opposite things.

Two flags sit on the operation rather than on a thread, and both answer a question the per-thread rows can only answer by being read one at a time:

  • needs_decision — somewhere under this root a task was handed back and nobody has taken it up. Read it next to awaiting_human: both say "the human is up", about situations of completely different urgency. A finished operation waiting to be read is the normal end; one whose root looks exactly the same while an unanswered escalation sits under it is a chain that has stopped — and an escalation holds the working copy, so it stops the machine too. It clears itself: re-commission the round and the flag goes.
  • holds_working_tree — somewhere under this root the working copy is held. It tells you whether to go looking; the per-thread working_tree field still says which thread.

nxc search <QUERY>

Case-insensitive substring over message bodies in the caller's channels, deterministically ordered.

$ NXC_ACTOR=alice nxc search "since flag" --json
[{"message_id":"m-00000000000000000000000001","channel_id":"dm:9445dbc93dfdad401585c42b","sender":"ab12/alice","body":"Add a --since flag to the export command."}]

It searches bodies only, and only in channels the consumer is a member of. To find what a session did rather than what it said, read its transcript.

Sessions and transcripts

These are the role runtime's own plumbing. The agent sidecar writes them; one of them is a read you will want by name.

nxc session bind <INTERNAL> <REAL>

Bind an internal (nxc-minted) session id to the real Claude Agent SDK session id the SDK handed back. The sidecar calls it once a session starts. An unknown internal id is reported as not_found.

nxc session state <SESSION> / --thread <ID>

Is that session still alive, or is it dead? The read opposite of the two writes the sidecar makes on this seam — session bind above and the session ended it calls last of all.

nxc session state m-01M0…            # one session
nxc session state --thread m-01M0…   # every session that ran on a thread, ended ones included

Three answers, and the third is not a hedge:

  • running — a live process stands behind it.
  • ended <instant> — the session announced its own end, and this is when. That is the fact a channel declared working_tree: exclusive opens its next step on.
  • unknown — nothing announced an end and no live process answers for it: a session killed hard, or one this machine never ran. Reporting ended there would state a fact nobody established.

unknown has a third reading, and the report tells you when you are looking at it. Whether a session is running is a question the worker answers, and not every worker can: one that starts no process has none to look at. So the answer carries worker_answers_livenessfalse means nothing here was ever asked, and no unknown under it says anything about a session. The shipped sidecar answers it, so on the normal command line the flag is true and an unknown really is a session that died without a word.

Use the session form at a thread that has gone quiet, before you conclude it failed. Use --thread before you touch a working copy a previous step may still have hands on: it lists what ran there and what became of each, which is the question nxc status cannot answer, because it reads a message board and this reads a process. An unknown session is not_found; a thread nothing ran on is an empty answer, not an error.

nxc transcript show <SESSION>

A session's normalized stream, rendered as a timeline: assistant text, thinking, tool calls and their results, with each Task-spawned subagent's entries nested under the tool_use that spawned them. The session id is the internal one — what send --to <persona> returns as session.

$ NXC_ACTOR=alice nxc transcript show m-00000000000000000000000001
transcript m-00000000000000000000000001  role=coder  (0 entries)

--from-seq <n> and --limit <n> page a long session: pass the largest seq the last chunk showed, and a chunk shorter than --limit is the end. An unknown session renders an empty transcript rather than failing — a session whose sidecar never flushed is indistinguishable from one that had nothing to say.

Two things to know before you paste one anywhere. A transcript contains raw tool inputs and results — whatever the agent read, wrote, or ran — so treat a dump like the workspace database, not like a message log. And it is deliberately not membership-gated: a transcript has no channel to gate on, and the table is device-local and never synced, so a gate would buy nothing that anyone holding the file cannot already do with sqlite3.

nxc transcript append --session <ID>

Append normalized entries, read as JSON-lines from STDIN. This is the sidecar's callback contract — the flag name, the stdin framing and the --json record are all load-bearing because an already-shipped producer depends on them. A line that does not parse is a loud validation error naming the line, never a silent skip.

nxc transcript prune

Retire the transcripts of sessions nobody has written to for a while, and say what went. Whole sessions, aged on their last recorded entry: a session still being written to is never a candidate however long it has been running, and a long one is never cut in half.

nxc transcript prune --dry-run          # report, remove nothing, take no write lock
nxc transcript prune --keep-days 7      # tighter than the configured window

You do not have to run it to keep the table bounded — the same retention rides the first flush of every new role session. Reach for it to clear a workspace that has gone quiet, or to apply a tighter window once. It stops the database growing; it does not make the file smaller (that needs a VACUUM, which is not run here because it rewrites the whole shared workspace under an exclusive lock). Sessions carrying no readable timestamp are of unknown age, so they are kept and reported separately — a visible gap rather than a silent one.

Documentation

nxc guide [TOPIC]

The guides you are reading, compiled into the binary. No workspace is needed and no network:

nxc guide                     # list chat's topics
nxc guide core-concepts       # print one
nxc guide --json              # [{topic, summary}, …] — the agent contract

nxs guide fans out over the active modules and lists all three blocks' topics at once. Where a topic name exists in more than one block — getting-started and commands do — it refuses to pick for you and names the per-tool commands instead.

Not verbs you type

Two subcommands exist and are hidden from --help, because nobody should type them:

  • nxc prime — the session bootstrap. Chat's own SessionStart hook runs it directly, and nxs prime fans out to it when you ask the umbrella by hand.
  • nxc tick --thread <id> — a clock's hand. The one-shot job a channel's declared timeout: schedules runs it to re-check a thread and, if due, route it through the channel's on_complete policy. It is idempotent: a thread whose completion was already handled is a clean no-op, never a second wake.

They are named here so that finding them in a process list or a log is not a mystery. Do not build on them.

What is deliberately absent

Half of a trustworthy reference is what it says is not there. These verbs existed and were removed; each entry says what to do instead.

  • nxc ask — folded into send --to. A channel is addressed exactly like a persona, and one verb that mints a thread beats two that disagree about whether they do.
  • nxc channels create / dm / join / leave — gone with the raw channels. A channel is a declaration in .nxs-personas/channels.yaml; membership is its members: list. A direct conversation is minted for you the moment you send --to a persona, under an id derived from the two handles, so there is nothing to create. Cross-project discovery of public channels left the agent surface entirely and is an app's job — nxc list --json still carries the front doors it can see.
  • nxc agents register / list / search / show — a team is declared, not registered. nxc list is the read, and it searches the same job_title / job_description fields agents search did. A profile row registered at runtime lived in one machine's database, nothing reviewed it, and it did not survive the run.
  • nxc workflow start / step / status / tick / done / bind / append / show / expect — the declarative run engine is gone, along with the run record. A channel declares its own order (flow: sequential), send --to <channel> starts it, and nxc status is where an operation's position is read. See channels.
  • send --role / --session--to is the one way to name a target. --role collapsed into it; --session is a recorded gap rather than a collapse, and is named as such in the source.
  • send --kind / --priority / --disposition / --model / --deadline, and reply --kind — removed together. The first three let a caller answer a question nobody had asked it; the last two are declared at the persona and at the channel, and a per-call override beside a declared value is two answers to one question. Say how much thinking a job is worth in the persona's stage: or model:, and how long a board may wait in the channel's timeout:.

Next

Type to search. Matches are found by wording and by meaning, so a section can answer a question it does not spell out.