{
  "markdown": "# YAAC — yet another agentic chat\n\n<!-- mcp-name: io.github.amyodov/yet-another-agentic-chat -->\n\nA radio for agentic coding sessions.\n\nYou have several sessions open at once — different worktrees, different tasks,\nmaybe different people driving them. They need to tell each other things:\n*\"schema for rosters changed, the field is `recipient_group` now\"*, *\"pushing the\nrefactor in ten minutes, hold your commits\"*. YAAC is how they do that, and how\nyou talk into the same conversation by hand.\n\nThe mental model is **a network of handheld radios, not a phone network.** Buy one\nand it works — it just has nobody to talk to. Buy a second and there's a\nconversation. There is no config file, no environment variable, no port to choose,\nno daemon, and nothing to run first. Sessions find each other at a fixed local\naddress — `tcp://127.0.0.1:19116`, and `19116` is `0x4AAC`, which is where the\nname comes from. Whichever session needs it first claims it and relays for the\nothers; if that session goes away, another takes over by itself, within a few\nseconds and without anyone doing anything.\n\n## What makes it different\n\n**It connects sessions that were never designed to meet.** Agents *inside* one\nharness could always talk — an orchestrator wires its own subagents, and that\nwas never the problem. YAAC is for two (or more) unrelated sessions in\nunrelated clients, alive on your machine right now: a Claude Code session and\na Claude Desktop chat, Codex, Gemini CLI — anything that can run a local MCP\nserver. One conversation is 300k tokens into a task; another, 400k tokens in,\nholds exactly the experience it needs. Give them a radio. If you can talk to\nboth of them, now they can talk to each other.\n\n**Configuration rounds to zero.** If you can add a local MCP server, you are\ndone — no Redis to stand up, no PostgreSQL to prepare, no broker, no port to\nchoose. Adding YAAC hands each client a radio, switched off. Then, at any\nmoment, you tell a session \"connect to yaac\" — and it deals with the rest.\n\n## What it's good for\n\n- **Parallel worktrees on one repo.** Two sessions refactor on different\n  branches. The one that renames a field tells the other before it builds a\n  day's work on the old name.\n- **A manager conversation.** You discuss what to build with Claude Desktop; it\n  passes the task to a Claude Code session over YAAC and collects the result.\n  Chat conversations and coding sessions are equal participants — any MCP client\n  can join.\n- **Announcements.** *\"CI is red, hold your pushes\"* — one broadcast reaches\n  every session on the channel.*\n- **Long jobs.** One session babysits a slow test suite and messages the coding\n  session when it goes green, instead of you ferrying the news by hand.*\n\n\\* On Claude Code, installed as a plugin, these arrive on their own — see\n*Instead: as a plugin*. Everywhere else the listening session receives them next\ntime it checks its inbox; see *Messages do not arrive on their own* below.\n\n## Installing\n\nTwo ways in. **As an MCP server** works in every MCP client and is the one to\nreach for by default. **As a plugin** is fewer steps and brings a skill along,\nbut only on clients that implement one of the two plugin standards.\n\nAll of these need [uv](https://docs.astral.sh/uv/) on your PATH. `uvx` fetches\n[the package](https://pypi.org/project/yet-another-agentic-chat/) and a suitable\nPython by itself, so there is nothing else to install and no virtualenv to\nmanage.\n\n### Claude Code\n\n```bash\nclaude mcp add yaac -s user -- uvx yet-another-agentic-chat\n```\n\n`-s user` installs it for every project on the machine, which is usually what you\nwant: a radio only one of your sessions can hear is not much of a radio. Leave it\nout to add YAAC to the current project only.\n\nCheck it took with `claude mcp list`, or `/mcp` inside a session.\n\n<details>\n<summary><b>Claude Desktop</b></summary>\n\nAdd YAAC to `claude_desktop_config.json`, then restart the app:\n\n- macOS — `~/Library/Application Support/Claude/claude_desktop_config.json`\n- Windows — `%APPDATA%\\Claude\\claude_desktop_config.json`\n\n```json\n{\n  \"mcpServers\": {\n    \"yaac\": {\n      \"command\": \"uvx\",\n      \"args\": [\"yet-another-agentic-chat\"]\n    }\n  }\n}\n```\n\nIf Desktop reports that it cannot find `uvx`, give the absolute path instead —\n`which uvx` will tell you where it is. GUI applications do not always inherit the\nPATH your shell has.\n\n</details>\n\n<details>\n<summary><b>Any other MCP client</b></summary>\n\nYAAC is a plain stdio MCP server with no client-specific behaviour. Whatever your\nclient's configuration looks like, the two things it needs are:\n\n- **command** — `uvx`\n- **arguments** — `yet-another-agentic-chat`\n\nSessions on different clients can talk to each other, as long as they are on the\nsame machine.\n\n</details>\n\n<details>\n<summary><b>Instead: as a plugin</b></summary>\n\nIf your client speaks one of the two plugin standards, this replaces the\nconfiguration above and adds a skill explaining how to use the radio.\n\n**Claude Code**, which has its own plugin format. This repository is the\nmarketplace:\n\n```\n/plugin marketplace add amyodov/yet-another-agentic-chat\n/plugin install yaac@yaac\n```\n\nThere it brings the one thing the MCP server alone cannot: messages arrive\n**without being asked for**. A hook hands the session whatever came in — as it\nworks, when you type, and as a turn ends, which is the one that reopens a\nfinished turn so it can act on the news. It is a delivery, not a nudge: the\nmessage text itself, already read, with `check_inbox` left for when you want to\nlook on purpose. The same hook fires after a compaction and hands the session\nback the connections it holds, since a summary drops the identifiers the other\ntools need. Nothing to configure, and silent when there is nothing to say.\n\n**[Agent Plugins](https://agent-plugins.org/) 1.0.0** — ChatGPT, Codex, Cursor,\nGitHub Copilot, Kiro, VS Code. Point your client at this repository; the plugin\nis the `plugin/` directory. Claude Code is not part of that standard, which is\nwhy there are two sets of instructions rather than one.\n\nEither way the plugin runs the published package with `uvx`, so it carries no\ncopy of the server and picks up new releases without being reinstalled.\n\n</details>\n\n<details>\n<summary><b>A terminal client for you, not for an agent</b></summary>\n\nTo sit on a channel yourself and watch it live:\n\n```bash\nuvx --from \"yet-another-agentic-chat[chat]\" yaac-chat\n```\n\nIt joins as an ordinary participant, so agents see you as one of them. Unlike an\nMCP session it gets messages the moment they arrive — the pull-only limitation\nbelow is MCP's, not YAAC's, and a terminal has no such problem. See\n[`docs/tui.md`](docs/tui.md).\n\n</details>\n\n<details>\n<summary><b>Development version</b></summary>\n\nTo run the latest unreleased code, replace `yet-another-agentic-chat` with\n`git+https://github.com/amyodov/yet-another-agentic-chat` in any command above.\nTo hack on a local checkout, see [`docs/development.md`](docs/development.md).\n\n</details>\n\n<details>\n<summary><b>Advanced: a different meeting place, or a session that will not relay</b></summary>\n\nAppend one of these as the final argument to any command above. All three name\nthe same thing — where sessions meet — and which one you use says what this\nsession is willing to do there:\n\n| | |\n| --- | --- |\n| `--rendezvous tcp://127.0.0.1:<port>` | Relay if this session gets there first, join whoever did otherwise. The default, and what happens with no flag at all. |\n| `--bind tcp://127.0.0.1:<port>` | Insist on relaying: refuse to join if another session already holds it. |\n| `--connect tcp://127.0.0.1:<port>` | Never relay for anybody. Join, talk, and leave the spine to someone else. |\n\nEvery session that should hear the others must be given the same address:\nsessions on different ones are invisible to each other, which is also exactly\nwhat makes this useful for a second, isolated net — a development build beside\nyour daily one, say, as in [`docs/development.md`](docs/development.md). Pick a\nfree port below 32768, out of the range the kernel hands to outbound\nconnections.\n\nThere is no `--port`, because the address already carries one. The two flags\nbeyond the default are worth having for opposite reasons: a terminal client you\nleave open makes an excellent stable hat, and `--bind` is how you find out it\ndid not become one, rather than discovering it when the net changes hands. A\nsession that will be gone in a minute makes a poor one, and `--connect` is how\nit declines.\n\nKeep the address on loopback. YAAC has no authentication and is not meant to —\nthe whole design assumes one machine and one user — so binding something the\nnetwork can reach hands an open message bus to whoever finds it. That is yours\nto do if you mean it; you will get a warning on stderr, not a refusal.\n\n</details>\n\n## Using it\n\nNothing happens until you say so. A freshly installed YAAC opens no socket and\ncreates no file — it is a switched-off radio that knows how to be switched on.\nIdle cost is as close to zero as it gets: no listener, no connection, nothing\non disk, and on most clients only two tools' worth of context in the session —\nthe full toolset appears when you join and withdraws when you leave. The first\nsession that actually joins is the one that binds the socket for everyone.\n\n```\nyou:    what channels are on the air?\nagent:  [list_channels] → \"z combinator forum\" (3), \"doom 13\" (1)\n\nyou:    you are Колян, go help Диман on z combinator\nagent:  [join_channel(channel=\"z combinator forum\", name=\"Колян\")]\n        Connected. Диман is here. Note this did not create the channel.\n```\n\nGoing on air is always an explicit act by you. The name is **your** choice —\nYAAC will never infer one from the directory, the hostname, or the task.\n\nChannel names and participant names are raw text. Any string works: spaces,\nCyrillic, emoji, punctuation. Nothing is reserved, parsed, or case-folded.\n\n### Tools\n\n| Tool | What it does |\n| --- | --- |\n| `list_channels()` | What is on the air, with participant counts. No side effects. |\n| `join_channel(channel, name)` | Go on air. If nobody is on the channel, joining creates it — and says so. |\n| `send(body, name=None)` | Message one participant, or the whole channel if `name` is omitted. |\n| `check_inbox()` | Read what has arrived since last time. |\n| `peers()` | Who else is on your channel. |\n| `dev_connections()` | Diagnostic: every connection you hold, with unread counts. |\n| `leave_channel()` | Leave one channel and remove its inbox. |\n\nOnly the first two are offered until you join something. The rest appear once you\nare on air and disappear when you leave the last channel, so a session that never\njoins carries almost nothing. On a client that cannot handle a changing tool list,\nall seven are listed from the start instead — see [Compatibility notes](#compatibility-notes).\n\nYou may be on more than one channel at once. `join_channel` returns a connection\nid; pass it as `connection_id` when you hold several, and leave it out when you\nhold one.\n\nThere is no separate verb for creating a channel, because a channel is not a\nlasting object — it exists exactly as long as somebody is on it. Joining an empty\nname is what brings it into being, and the result says `created: true` so a typo\ncannot quietly leave you alone on `z combinator forun`.\n\n### Messages do not arrive on their own\n\n**This is the thing to understand about v0.** MCP has no way for a server to push\ntext into a session that is sitting idle — the protocol simply has no such verb.\nSo an agent only hears what it has collected: **it must call `check_inbox()`**.\n\nThe tool descriptions tell it to do this before acting and before ending a turn,\nand every YAAC tool result carries an unread count as a nudge. It still means a\nmessage sent to an idle session waits until that session's agent next checks. If\nyour agent seems deaf, tell it to check the inbox.\n\n### Getting messages sooner\n\nTwo clients can be told to hand a session its mail without it asking. Both are\noptional, both are additive, and plain `check_inbox()` keeps working underneath.\n\n**Claude Code** gets it from the plugin, with nothing to configure: a hook hands\nthe session whatever arrived, as it works and as a turn ends. Those messages are\nthen already read. The one thing a hook cannot reach is a session sitting idle,\nso `join_channel` also returns a `watch` URL — point the `Monitor` tool at it\nonce per join and each arrival becomes an event, even while nothing is running.\nThat event is a doorbell, not the message: `check_inbox()` still reads it.\n\nThe same hook covers the other way a session goes quiet. `join_channel()` returns\na `connection_id` and a `peer_secret` that the other tools require, and a\ncompaction is exactly the thing that drops an opaque string from a conversation —\nleaving a session still on the air, still holding its name, and unable to say a\nword. So the plugin also fires on a post-compaction `SessionStart` and hands the\nsession back what it was holding. Nothing is stored to make that work: the\nmemberships never went anywhere, only the model's record of them did.\n\nIf you are not on Claude Code, or the hook does not fire, the recovery is the\nsame one the tool descriptions name: call `join_channel()` again with the same\nchannel and name, and the membership you already hold comes back, secret\nincluded.\n\n**Codex** needs one file, because its hooks cannot call an MCP tool — so a\nseparate program answers them. In `~/.codex/hooks.json`, or `.codex/hooks.json`\nin a project:\n\n```json\n{\n  \"hooks\": {\n    \"PreToolUse\": [{\"hooks\": [{\"type\": \"command\", \"command\": \"yaac-hook\"}]}],\n    \"Stop\": [{\"hooks\": [{\"type\": \"command\", \"command\": \"yaac-hook\"}]}]\n  }\n}\n```\n\nNothing to name and nothing to keep in step: `yaac-hook` asks the same address\nevery participant already meets at which sessions are running here, and picks\nout its own by what its client told both halves, by the directory it was started\nin, or by the process line it shares with its server. Several Codex sessions at\nonce are told apart the same way. Where that is genuinely ambiguous it says\nnothing, because somebody else's mail is worse than none.\n\nCodex reviews a hook before it runs it — approve it with `/hooks`. From then on,\na session is told when mail is waiting and reads it with `check_inbox()` itself.\n\n<details>\n<summary><b>Waking an idle Codex session</b></summary>\n\nA hook only fires when a session does something, so none of the above reaches one\nwaiting at its prompt. Codex can be reached there through its **app-server**,\nwhich is experimental and off by default. Run your session under one:\n\n```bash\ncodex app-server --listen ws://127.0.0.1:4500\n```\n\nThat is the whole of it. There is nothing to add to `config.toml`, because the\napp-server is the process that starts the MCP server, and its address is written\non its own command line — so a session finds its own door by looking up its\nancestry. Not *an* app-server on the machine: the one actually running it, which\nis why several at once are no more ambiguous than one, and why Codex's permanent\n`app-server daemon`, which listens on nothing, is never mistaken for a door.\n\nYAAC then puts a line in front of that session when mail arrives, exactly as if\nyou had typed it — the model reads its history, hooks fire, and `check_inbox()`\ndoes the rest. It joins the session's queue rather than barging in, so a session\nthat is in the middle of something is told when it finishes rather than\ninterrupted. One wake covers any number of messages, and the next needs new mail\nto exist.\n\nThe app-server is experimental, and the port is yours to pick. Every failure is\nsilent: nothing above this session that could start a turn, nothing listening, no\nsuch thread. Your mail waits in the inbox exactly as it would have anyway, so the\nworst case is the behaviour you had before.\n\nCredit where it is due: this route was found by Vadim, who had it working before\nit was in YAAC at all.\n\n</details>\n\n**Both clients:** `join_channel()` returns a `peer_uid` and a `peer_secret`.\n`send()`, `peers()` and `check_inbox()` want the secret back — it keeps one\nconversation from reaching into another's connection in a client that runs a\nsingle server for the whole application, and it is an honour-system convention\nrather than a boundary, since everything here runs under one user account.\nJoining again with the same pair comes back as the same participant, which is how\na session reclaims its name after a restart.\n\nNothing here is required, and nothing writes to disk.\n\n### Direct by default\n\n`send` addresses one person unless you leave out the name. A broadcast\ninterrupts every session on the channel and costs each of them context, so it is\nfor genuine announcements — not politeness.\n\n`send` reports `accepted`, never `delivered`. It means handed to the network. It\ndoes not mean anybody read it, and today it does not even guarantee arrival.\n\n## What it is not\n\nNot a chat application. No threads, no reactions, no history, no shared task\nlist, no \"who is editing which file\" presence. These were considered and left\nout on purpose.\n\n## Honest limitations\n\n**v0 may lose messages.** There is no spool, no retry, no acknowledgement. A\nmessage in flight while the rendezvous point changes hands is gone. What v0\npromises is that it loses messages *loudly*: an undeliverable message produces a\nbounce in the sender's inbox rather than silence.\n\n**A channel is not a confidentiality boundary.** Whichever session claimed the\nrendezvous point relays all traffic, in every channel, in clear text — and that\nis an ordinary session that happened to get there first. A channel isolates\nparticipants at the transport level, so you never receive another channel's\nmessages; but the relaying session sees everything — the hat is Eve by\nconstruction, not by accident. On one machine under one user account this is\nfine. Do not treat it as more than it is.\n\n**Messages become context in the receiving session.** Whatever another\nparticipant sends is read by your agent as text it may act on — \"hold your\ncommits\" is indistinguishable from an instruction you typed yourself. The hat\nnever parses a body, so nobody can forge the protocol or another name, but\nnothing prevents a body from *reading* as an instruction. Join channels with\nsessions you trust, and treat an incoming message the way you would treat a\nmessage in any chat: as something a person said, not as a command.\n\n**Local only.** `127.0.0.1`. No multi-host, no authentication, no encryption.\n\n## Compatibility notes\n\n<details>\n<summary><b>Codex</b></summary>\n\nCodex works with YAAC. It just costs more context there than it should, and the\nreason is worth knowing.\n\nMCP lets a server change its tool list while running and say so, with\n`notifications/tools/list_changed`. YAAC uses that: a dormant session carries two\ntools, and the other five appear the moment you join a channel. Codex receives\nthe notification, writes a line to its log, and re-reads nothing — the tool list\na session sees is fixed when its thread starts, and no prompting will shake it\nloose. Left alone, a Codex session could join a channel and then be unable to\nsend, read, or leave.\n\nSo YAAC looks at who connected. When the client identifies itself as Codex, all\nseven tools are listed from the start, because a tool published later is one that\nclient will never see. There is nothing to configure — it works, it is simply\nfive tool descriptions a Codex session may never use.\n\nThis is not new, and it is not obscure:\n\n- [openai/codex#10105](https://github.com/openai/codex/issues/10105) — *\"Support\n  `notifications/tools/list_changed`\"*, open since January 2026. Filed against a\n  part of the spec that has been there since 2024-11-05.\n- [openai/codex#12449](https://github.com/openai/codex/pull/12449) — a working\n  implementation, contributed and closed within six hours as an \"unsolicited code\n  contribution\". Never merged.\n- [openai/codex#33266](https://github.com/openai/codex/issues/33266) and\n  [#35583](https://github.com/openai/codex/issues/35583) — the same bug found\n  again, independently, in the CLI and in the desktop app.\n- [openai/codex#19155](https://github.com/openai/codex/issues/19155) — the same\n  stale cache, this time serving a tool schema that no longer exists.\n\nClaude Code, Gemini CLI and OpenCode all implement it. OpenAI's stated policy is\nto prioritise by community upvotes, and on #12449 the reason given for not acting\nwas that #10105 *\"has received zero upvotes\"*. So if the extra tools bother you,\nyou know where to vote.\n\n</details>\n\n**On Claude Desktop, one name per conversation takes a little care.**\nDesktop runs one MCP server for the whole application rather than one per\nconversation. YAAC handles that — a session can hold several connections at once,\neach with its own name and inbox — but the conversation has to remember which\nconnection is its own. A call that cannot tell which connection you meant reports\nthe choices, and `dev_connections()` lists them on demand.\n\n## Status\n\n<details>\n<summary><b>What's working</b></summary>\n\n- Join a channel under a chosen name; leave and go dormant again\n- Several channels at once, each with its own name and inbox\n- Sessions in **different clients** talking to each other — a Claude Code session\n  and a Codex session on one channel is a tested case, not a claim\n- A terminal client, so you can be on the channel yourself; it gets messages the\n  moment they arrive, with nothing to poll\n- A tool list that grows when you connect and shrinks when you leave — and, on a\n  client that would never re-read it, is complete from the start instead\n- Direct messages and channel broadcasts, with the two distinguishable on arrival\n- Mentions: a broadcast everyone hears that calls on one person by name, which is\n  a different thing from whispering to them\n- Tags and a JSON payload beside the text, for messages that are more than a\n  sentence\n- A peer identity that survives a restart, so a session that comes back reclaims\n  the name it had rather than being told it is taken\n- And a membership that survives a compaction: on Claude Code the session is\n  handed back what it held, and on any client rejoining the same channel under\n  the same name returns it\n- Channel creation reported, so a mistyped channel name is caught immediately\n- Bounces for messages that could not be delivered\n- Nickname collisions refused, except when the holder's session is gone, or when\n  the holder is you coming back\n- Automatic takeover when the relaying session disappears, in a few seconds, with\n  no user action and no configuration\n- `list_channels` from a session that has not joined anything, with no side effects\n- Installable as a plugin as well as a plain MCP server, in both plugin standards\n- On Claude Code, messages delivered into the session as they arrive, without\n  anyone remembering to ask\n- On Codex, the same, from a hooks file that names nothing: the hook finds its own\n  session at the address everyone already meets at\n- And a watch a session can arm once, so mail reaches it even while it sits idle\n  doing nothing — the one case a hook cannot cover, since a hook needs the\n  session to act first\n- A Codex session waking from its prompt when mail arrives, joining its queue\n  rather than interrupting whatever it is in the middle of\n- One address under three names — `--rendezvous`, `--bind`, `--connect` — so a\n  session can insist on relaying, or refuse to, without anyone deciding who wins\n- Runs on macOS, Linux, and Windows — every commit runs the full test suite on\n  all three\n\n</details>\n\n<details>\n<summary><b>What's planned</b></summary>\n\nEverything below is additive. Pure MCP keeps working underneath all of it, so a\nclient with no extension mechanism at all loses nothing it has today, and none of\nthis changes the core.\n\nA [Claude Code channel](https://code.claude.com/docs/en/channels) would do what\nthe watch does, more neatly and with no watcher to arm: events arrive as\n`<channel>` tags in the model's own context. It is a research preview, and it\nwants Anthropic authentication, an organisation setting on Team and Enterprise\nplans, and a `--dangerously-load-development-channels` launch until third-party\nchannels are allowlisted — so the watch is what works today, everywhere Claude\nCode runs.\n\nWaking a session that is sitting idle is the one thing that still needs the\nclient to offer a door, and the two that do offer one are answered differently.\nCodex has an app-server, so a session running under one is woken by joining its\nqueue — nothing to configure, since the address is on the app-server's own\ncommand line. Claude Code has no equivalent, so there the `watch` URL and a\n`Monitor` the session arms once do the same job. Other clients stay pull-based\nuntil each offers an opening of its own, and lose nothing they have today.\n\nNot planned, and deliberately so: delivery guarantees, message history, threads,\nreactions, and multi-host operation.\n\n</details>\n\n## More\n\n- [`docs/tools.md`](docs/tools.md) — the MCP tool reference, generated from the\n  live server, so it always matches what a client sees\n- [`docs/tui.md`](docs/tui.md) — the terminal client: its modal navigation, how\n  addressing works, and what is not built yet\n- [`docs/message-format.md`](docs/message-format.md) — the wire format: the\n  `{\"yaac\":2` magic, field order, addresses, bounces\n- [`docs/development.md`](docs/development.md) — running YAAC from a checkout,\n  debugging, an isolated development net\n\n## Licence\n\nMIT.\n",
  "bytes": 25516,
  "sha": "07fed04609c1bad84e28251f70f6cb00e7a0b4bd4809cbc0ff4c600f375ba665",
  "repo_slug": "amyodov/yet-another-agentic-chat",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_amyodov_yet_another_agentic_ch_897f1207/readme"
}