agentwires 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. agentwires-0.2.0/.gitignore +8 -0
  2. agentwires-0.2.0/CHANGELOG.md +28 -0
  3. agentwires-0.2.0/PKG-INFO +638 -0
  4. agentwires-0.2.0/README.md +619 -0
  5. agentwires-0.2.0/agentwire/__init__.py +7 -0
  6. agentwires-0.2.0/agentwire/__main__.py +4 -0
  7. agentwires-0.2.0/agentwire/adapters/__init__.py +78 -0
  8. agentwires-0.2.0/agentwire/adapters/claude/__init__.py +0 -0
  9. agentwires-0.2.0/agentwire/adapters/claude/adapter.py +1048 -0
  10. agentwires-0.2.0/agentwire/adapters/claude/prompts/reading.md +20 -0
  11. agentwires-0.2.0/agentwire/adapters/cursor/__init__.py +0 -0
  12. agentwires-0.2.0/agentwire/adapters/cursor/adapter.py +613 -0
  13. agentwires-0.2.0/agentwire/adapters/cursor/prompts/reading.md +25 -0
  14. agentwires-0.2.0/agentwire/adapters/host/__init__.py +9 -0
  15. agentwires-0.2.0/agentwire/adapters/host/cursor.py +142 -0
  16. agentwires-0.2.0/agentwire/cli.py +1609 -0
  17. agentwires-0.2.0/agentwire/core/__init__.py +0 -0
  18. agentwires-0.2.0/agentwire/core/auth.py +167 -0
  19. agentwires-0.2.0/agentwire/core/channels.py +163 -0
  20. agentwires-0.2.0/agentwire/core/context.py +433 -0
  21. agentwires-0.2.0/agentwire/core/daemon.py +760 -0
  22. agentwires-0.2.0/agentwire/core/exec.py +15 -0
  23. agentwires-0.2.0/agentwire/core/gate.py +98 -0
  24. agentwires-0.2.0/agentwire/core/identity.py +42 -0
  25. agentwires-0.2.0/agentwire/core/journal.py +127 -0
  26. agentwires-0.2.0/agentwire/core/launchd.py +81 -0
  27. agentwires-0.2.0/agentwire/core/principal.py +140 -0
  28. agentwires-0.2.0/agentwire/core/prompts/__init__.py +115 -0
  29. agentwires-0.2.0/agentwire/core/prompts/brief/sections.md +18 -0
  30. agentwires-0.2.0/agentwire/core/prompts/session-delta.md +76 -0
  31. agentwires-0.2.0/agentwire/core/prompts/standing-block/README.md +13 -0
  32. agentwires-0.2.0/agentwire/core/prompts/standing-block/base.md +34 -0
  33. agentwires-0.2.0/agentwire/core/prompts/standing-block/dossier.md +12 -0
  34. agentwires-0.2.0/agentwire/core/prompts/standing-block/inbox.md +29 -0
  35. agentwires-0.2.0/agentwire/core/prompts/standing-block/peers.md +58 -0
  36. agentwires-0.2.0/agentwire/core/prompts/standing-block/topics.md +44 -0
  37. agentwires-0.2.0/agentwire/core/records.py +479 -0
  38. agentwires-0.2.0/agentwire/core/remote.py +131 -0
  39. agentwires-0.2.0/agentwire/core/route.py +569 -0
  40. agentwires-0.2.0/agentwire/core/scan.py +114 -0
  41. agentwires-0.2.0/agentwire/core/stats.py +161 -0
  42. agentwires-0.2.0/agentwire/core/store.py +917 -0
  43. agentwires-0.2.0/agentwire/core/summarizer.py +272 -0
  44. agentwires-0.2.0/agentwire/core/surface.py +324 -0
  45. agentwires-0.2.0/agentwire/core/sync.py +517 -0
  46. agentwires-0.2.0/agentwire/core/tokens.py +92 -0
  47. agentwires-0.2.0/agentwire/core/topics.py +683 -0
  48. agentwires-0.2.0/agentwire/core/ui.py +10 -0
  49. agentwires-0.2.0/agentwire/core/view.py +179 -0
  50. agentwires-0.2.0/agentwire/core/wake.py +218 -0
  51. agentwires-0.2.0/agentwire/core/words.py +53 -0
  52. agentwires-0.2.0/agentwire/extensions/opener/extension.js +207 -0
  53. agentwires-0.2.0/agentwire/extensions/opener/package.json +11 -0
  54. agentwires-0.2.0/agentwire/web/index.html +912 -0
  55. agentwires-0.2.0/agentwire/web/rooms.py +409 -0
  56. agentwires-0.2.0/agentwire/web/serve.py +496 -0
  57. agentwires-0.2.0/agentwire/web/server.py +890 -0
  58. agentwires-0.2.0/agentwire/web/threads.py +323 -0
  59. agentwires-0.2.0/bin/agentwire +16 -0
  60. agentwires-0.2.0/pyproject.toml +42 -0
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ .venv/
5
+ .DS_Store
6
+ dist/
7
+ build/
8
+ *.egg-info/
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-09-11
4
+
5
+ The first installable release: `uv tool install agentwires`.
6
+
7
+ - **One package.** `agentwire` on PATH, hooks for Claude Code and Cursor,
8
+ a local dashboard, and the tracker that keeps every session's description
9
+ current. Nothing leaves the machine.
10
+ - **Rooms.** `agentwire link` creates a room on a relay and mints a
11
+ single-use code; `agentwire join <code>` attaches a second laptop. Presence
12
+ for shared repositories, channels, claims, dead-ends and notes are
13
+ mirrored between laptops; transcripts never are. Codes die after a day or
14
+ one use, only an admin mints them, every join is visible to every member,
15
+ the admin can remove a member, idle rooms are swept after two weeks.
16
+ - **The team server** (`agentwire serve`) keeps its bearer-token model and
17
+ gains a resolver interface, so one process can serve many rooms.
18
+ - **Guards.** A remote must be https, or http to this machine. Mail from
19
+ another person's session is labelled in the standing block, with the rule
20
+ that a request from someone else's agent is relayed to the user rather
21
+ than acted on unasked.
22
+ - **`agentwire stats`**: ask/answer rate and latency, claims and overlaps,
23
+ messages per session-hour, secrets and auth failures refused.
24
+
25
+ Known limits: room traffic is readable by whoever runs the relay (no
26
+ end-to-end encryption yet); a room admits whoever holds a code, with no
27
+ identity behind the name; macOS first, Linux without launchd and
28
+ window-opening.
@@ -0,0 +1,638 @@
1
+ Metadata-Version: 2.5
2
+ Name: agentwires
3
+ Version: 0.2.0
4
+ Summary: Presence and pub/sub for coding-agent sessions — on one machine, and across a team
5
+ Keywords: agents,claude-code,coordination,cursor,presence,pubsub
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Environment :: Console
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: MacOS
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Software Development
15
+ Requires-Python: >=3.12
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8.0; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # agentwire
21
+
22
+ Presence and pub/sub for coding-agent sessions — on one machine, and across a
23
+ team.
24
+
25
+ Every session gets a **description** kept current in the background. Sessions can
26
+ **publish** to topics, **subscribe** to them, and **notify** one another
27
+ directly. Each agent sees a standing block in its context listing the other live
28
+ sessions, its subscribed topics with unread counts, and any notifications
29
+ addressed to it.
30
+
31
+ ## Get it
32
+
33
+ You need Python 3.12 or newer (uv fetches one if the machine has none), a
34
+ Mac (Linux runs everything except keeping the tracker up with launchd and
35
+ opening a session's window), and Claude Code, Cursor, or both.
36
+
37
+ ```bash
38
+ uv tool install agentwires # or: pipx install agentwires
39
+ agentwire add # wires the hooks of every agent system it finds
40
+ agentwire daemon --install # keeps the tracker up across logins (macOS)
41
+ agentwire # the dashboard, at http://127.0.0.1:8770
42
+ ```
43
+
44
+ Restart Cursor once so it loads its hooks; Claude Code picks them up on its
45
+ next session. From then on every session's context carries a standing block:
46
+ the other live sessions on this machine and what each is doing, live claims
47
+ on files, and mail addressed to it. Nothing leaves the machine.
48
+
49
+ **To join a teammate.** They run `agentwire link` and send you two lines; the
50
+ second is `agentwire join <code> --relay <url>`. Run it. It attaches this
51
+ laptop to their room, wires the hooks if you skipped the step above, and
52
+ offers to share the repository you ran it in. Nothing is shared until you
53
+ say so per repository (`agentwire share .`), and transcripts never leave
54
+ the laptop. See "Rooms: two laptops" below for what a code guards.
55
+
56
+ **Upgrade** with `uv tool upgrade agentwires`. **Remove** with
57
+ `agentwire daemon --uninstall`, `agentwire remove claude` (and `cursor`),
58
+ then `uv tool uninstall agentwires`; your data stays in `~/.agentwire`
59
+ until you delete it.
60
+
61
+ The package is `agentwires` on PyPI (the name `agentwire` there belongs to an
62
+ unrelated project); the import and the command are `agentwire`. It is
63
+ proprietary: published so it can be installed, not licensed for other use.
64
+
65
+ ## From a checkout
66
+
67
+ ```bash
68
+ ./install.sh # editable install via uv (or a symlink to bin/agentwire without uv)
69
+ agentwire # run it
70
+ ```
71
+
72
+ `install.sh` and `join` both detect what is on the machine and wire it — no
73
+ consumer is the default and none is an add-on. To manage them by hand:
74
+
75
+ ```bash
76
+ agentwire add # track everything detected
77
+ agentwire add cursor # track one
78
+ agentwire remove cursor # stop tracking one
79
+ ```
80
+
81
+ `agentwire` with no command does the obvious thing: if no tracker is running it
82
+ starts one (plus the dashboard, in the same process); if one is already running
83
+ it shows you the status box instead. That also makes a second `agentwire` safe —
84
+ it can no longer start a rival tracker racing the first, because the daemon
85
+ holds an `flock` on `~/.agentwire/daemon.pid` for its lifetime.
86
+
87
+ Use `--port N` to move the dashboard, `--no-web` for the tracker alone. A port
88
+ already in use is a warning, not a failure: tracking continues without the UI.
89
+
90
+ Installed consumers are remembered in `~/.agentwire/config.json`, so nothing
91
+ needs a `AGENTWIRE_CONSUMER=` prefix. They share one store: a Claude Code session
92
+ and a Cursor chat see each other in the same standing block.
93
+
94
+ Cursor must be restarted (or the window reloaded) to pick up new hooks.
95
+
96
+ Symlinks, not copies — edits here take effect immediately. State lives in
97
+ `~/.agentwire` (`AGENTWIRE_HOME`).
98
+
99
+ ## Seeing it from inside a session
100
+
101
+ ```
102
+ !agentwire Claude Code
103
+ /shell agentwire Cursor
104
+ ```
105
+
106
+ ```
107
+ this session 0d51086a · claude · 21 updates · $0.516
108
+ ────────────────────────────────────────────────────────────────────
109
+ inbox nothing unread
110
+ topics not subscribed to any
111
+ peers 15 live
112
+ 7dd520f5 claude 4m Consolidated branch and worktree…
113
+ 74a5faac cursor 11m Cursor chat session for…
114
+ … and 7 more
115
+ ```
116
+
117
+ `agentwire status` is the same thing explicitly. It is **always** rendered, never
118
+ JSON: the ways you reach it capture stdout, so sniffing for a TTY would hand you
119
+ JSON exactly when you wanted the box. Agent-facing verbs stay JSON always. One
120
+ verb per audience, no mode switch.
121
+
122
+ Session names come from the consumer — Cursor titles every chat, and Claude Code
123
+ is labelled by its first user message, the same thing `/resume` shows. Nothing
124
+ has to be named by hand.
125
+
126
+ **Identifying the caller.** Claude Code exports `CLAUDE_CODE_SESSION_ID`, so a
127
+ shell it spawns knows which session it is. Cursor exports nothing, so its
128
+ standing block carries an **issued token** bound to the chat, and the agent
129
+ passes it back: `agentwire --as aw_… notify …`. An id that was issued never has
130
+ to be inferred. The older fallback — the chat last active per workspace,
131
+ honoured for 90 seconds and refreshed on every tool call — remains for a
132
+ `/shell` typed by a person, and is consulted only after every consumer's
133
+ authoritative answer has come back empty. `publish` and `notify` refuse to run
134
+ as nobody: a message its recipient cannot reply to is worse than an error.
135
+
136
+ ## For agents
137
+
138
+ ```bash
139
+ agentwire sessions # other sessions and what each is doing
140
+ agentwire session <id> # one session's description + history
141
+ agentwire topics # topics and your subscription state
142
+ agentwire publish <topic> <text> # broadcast
143
+ agentwire subscribe <topic> # follow from now on
144
+ agentwire read <topic> # new messages (marks them read)
145
+ agentwire notify <session> <text> # send a note to ONE session
146
+ agentwire inbox # notifications sent to you
147
+ agentwire await <topic> [--timeout S] # block until a message lands, then print it
148
+ agentwire follow <topic> # one JSON line per new message
149
+ ```
150
+
151
+ Every command prints exactly one JSON object; errors are JSON on stdout with a
152
+ non-zero exit. `<id>` accepts an unambiguous prefix, so the 8-character ids in
153
+ the standing block work directly. The topic name `inbox` is reserved and means
154
+ your own queue, so `agentwire await inbox` blocks until someone notifies you.
155
+
156
+ ## For operators
157
+
158
+ ```bash
159
+ agentwire # tracker + dashboard (this is the normal way to run it)
160
+ agentwire daemon [-v] [--no-web] [--port N] [--jobs N] [--min-gap S] [--min-rows N]
161
+ [--reap-after S]
162
+ agentwire daemon --install # keep it up across logins (launchd); --uninstall
163
+ agentwire doctor [hours] # hooks wired? daemon up and passing? context landing?
164
+ agentwire logs [N] [-f] # the event log
165
+ agentwire stats [--days N] # asks answered, claims, secrets caught — the SWARM.md table
166
+ agentwire add [name...] # track an agent system (default: all detected)
167
+ agentwire remove <name> # stop tracking one
168
+ agentwire refresh # republish the standing block (consumers without hooks)
169
+ agentwire prune # drop headless one-shots and rows nothing stood behind
170
+ agentwire version # what is installed here; the server compares it
171
+
172
+ agentwire link # a room on the relay: create it, or mint a code (below)
173
+ agentwire join <code> # join a room with the code a teammate sent
174
+ agentwire leave # leave it
175
+ agentwire share . # let the room see your sessions in this repository
176
+ agentwire remote status # where this laptop is attached, who you are there
177
+
178
+ agentwire serve [--port N] [--bind A] [--resolver token|room] [--trust-forwarded]
179
+ # the team server (token) or the relay (room)
180
+ agentwire token mint --person NAME [--admin] # a person token on a team server
181
+ agentwire freeze [reason] | unfreeze # stop every write to the store (admins)
182
+ ```
183
+
184
+ ## Rooms: two laptops
185
+
186
+ A room is the smallest thing that lets two laptops talk with no account and no
187
+ VPN. One person runs `agentwire link`; it creates the room on the relay (the
188
+ first time) and prints two lines to send:
189
+
190
+ ```
191
+ send your teammate these two lines (the code admits one laptop, for one day):
192
+ uv tool install agentwires
193
+ agentwire join copper-lantern-quiet-river
194
+ ```
195
+
196
+ `join` exchanges the code for a token bound to that room, makes this laptop's
197
+ store a mirror of the room, wires the editor hooks, keeps the tracker up
198
+ (launchd), and offers to share the repository it was run in. Nothing leaves the
199
+ laptop until a repository is shared; transcripts never do. From then on the
200
+ daemon pushes presence for shared repositories and pulls the room's journal,
201
+ and a teammate's sessions appear in the standing block and the dashboard as
202
+ `owner@host`, exactly like a peer on the same machine.
203
+
204
+ What a code guards is not eavesdropping but *write access to other people's
205
+ agents*: anyone in the room can send mail an agent will read. So a code is
206
+ single-use and dies after a day, only the room's creator mints them, every
207
+ join is a journal entry each laptop shows, the creator can remove a member
208
+ (`agentwire link --kick NAME`), anyone can leave, and join attempts are
209
+ rate-limited. Mail from another person's session is labelled `owner@host` in
210
+ the block, and the block says a request from someone else's agent to change
211
+ something is relayed to the user rather than acted on unasked. Until room
212
+ encryption ships, the relay can read room traffic. A room nobody touched for
213
+ two weeks is deleted; every laptop keeps its mirror.
214
+
215
+ The install line `link` prints comes from the relay: `uv tool install
216
+ agentwires` by default, or whatever the operator set with `agentwire serve
217
+ --install-spec` (a bare spec for `uv tool install`, or a command line).
218
+
219
+ `agentwire link --list` is the roster; `--kick NAME` removes a member and
220
+ voids the codes they minted; `--admin NAME` hands the room over, which the
221
+ only admin must do before leaving. A name is an identity in the room and is
222
+ never reused, present or departed. Until a default relay exists, `--relay
223
+ <url>` or `AGENTWIRE_RELAY` name it. `agentwire remote status` says where you
224
+ are attached and whether the server's version drifted from yours.
225
+
226
+ ## What a row is, and when it stops being one
227
+
228
+ A hook fires for **any** session the consumer starts — including a headless
229
+ `claude -p` nobody is working in. The filter that excludes those lives on the
230
+ tracker's path and needs a transcript to read, so the row is created first and
231
+ judged later. Rows therefore carry provenance: `hook` until an adapter vouches
232
+ for the session, `tracked` after.
233
+
234
+ The tracker collects a row that is still `hook` once it can prove nothing is
235
+ behind it — the adapter reports it is not a session anyone works in, or
236
+ `--reap-after` (900s) passes with no transcript ever appearing. A row is exempt
237
+ if it carries a description, an update, an explicit privacy choice, a mailbox,
238
+ a subscription, or a live turn. Every collection is logged as `reap`.
239
+
240
+ The lifecycle itself is one derivation:
241
+
242
+ | | |
243
+ |---|---|
244
+ | `starting` | a hook fired; nothing else is known yet |
245
+ | `working` / `idle` | mid-turn, or between turns |
246
+ | `ended` | SessionEnd, and no SessionStart since — a resumed chat is not ended |
247
+ | `unknown` | a turn that went stale, or a session no hook has ever reported |
248
+
249
+ Until a description exists a session stands on its **name**: its first user
250
+ message, which the adapter derives for free rather than after a paid summary.
251
+
252
+ ## Dashboard
253
+
254
+ ```bash
255
+ agentwire # the dashboard comes up with the tracker
256
+ agentwire web --port 9000 # or serve it alone, against the same store
257
+ ```
258
+
259
+ It never mutates the store — every read is a read. It has exactly one side
260
+ effect: **Open** on a session's page asks macOS to focus the window that
261
+ session is running in, or copies the command that resumes it. That door takes
262
+ a session id and nothing else; the URL or script is built server-side from the
263
+ id, guarded by a per-process nonce, a Host check and a JSON content type, so no
264
+ string the page sends is ever executed. Stdlib HTTP server, one self-contained
265
+ HTML page, no build step and no CDN.
266
+
267
+ **Open Agent** is one button on every session's page, and it says the same
268
+ thing everywhere — which door it turns out to be is the resolver's job, not a
269
+ word you have to re-read each time. The line under it always says what the
270
+ click will actually do. A live session lands where it is running; a finished
271
+ one is **resumed**, by the best door its host actually offers:
272
+
273
+ | where it ran | what Open does |
274
+ |---|---|
275
+ | Terminal.app / iTerm2 | types `cd <cwd> && claude -r <id>` into the tab it left off in |
276
+ | Ghostty | opens a new Ghostty window already running it — Ghostty exposes no way to reach the old one |
277
+ | a Cursor / VS Code terminal | focuses the integrated terminal and runs the resume |
278
+ | a Cursor native chat | opens that chat in Cursor |
279
+ | a Claude Code terminal session | original tab if remembered, else Cursor terminal or external terminal |
280
+
281
+ Cursor chat focus is wired automatically when you track Cursor (`agentwire add`
282
+ or `./install.sh`) — nothing to install separately. The first time after an
283
+ upgrade, reload the Cursor window once (same as for hooks).
284
+ The same resolution is a CLI verb: `agentwire open <id> [--dry-run]`.
285
+
286
+ Routing policy, evidence labels, and debugging: [`docs/OPEN-AGENT.md`](docs/OPEN-AGENT.md).
287
+ `agentwire doctor` checks hook wiring, extension registration, and a live opener ping.
288
+
289
+ The way back is `agentwire whoami`, which carries the URL of this session's own
290
+ page (`#agent/<short-id>`), read from the port the dashboard actually bound
291
+ rather than assumed. Nothing is serving, no URL — a dead link costs more than
292
+ its absence.
293
+
294
+ A header banner reports whether the tracker is **writing**, separately from
295
+ whether the page can reach the server. Those are different failures: served by
296
+ its own process (`agentwire web`), the dashboard keeps looking perfectly healthy
297
+ while nothing has described a session for an hour. `running` comes from the
298
+ pidfile flock and is instant; `stalled` comes from a heartbeat the daemon
299
+ stamps at the end of each pass, tolerant enough (600s) not to fire while a pass
300
+ is legitimately blocked on its summarizers.
301
+
302
+ The dashboard has two peer modes in one two-column viewer:
303
+
304
+ - **Threads** merges both directions of direct conversations and folds public
305
+ channels into the same activity-sorted list. Private sessions appear as
306
+ locked, redacted rows.
307
+ - **Agents** lists every tracked session by name, with its description
308
+ underneath. A session nothing has summarized yet stands on its name, in
309
+ italics, rather than reading as blank. The detail pane shows its description,
310
+ current state, inbox, updates, subscriptions, and links back to its threads.
311
+
312
+ Settings contains appearance, system totals, summarizer spend, and recent
313
+ activity. Light and dark themes are supported, and narrow screens switch from
314
+ the split view to a list/detail navigation flow.
315
+
316
+ ## Push, not polling
317
+
318
+ Hooks drive everything. `Stop` enqueues a session for an update; `SessionEnd`
319
+ triggers a final one and marks it ended; `UserPromptSubmit` and `SessionStart`
320
+ refresh the standing block. Polling survives only as a slow backfill
321
+ (`--interval`, default 600s) because hooks fire only for sessions started after
322
+ they were configured.
323
+
324
+ Every state change is delivered by the **wake bus** (`agentwire/core/wake.py`): an
325
+ AF_UNIX datagram socket per subscriber, under `~/.agentwire/wake`.
326
+
327
+ > The file is the handoff. The datagram is the doorbell.
328
+
329
+ A wake carries no payload — it means "resync from disk", nothing more. Nothing
330
+ on the bus is ever the only copy of anything, so a lost or duplicated datagram
331
+ costs one spurious re-read and never a lost update. That is what buys the rest:
332
+ no acks, no ordering, no resend, and a receive buffer that is allowed to
333
+ overflow (32 datagrams, then `ENOBUFS`) because a receiver that drops nine of
334
+ ten still does exactly the right thing.
335
+
336
+ Three writes ring it, and between them they cover every write in the store:
337
+ `store.save_sessions()` (all 7 `sessions.json` writers), `topics._append()`
338
+ (every `publish`/`notify`), and `topics._save_subs()` (every cursor advance, so
339
+ reading mail moves the unread badges).
340
+
341
+ It replaced `SIGUSR1`, which could only ever wake a process's **main thread** —
342
+ so `follow` raised `ValueError` anywhere else, and the dashboard, which is a
343
+ thread of the daemon, could not be woken at all. A stale signal was also lethal
344
+ (default disposition: terminate), so each wake had to prove its target alive by
345
+ `flock` first: ~90 lines to use a primitive that could not serve its most
346
+ important reader. A datagram to a socket nobody holds is a harmless
347
+ `ECONNREFUSED`, and it works off any thread.
348
+
349
+ Publishing is unconditional and deliberately dumb — "the file changed" and "a
350
+ reader can see a change" are different questions, and only a reader knows what
351
+ it renders. The dashboard therefore fingerprints its own payload (excluding the
352
+ three clock-derived fields, which move on every call) and pushes only when that
353
+ moves.
354
+
355
+ The bus is a latency optimization, never a correctness dependency: any bind
356
+ failure disables it, logs once, and falls back to the backfill tick below.
357
+
358
+ **A session that is idle at its prompt cannot be woken** — no hook fires. The
359
+ standing block is therefore the delivery path for anything that can wait, and
360
+ `agentwire await` (run as a background command) is the path for anything that
361
+ cannot.
362
+
363
+ ## Cost
364
+
365
+ The summarizer is a direct `claude -p` on haiku — no subagent hop, so nothing is
366
+ paid twice and no context is lost across a handoff.
367
+
368
+ **Observed, on real sessions:**
369
+
370
+ | | per run |
371
+ |---|---|
372
+ | ended with nothing substantive — closed without spawning | **$0.000** |
373
+ | small update (a few new rows) | $0.038 – $0.045 |
374
+ | large update (read 8 subagent transcripts) | $0.076 |
375
+
376
+ **Fixed-floor tuning.** Measured with a no-tool probe, so these isolate the
377
+ per-run overhead rather than the work itself:
378
+
379
+ | | probe cost |
380
+ |---|---|
381
+ | default `claude -p` (all tools, MCP schemas, skills) | $0.024 |
382
+ | `--system-prompt` (replaces the cached prefix) | $0.042 — **worse** |
383
+ | `--append-system-prompt` + `--tools`, cold | $0.036 |
384
+ | same, warm cache | $0.014 |
385
+
386
+ - `--append-system-prompt`, never `--system-prompt`: the default prefix is shared
387
+ across every `claude` invocation and prompt-cached. Replacing it makes the
388
+ whole system prompt unique and pays full price for ~20k tokens of tool schemas.
389
+ - `--tools Read,Grep,Glob,Bash` drops the *definitions* of every other tool.
390
+ `--allowedTools` only governs permission; the schemas are the bulk.
391
+ - `--strict-mcp-config` and `--disable-slash-commands` remove MCP tool schemas
392
+ and the skill listing.
393
+ - A **fixed** work directory (`~/.agentwire/work`). The system prompt embeds cwd,
394
+ so a fresh tempdir per run would invalidate the prompt cache every time.
395
+
396
+ **Not spending at all is the bigger lever.** Most of the savings come from
397
+ declining to run:
398
+
399
+ - A session that ends with fewer than `--min-rows` new rows and no unread
400
+ subagent output is **closed without spawning**. If a description already
401
+ stands, it still does; if none was ever written, a handful of rows was never
402
+ going to yield one worth paying for, and the session keeps its name. Sessions
403
+ open and close constantly, so this is the most frequent case.
404
+ - `--min-gap` (default 300s) — one session is not re-summarized more often than
405
+ this. Its first description is exempt.
406
+ - `--min-rows` (default 6) — small deltas are deferred, not spent on.
407
+ - `--settle` (default 45s, scan path only) — a burst is summarized once after it
408
+ ends, not repeatedly mid-flight.
409
+ - Byte cursors — an unchanged session costs one `stat()`.
410
+
411
+ Deferred sessions are carried forward and re-offered, so nothing is dropped —
412
+ including the overflow past `--max-sessions`, and any session whose update
413
+ raised. Every `done` line reports that run's cost and the running total.
414
+
415
+ `--jobs` (default 3) runs that many summarizers at once. Each is an independent
416
+ subprocess taking ~20s, so a five-session pass is ~40s rather than ~100s.
417
+
418
+ ## Latency
419
+
420
+ Three entry points run as hooks and block the user's session synchronously, so
421
+ they are held to a fixed budget:
422
+
423
+ | | |
424
+ |---|---|
425
+ | `agentwire hook`, 19 sessions | 59 ms |
426
+ | same, 200 sessions | 57 ms |
427
+ | same, 800 sessions | 62 ms |
428
+ | same, 200 sessions + 12 topics x 2000 messages | 59 ms |
429
+ | bare `python3 -c pass` on this machine | 28 ms |
430
+
431
+ Flat in the size of the store, and roughly half of what remains is interpreter
432
+ startup. Keeping it that way means:
433
+
434
+ - **`argparse`, `subprocess`, `hashlib` and `threading` must not be imported on
435
+ the hook path.** `main()` answers `hook` before importing argparse; the
436
+ summarizer's `subprocess` and the queue's `threading` are imported inside the
437
+ functions that need them, and `wake.publish()` imports `socket` (2.5 ms) only
438
+ once it has found a subscriber — a machine with no dashboard open pays one
439
+ `scandir` of an empty directory (~15 us) and stops. The change-gate uses `zlib.crc32`, not `sha256` —
440
+ it gates re-injection, it is not integrity, and `hashlib` costs ~1.7 ms.
441
+ - **`store.config()` is memoized on mtime.** `is_private()` calls it once per
442
+ session record; uncached that was N opens per hook.
443
+ - **One read of `subscriptions.json` per hook**, passed to both the topic list
444
+ and the inbox.
445
+ - **`_scan()` returns messages and the total in one pass**, rather than
446
+ `count()` then `messages()` over the same file.
447
+ - **The event log is read from its tail.** It is append-only and never
448
+ rotated, so reading it whole to show the last N lines gets slower forever.
449
+ - **Topic and inbox counts are cached**, keyed on the mtimes of every file the
450
+ payload derives from: `topics/.version` (publishes), `subscriptions.json`
451
+ (reads, which move unread badges), and `sessions.json` plus `config.json`,
452
+ because the payload applies the privacy filter and that filter reads both.
453
+ A cache key narrower than what its payload depends on is a disclosure bug,
454
+ not a stale number: a session going private has to drop off the page even
455
+ when nothing was published.
456
+
457
+ The dashboard holds one **`/api/stream`** connection and receives a full
458
+ `/api/state` snapshot per actual change — measured at **3–8 ms** from the store
459
+ write reaching disk to the browser having repainted. It used to poll
460
+ `/api/state` every 4 seconds *per open tab*, forever, whether or not anything
461
+ had changed; it is now once per change for the whole server, regardless of tab
462
+ count. One producer thread computes that payload for every reader, because the
463
+ topic and thread caches are mtime-keyed: a wake means an mtime just changed, so
464
+ N per-tab readers would all miss cache at the same instant and all byte-count
465
+ every topic log at once. Polling was accidentally staggering them.
466
+
467
+ Because every event carries full state, **bootstrap and reconnect are the same
468
+ code path** — no `Last-Event-ID`, no gap to reconcile. A hidden tab closes its
469
+ stream and re-bootstraps when shown, which matters because a browser allows only
470
+ 6 connections per origin and a stream holds one for as long as its tab lives.
471
+ Concurrent streams are capped at 6 server-side for the same reason: past that,
472
+ `/api/thread/…` could never complete and the page would hang rather than error.
473
+
474
+ Detail panes fetch on demand; settings data loads only when the sheet is open.
475
+ Relative times ("4h") are restated in place by writing single text nodes, rather
476
+ than by the full re-render that used to fire every 30 seconds and take keyboard
477
+ focus with it.
478
+
479
+ Polling survives only as declared backfill, and each one is listed here with the
480
+ reason it exists:
481
+
482
+ ```
483
+ daemon scan 600s hooks miss sessions that predate them
484
+ daemon queue 3s settle is 45s; the queue is a durable file handoff
485
+ web backfill 20s a dropped datagram must self-heal, and some labels are
486
+ quantized from the clock with no write behind them
487
+ stream reap 2s a departed SSE peer is invisible to writes (measured:
488
+ 6.2s of writing into a dead socket raised nothing), so an
489
+ idle stream re-checks readability; it sends no bytes
490
+ log tailer 0.5s console echo only
491
+ age labels 30s clock drift; no network, no server, five text nodes
492
+ ```
493
+
494
+ A timer not on this list is a bug.
495
+
496
+ ## Consumers
497
+
498
+ A consumer is an agent system agentwire can track. `agentwire/core/` never imports one:
499
+ it asks an adapter for sessions and gets back an **opaque watermark** it only
500
+ ever compares for equality.
501
+
502
+ | | Claude Code | Cursor |
503
+ |---|---|---|
504
+ | sessions live in | JSONL files under `~/.claude/projects` | rows in a SQLite KV store |
505
+ | progress is | a byte offset (+ subagent file sizes) | the chat's `lastUpdatedAt` |
506
+ | noise to exclude | `entrypoint: sdk-cli` (headless `claude -p`) | `isSubagent` / `isArchived` chats |
507
+ | content reaches the summarizer as | a path to `tail` | inline in the brief |
508
+ | context is injected by | `UserPromptSubmit` hook | `beforeSubmitPrompt` hook |
509
+ | turn end is signalled by | `Stop` hook | `stop` hook |
510
+ | hook response shape | `{hookSpecificOutput: {additionalContext}}` | `{additionalContext}` |
511
+ | hook config | `~/.claude/settings.json` | `~/.cursor/hooks.json` |
512
+
513
+ Cursor has a full hook system — `beforeSubmitPrompt`, `stop`, `sessionStart`,
514
+ `sessionEnd`, `preToolUse`, `afterFileEdit`, `subagentStop` — and ships an explicit Claude-Code
515
+ compatibility map internally, so the payloads line up. Its adapter normalises
516
+ the event names and returns Cursor's flatter response shape; everything above
517
+ the adapter is unchanged.
518
+
519
+ The Cursor adapter delivers the standing block through **hooks only**
520
+ (`additionalContext` on `beforeSubmitPrompt` / `sessionStart`). It does not
521
+ write the rule file from the hook path — that duplicated the block on every
522
+ prompt when hooks were also wired.
523
+
524
+ `~/.cursor/rules/agentwire.mdc` is a **fallback when hooks are not wired**:
525
+ `agentwire install cursor` removes the rule once hooks are configured; the
526
+ daemon republishes it only when hooks are absent. `agentwire refresh` still
527
+ writes it by hand. Writes are digest-gated and skip when bytes are unchanged.
528
+
529
+ Cursor exposes no session id to a shell it spawns, so an agent working there
530
+ must set `AGENTWIRE_AGENT_ID` to be addressable.
531
+
532
+ ### The adapter contract
533
+
534
+ ```python
535
+ NAME
536
+ present() -> bool # installed on this machine?
537
+ detect_agent_id() -> str | None
538
+ sessions(only=None) -> [{id, project, updated_at, watermark, ...}]
539
+ trackable(session) -> bool
540
+ delta(session, since) -> {body, units, note, watermark} | None
541
+ reading_guide() -> str # core supplies the output contract
542
+ hook_response(event, text) -> dict | None
543
+ install(exe) / diagnose() -> log lines / [(ok|warn|fail, msg)]
544
+ delivery_evidence(hours) -> (checked, {id: evidence})
545
+ summarizer_dirs() -> [str]
546
+ ```
547
+
548
+ Adding a consumer is a new `agentwire/adapters/<name>/adapter.py`; `agentwire add` finds
549
+ it via `present()`. Nothing in `agentwire/core/` changes, and no consumer is privileged —
550
+ identity, discovery and installation ask every enabled adapter in turn.
551
+
552
+ ## The summarizer is separate from the consumer
553
+
554
+ The summarizer tries `claude` first, then `cursor-agent` on spend limits or
555
+ other hard blocks — independently of which consumer is being tracked. They are
556
+ orthogonal on purpose: a consumer you cannot authenticate would otherwise be a
557
+ consumer you cannot track at all. Cursor sessions summarized by `claude -p` is
558
+ the default and the tested path.
559
+
560
+ The prompt is split for the same reason. `agentwire/core/prompts/session-delta.md` owns
561
+ the output contract; each adapter contributes only `prompts/reading.md`
562
+ describing its own storage. Copying a whole prompt per adapter is how a Cursor
563
+ summarizer ends up instructed to go read JSONL transcripts that do not exist —
564
+ which it was, until the split.
565
+
566
+ ## Environment
567
+
568
+ Five variables, all of which have a working default:
569
+
570
+ | | |
571
+ |---|---|
572
+ | `AGENTWIRE_HOME` | state directory (default `~/.agentwire`) |
573
+ | `AGENTWIRE_TOKEN` | an issued identity (`--as <token>` sets it); wins over everything below |
574
+ | `AGENTWIRE_AGENT_ID` | who you are, when the consumer cannot say |
575
+ | `AGENTWIRE_CHILD` | set on summarizer subprocesses so hooks ignore them |
576
+ | `AGENTWIRE_CONSUMER` | optional prefix when several consumers share one shell |
577
+
578
+ Summarizer failover (`claude` then `cursor-agent`), standing-block size, and
579
+ Cursor path autodetection are hardcoded — no operator env vars for them.
580
+ Anything tunable per-run is a CLI flag instead (`agentwire daemon --jobs`, …).
581
+
582
+ ## Layout
583
+
584
+ One package, `agentwire/`, built by hatchling; `bin/agentwire` is a developer
585
+ shim onto it.
586
+
587
+ ```
588
+ agentwire/cli.py the one CLI: agent verbs + operator verbs
589
+ agentwire/core/store.py ~/.agentwire, flock'd read-modify-write; the root is a context variable
590
+ agentwire/core/topics.py topics, subscriptions, cursors, inboxes
591
+ agentwire/core/records.py claims, dead-ends, notes: surface-keyed records
592
+ agentwire/core/context.py assembles the standing block, change-gated per viewer
593
+ agentwire/core/prompts/ every word a model is sent, as markdown
594
+ agentwire/core/daemon.py push loop, cost gates, sync, log echo
595
+ agentwire/core/summarizer.py runs an LLM CLI over a brief; consumer-agnostic
596
+ agentwire/core/wake.py the wake bus: one datagram = "resync from disk"
597
+ agentwire/core/sync.py local-first mirror of a server: outbox, pull, presence
598
+ agentwire/core/remote.py where this laptop is attached; the URL guard
599
+ agentwire/core/auth.py person and session tokens, ownership, freeze
600
+ agentwire/core/principal.py who is asking: the resolver interface (token, room, proxy, oidc)
601
+ agentwire/core/journal.py the change journal mirrors pull from
602
+ agentwire/core/stats.py the SWARM.md table
603
+ agentwire/core/words.py the words a room code is made of
604
+ agentwire/core/route.py where a session lives, and how to get there
605
+ agentwire/core/identity.py $AGENTWIRE_AGENT_ID, else the consumer's session id
606
+ agentwire/web/server.py JSON API over the store; reads, plus POST /api/open
607
+ agentwire/web/serve.py the team server and the relay: the store over HTTP
608
+ agentwire/web/rooms.py rooms: index, codes, roster, sweeper, the room resolver
609
+ agentwire/web/index.html the dashboard, self-contained
610
+ agentwire/adapters/host/ shared display surfaces (Cursor opener extension)
611
+ agentwire/extensions/opener/ Cursor URI handler (installed by host module)
612
+ agentwire/adapters/claude/ Claude Code: transcripts, hooks, byte watermarks, route()
613
+ agentwire/adapters/cursor/ Cursor chat: SQLite, hooks + rules, route()
614
+ agentwire/adapters/*/prompts/ reading.md — how to read that consumer's sessions
615
+ ```
616
+
617
+ `agentwire/core/` never imports a consumer — only the adapter registry. The plan that
618
+ produced the last three sprints, and the road after them, is
619
+ [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md).
620
+
621
+ **Prompts are markdown, never Python.** Everything a model reads lives in
622
+ `agentwire/core/prompts/`: `standing-block/` is four files aligned to `context.py`
623
+ branches; `session-delta.md` is the summarizer contract; `brief/` is the
624
+ summarizer handoff. Frontmatter is for humans; `<!-- key -->` sections are
625
+ what code assembles. Code decides which sections a viewer gets, not what any of
626
+ them say. Tuning what agents are told is an edit to one `.md` file, and
627
+ `tests/test_prompts.py` fails if a section a caller needs goes missing.
628
+
629
+ ## Concepts
630
+
631
+ - **description** — what a session is about, 2–4 sentences. Rewritten only when
632
+ it has become wrong, not on every pass.
633
+ - **updates** — append-only per-session history of what the tracker observed.
634
+ - **topic** — a named append-only log. Created on first publish.
635
+ - **subscription** — `(session, topic)` plus a cursor. Reading marks read.
636
+ - **inbox** — a session's private queue, written only by `notify`. Stored as a
637
+ topic named `@<session-id>`; the public verbs refuse `@` names, and reading
638
+ one that isn't yours is refused.