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.
- agentwires-0.2.0/.gitignore +8 -0
- agentwires-0.2.0/CHANGELOG.md +28 -0
- agentwires-0.2.0/PKG-INFO +638 -0
- agentwires-0.2.0/README.md +619 -0
- agentwires-0.2.0/agentwire/__init__.py +7 -0
- agentwires-0.2.0/agentwire/__main__.py +4 -0
- agentwires-0.2.0/agentwire/adapters/__init__.py +78 -0
- agentwires-0.2.0/agentwire/adapters/claude/__init__.py +0 -0
- agentwires-0.2.0/agentwire/adapters/claude/adapter.py +1048 -0
- agentwires-0.2.0/agentwire/adapters/claude/prompts/reading.md +20 -0
- agentwires-0.2.0/agentwire/adapters/cursor/__init__.py +0 -0
- agentwires-0.2.0/agentwire/adapters/cursor/adapter.py +613 -0
- agentwires-0.2.0/agentwire/adapters/cursor/prompts/reading.md +25 -0
- agentwires-0.2.0/agentwire/adapters/host/__init__.py +9 -0
- agentwires-0.2.0/agentwire/adapters/host/cursor.py +142 -0
- agentwires-0.2.0/agentwire/cli.py +1609 -0
- agentwires-0.2.0/agentwire/core/__init__.py +0 -0
- agentwires-0.2.0/agentwire/core/auth.py +167 -0
- agentwires-0.2.0/agentwire/core/channels.py +163 -0
- agentwires-0.2.0/agentwire/core/context.py +433 -0
- agentwires-0.2.0/agentwire/core/daemon.py +760 -0
- agentwires-0.2.0/agentwire/core/exec.py +15 -0
- agentwires-0.2.0/agentwire/core/gate.py +98 -0
- agentwires-0.2.0/agentwire/core/identity.py +42 -0
- agentwires-0.2.0/agentwire/core/journal.py +127 -0
- agentwires-0.2.0/agentwire/core/launchd.py +81 -0
- agentwires-0.2.0/agentwire/core/principal.py +140 -0
- agentwires-0.2.0/agentwire/core/prompts/__init__.py +115 -0
- agentwires-0.2.0/agentwire/core/prompts/brief/sections.md +18 -0
- agentwires-0.2.0/agentwire/core/prompts/session-delta.md +76 -0
- agentwires-0.2.0/agentwire/core/prompts/standing-block/README.md +13 -0
- agentwires-0.2.0/agentwire/core/prompts/standing-block/base.md +34 -0
- agentwires-0.2.0/agentwire/core/prompts/standing-block/dossier.md +12 -0
- agentwires-0.2.0/agentwire/core/prompts/standing-block/inbox.md +29 -0
- agentwires-0.2.0/agentwire/core/prompts/standing-block/peers.md +58 -0
- agentwires-0.2.0/agentwire/core/prompts/standing-block/topics.md +44 -0
- agentwires-0.2.0/agentwire/core/records.py +479 -0
- agentwires-0.2.0/agentwire/core/remote.py +131 -0
- agentwires-0.2.0/agentwire/core/route.py +569 -0
- agentwires-0.2.0/agentwire/core/scan.py +114 -0
- agentwires-0.2.0/agentwire/core/stats.py +161 -0
- agentwires-0.2.0/agentwire/core/store.py +917 -0
- agentwires-0.2.0/agentwire/core/summarizer.py +272 -0
- agentwires-0.2.0/agentwire/core/surface.py +324 -0
- agentwires-0.2.0/agentwire/core/sync.py +517 -0
- agentwires-0.2.0/agentwire/core/tokens.py +92 -0
- agentwires-0.2.0/agentwire/core/topics.py +683 -0
- agentwires-0.2.0/agentwire/core/ui.py +10 -0
- agentwires-0.2.0/agentwire/core/view.py +179 -0
- agentwires-0.2.0/agentwire/core/wake.py +218 -0
- agentwires-0.2.0/agentwire/core/words.py +53 -0
- agentwires-0.2.0/agentwire/extensions/opener/extension.js +207 -0
- agentwires-0.2.0/agentwire/extensions/opener/package.json +11 -0
- agentwires-0.2.0/agentwire/web/index.html +912 -0
- agentwires-0.2.0/agentwire/web/rooms.py +409 -0
- agentwires-0.2.0/agentwire/web/serve.py +496 -0
- agentwires-0.2.0/agentwire/web/server.py +890 -0
- agentwires-0.2.0/agentwire/web/threads.py +323 -0
- agentwires-0.2.0/bin/agentwire +16 -0
- agentwires-0.2.0/pyproject.toml +42 -0
|
@@ -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.
|