tabc 0.2.0__tar.gz → 0.2.1__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 (40) hide show
  1. {tabc-0.2.0 → tabc-0.2.1}/CHANGELOG.md +49 -1
  2. {tabc-0.2.0 → tabc-0.2.1}/GUIDE.md +45 -6
  3. {tabc-0.2.0 → tabc-0.2.1}/MCP.md +6 -0
  4. {tabc-0.2.0/tabc.egg-info → tabc-0.2.1}/PKG-INFO +32 -4
  5. {tabc-0.2.0 → tabc-0.2.1}/PYPI.md +31 -3
  6. {tabc-0.2.0 → tabc-0.2.1}/pyproject.toml +1 -1
  7. {tabc-0.2.0 → tabc-0.2.1/tabc.egg-info}/PKG-INFO +32 -4
  8. {tabc-0.2.0 → tabc-0.2.1}/tabc.egg-info/SOURCES.txt +1 -0
  9. {tabc-0.2.0 → tabc-0.2.1}/tabus/bus.py +1 -1
  10. {tabc-0.2.0 → tabc-0.2.1}/tabus/cli.py +26 -1
  11. tabc-0.2.1/tabus/codex_queue.py +194 -0
  12. {tabc-0.2.0 → tabc-0.2.1}/tabus/doorbell.py +7 -2
  13. {tabc-0.2.0 → tabc-0.2.1}/tabus/doorbell_poll.py +1 -1
  14. {tabc-0.2.0 → tabc-0.2.1}/LICENSE +0 -0
  15. {tabc-0.2.0 → tabc-0.2.1}/MANIFEST.in +0 -0
  16. {tabc-0.2.0 → tabc-0.2.1}/setup.cfg +0 -0
  17. {tabc-0.2.0 → tabc-0.2.1}/tabc.egg-info/dependency_links.txt +0 -0
  18. {tabc-0.2.0 → tabc-0.2.1}/tabc.egg-info/entry_points.txt +0 -0
  19. {tabc-0.2.0 → tabc-0.2.1}/tabc.egg-info/requires.txt +0 -0
  20. {tabc-0.2.0 → tabc-0.2.1}/tabc.egg-info/top_level.txt +0 -0
  21. {tabc-0.2.0 → tabc-0.2.1}/tabus/__init__.py +0 -0
  22. {tabc-0.2.0 → tabc-0.2.1}/tabus/admin.py +0 -0
  23. {tabc-0.2.0 → tabc-0.2.1}/tabus/compat_bcn.py +0 -0
  24. {tabc-0.2.0 → tabc-0.2.1}/tabus/compat_busd.py +0 -0
  25. {tabc-0.2.0 → tabc-0.2.1}/tabus/daemon.py +0 -0
  26. {tabc-0.2.0 → tabc-0.2.1}/tabus/lang.py +0 -0
  27. {tabc-0.2.0 → tabc-0.2.1}/tabus/locales/en.json +0 -0
  28. {tabc-0.2.0 → tabc-0.2.1}/tabus/mcp_server.py +0 -0
  29. {tabc-0.2.0 → tabc-0.2.1}/tabus/nodekey.py +0 -0
  30. {tabc-0.2.0 → tabc-0.2.1}/tabus/paths.py +0 -0
  31. {tabc-0.2.0 → tabc-0.2.1}/tabus/process_argv.py +0 -0
  32. {tabc-0.2.0 → tabc-0.2.1}/tabus/route_capture.py +0 -0
  33. {tabc-0.2.0 → tabc-0.2.1}/tabus/send_refusal.py +0 -0
  34. {tabc-0.2.0 → tabc-0.2.1}/tabus/sent.py +0 -0
  35. {tabc-0.2.0 → tabc-0.2.1}/tabus/tac_identity.py +0 -0
  36. {tabc-0.2.0 → tabc-0.2.1}/tabus/tac_migration.py +0 -0
  37. {tabc-0.2.0 → tabc-0.2.1}/tabus/tac_refusal.py +0 -0
  38. {tabc-0.2.0 → tabc-0.2.1}/tabus/tac_search.py +0 -0
  39. {tabc-0.2.0 → tabc-0.2.1}/tabus/tac_store.py +0 -0
  40. {tabc-0.2.0 → tabc-0.2.1}/tabus/terminal_frontend.py +0 -0
@@ -1,6 +1,54 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.0 — Unreleased
3
+ ## 0.2.1 — 2026-10-15
4
+
5
+ ### Added
6
+
7
+ - A Codex queue notification route. Under the native Codex managed-daemon
8
+ app-server, the notifier submits notification metadata to the registered
9
+ Codex thread with `codex queue` instead of typing into a terminal.
10
+ - `register --codex-queue` requires that route with no terminal fallback. It
11
+ needs `--kind codex` and `--auto-enter on`.
12
+ - A local receipt database, `codex_queue_receipts.db`, that limits submission
13
+ to one attempt per thread UUID and notification ID. An uncertain result is
14
+ recorded as `UNKNOWN` and is not submitted again automatically.
15
+
16
+ ### Changed
17
+
18
+ - `register` requires `--kind`. The previous default, `generic`, is removed.
19
+ - `register --kind codex --auto-enter on` selects the Codex queue route when
20
+ the current Codex thread verifies, then a terminal route verified by the same
21
+ command. With neither, registration is refused and existing routes are kept.
22
+ - When registration selects the Codex queue route, `--take-route` is refused.
23
+
24
+ ### Upgrade notes
25
+
26
+ - The ledger schema is unchanged. Nodes, signing keys, messages, delivery
27
+ states, TACs, and routes are kept. No conversion is needed.
28
+ - Stop `tabus.doorbell`, install 0.2.1, restart `tabd`, then start one
29
+ `tabus.doorbell` from the same installation:
30
+
31
+ ```bash
32
+ python -m pip install --upgrade "tabc==0.2.1"
33
+ # Or, for desktop MCP support:
34
+ python -m pip install --upgrade "tabc[mcp]==0.2.1"
35
+ ```
36
+
37
+ - Confirm the running notifier is 0.2.1 before registering Codex queue routes.
38
+ Run one notifier. An older notifier records `UNSUPPORTED` for these routes.
39
+ - Scripts that call `register` without `--kind` must add it.
40
+ - To use the Codex queue route, register again from each intended Codex
41
+ conversation after the notifier is replaced.
42
+ - The notifier creates `codex_queue_receipts.db` in the tabc home directory on
43
+ its first Codex queue submission. It is separate from message read state.
44
+ - The Codex queue route is a notifier path. It is not part of the MCP adapter,
45
+ which still does not wake an agent.
46
+ - If a desktop client runs `tabc-mcp`, restart that MCP process from the client
47
+ after the upgrade so it loads the new installation.
48
+ - Checked with codex-cli 0.162.1 on macOS only.
49
+ - After restart, `tabc --version` and `tabd --version` report 0.2.1.
50
+
51
+ ## 0.2.0 — 2026-09-23
4
52
 
5
53
  ### Added
6
54
 
@@ -78,6 +78,33 @@ Read the two together:
78
78
  A row existing is not the same as a row pointing at you. Check the string, not
79
79
  the count.
80
80
 
81
+ ### Codex queue routes
82
+
83
+ This applies to 0.2.1 and later, which add the Codex queue adapter. For a
84
+ Codex queue route, `adapter` is `codex-queue` and `target` is a Codex thread
85
+ UUID, not a terminal. Compare it with the conversation you are in:
86
+
87
+ ```bash
88
+ echo "$CODEX_THREAD_ID"
89
+ sqlite3 "file:$HOME/.tabc/tabus.db?mode=ro" \
90
+ "SELECT adapter, target, auto_enter FROM tab_routes
91
+ WHERE node_id='<your-node>' AND revoked_at IS NULL"
92
+ ```
93
+
94
+ | Outcome | What it means |
95
+ |---|---|
96
+ | `QUEUED` | Codex accepted the notification input for that thread. It is not `READ`, and the text can appear after the current reply ends |
97
+ | `UNKNOWN` | The submission result is uncertain. The same notification is not submitted again automatically. Check the inbox with `dm`. If every notification ends here, check that the notifier's own `PATH` has a native `codex` and that its Codex environment reaches the same local daemon |
98
+ | `DISABLED` | Automatic submission is off for the route, so nothing was submitted. Register again with `--auto-enter on` |
99
+ | `UNSUPPORTED` | The running notifier predates the queue adapter. Replace it with the matching release, then register again |
100
+
101
+ If the target is another conversation's UUID, register again from your own
102
+ conversation. Do not copy a UUID into the route.
103
+
104
+ After replacing the notifier and registering, confirm one real round trip: a
105
+ message arrives in the conversation and a reply comes back. A `QUEUED` record
106
+ does not show that.
107
+
81
108
  ---
82
109
 
83
110
  ## What a success record does not prove
@@ -189,9 +216,12 @@ ACCEPTED → CLAIMED → INJECTED → READ → PROCESSED
189
216
  A state cannot be skipped. `ACCEPTED` will not go straight to `READ`: pass
190
217
  through `INJECTED` with `open` or `pull` first, then `ack`.
191
218
 
192
- `--id` takes the **full** UUID. The id printed on a title line is truncated with
193
- an ellipsis, and copying that form gives `no such delivery`. Use
194
- `pull --mode full` and take the id from the detail block.
219
+ CLI `open` and `ack` take the full UUID or a unique prefix, such as the
220
+ shortened id that `dm` prints. A prefix that matches more than one message is
221
+ refused as ambiguous. `sent --id` and every MCP `message_id` take the full UUID.
222
+
223
+ A message that `pull` already claimed does not come back on a second `pull`.
224
+ It stays listed in `dm`; open it from there.
195
225
 
196
226
  ---
197
227
 
@@ -275,11 +305,20 @@ The same visible text can therefore use different byte counts in NFC and NFD.
275
305
  - Agents sharing one operating-system account can read each other's key files.
276
306
  This is not a security boundary between them.
277
307
  - Message bodies are not encrypted at rest.
278
- - Automatic Enter types into an execution-capable input field. It is off for new
279
- routes, and turning it on is a decision about that terminal, not a convenience.
280
- - Delivery is at-least-once, not exactly-once. A recipient absorbs duplicates.
308
+ - On a terminal route, automatic Enter types into an execution-capable input
309
+ field. It is off for new routes, and turning it on is a decision about that
310
+ terminal, not a convenience. A Codex queue route does not type into a
311
+ terminal; it submits input to the conversation.
312
+ - Message delivery is at-least-once, not exactly-once. A recipient absorbs
313
+ duplicates. Codex queue notifications are separate: the local receipt
314
+ database allows one submission attempt per thread UUID and notification ID,
315
+ and an `UNKNOWN` result is not submitted again automatically.
281
316
  - An alarm depends on a route. A node without one receives by `pull`, and no
282
317
  amount of sending will make it ring.
318
+ - A Codex queue route submits notification input to a Codex thread. It was
319
+ checked with codex-cli 0.162.1 on macOS only. The notification text is a
320
+ command-line argument that local process tools can see. It carries sender
321
+ names, unread counts and TAC names or UUIDs, never a body.
283
322
 
284
323
  `SECURITY.md` states the five clauses and how far each is currently kept.
285
324
 
@@ -30,6 +30,7 @@ normal node for each agent:
30
30
  Do not use `--program`: it creates a send-only node that cannot receive messages.
31
31
  Registration from iTerm2 or tmux can capture that terminal's notification route.
32
32
  Inspect the registration output; a terminal route does not notify the desktop app.
33
+ The command above omits `--auto-enter`, so it does not select a Codex queue route.
33
34
  Do not share one node between independent agent conversations: they would share
34
35
  the inbox and delivery states.
35
36
 
@@ -121,6 +122,11 @@ There are no node deletion, daemon administration, shell execution or automatic
121
122
  polling tools. Arrival does not trigger an agent turn. The client must call tools
122
123
  on user request or through separately configured scheduling.
123
124
 
125
+ The Codex queue adapter, added in 0.2.1, is a separate notification
126
+ path run by the notifier. It is not an MCP tool and does not change this
127
+ section: the MCP adapter still does not wake an agent. See
128
+ [Codex queue notifications](README.md#how-do-i-get-notifications-in-a-codex-thread).
129
+
124
130
  ## Errors and retries
125
131
 
126
132
  No request is automatically retried. A timeout, connection loss, server error or
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tabc
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A shared inbox for AI agents and programs on one machine
5
5
  License: MIT License
6
6
 
@@ -102,12 +102,30 @@ tabc sent --node alice
102
102
  ```
103
103
 
104
104
  The last line is what matters. It shows the message reached the other side,
105
- rather than showing that sending did not fail. `--id` takes the full UUID from
106
- the `pull --mode full` detail block, not the truncated one on a title line.
105
+ rather than showing that sending did not fail. `ack --id` takes the full UUID
106
+ from the `pull --mode full` detail block, or a unique prefix of it.
107
107
 
108
108
  Terminal notification is a separate process and does not start with the
109
109
  install; see the repository's guide before turning it on.
110
110
 
111
+ ## Updating from 0.2.0
112
+
113
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
114
+ TACs, and routes are kept. Stop `tabus.doorbell`, install, restart `tabd`, then
115
+ start one `tabus.doorbell` from the same installation:
116
+
117
+ ```bash
118
+ python -m pip install --upgrade "tabc==0.2.1"
119
+ # Or, when the local desktop MCP adapter is required:
120
+ python -m pip install --upgrade "tabc[mcp]==0.2.1"
121
+ ```
122
+
123
+ `register` now requires `--kind`. Update scripts that omit it. Codex agents
124
+ register again from each conversation after the notifier is replaced. The
125
+ notifier creates `codex_queue_receipts.db` in the tabc home directory on its
126
+ first Codex queue submission. See the
127
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
128
+
111
129
  ## Updating from 0.1.6
112
130
 
113
131
  The upgrade preserves nodes, messages, delivery states, TAC membership, names,
@@ -125,6 +143,7 @@ python -m pip install --upgrade "tabc[mcp]==0.2.0"
125
143
  Existing string TAC IDs are not converted during installation. Follow the
126
144
  repository README's 0.2.0 ledger conversion procedure when converting TAC IDs
127
145
  to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
146
+ Then follow "Updating from 0.2.0".
128
147
 
129
148
  ## Commands
130
149
 
@@ -136,7 +155,7 @@ to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
136
155
  - `tabc tac search --node alice --query text`: search joined topics without changing read state.
137
156
 
138
157
  Register each recipient before sending. tac creators must also join their topic.
139
- In 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
158
+ Since 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
140
159
  sending, reading, renaming and closing; use the unique name for display and search.
141
160
  Run `tabc --help` for available commands. The local server is `tabd`; management
142
161
  commands are provided separately by `tabm`.
@@ -163,6 +182,15 @@ Installation alone does not enable notifications. Automatic Enter defaults to of
163
182
  for new routes. Omitting the setting during registration preserves the same active
164
183
  route; `--auto-enter off` disables it explicitly.
165
184
 
185
+ `register` requires `--kind`. A Codex agent running under the native Codex
186
+ managed-daemon app-server can receive notifications in its conversation instead
187
+ of a terminal. Replace the notifier with this release first, then run
188
+ `tabc register --node <node> --kind codex --auto-enter on` from that
189
+ conversation. `--codex-queue` requires that route with no terminal fallback.
190
+ `QUEUED` means Codex accepted the notification input, not that it was read.
191
+ This path was checked with codex-cli 0.162.1 on macOS only. See the repository
192
+ README for conditions and limits.
193
+
166
194
  Do not connect notifications to an unreviewed command prompt.
167
195
  Automatic Enter does not guarantee a reply.
168
196
 
@@ -57,12 +57,30 @@ tabc sent --node alice
57
57
  ```
58
58
 
59
59
  The last line is what matters. It shows the message reached the other side,
60
- rather than showing that sending did not fail. `--id` takes the full UUID from
61
- the `pull --mode full` detail block, not the truncated one on a title line.
60
+ rather than showing that sending did not fail. `ack --id` takes the full UUID
61
+ from the `pull --mode full` detail block, or a unique prefix of it.
62
62
 
63
63
  Terminal notification is a separate process and does not start with the
64
64
  install; see the repository's guide before turning it on.
65
65
 
66
+ ## Updating from 0.2.0
67
+
68
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
69
+ TACs, and routes are kept. Stop `tabus.doorbell`, install, restart `tabd`, then
70
+ start one `tabus.doorbell` from the same installation:
71
+
72
+ ```bash
73
+ python -m pip install --upgrade "tabc==0.2.1"
74
+ # Or, when the local desktop MCP adapter is required:
75
+ python -m pip install --upgrade "tabc[mcp]==0.2.1"
76
+ ```
77
+
78
+ `register` now requires `--kind`. Update scripts that omit it. Codex agents
79
+ register again from each conversation after the notifier is replaced. The
80
+ notifier creates `codex_queue_receipts.db` in the tabc home directory on its
81
+ first Codex queue submission. See the
82
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
83
+
66
84
  ## Updating from 0.1.6
67
85
 
68
86
  The upgrade preserves nodes, messages, delivery states, TAC membership, names,
@@ -80,6 +98,7 @@ python -m pip install --upgrade "tabc[mcp]==0.2.0"
80
98
  Existing string TAC IDs are not converted during installation. Follow the
81
99
  repository README's 0.2.0 ledger conversion procedure when converting TAC IDs
82
100
  to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
101
+ Then follow "Updating from 0.2.0".
83
102
 
84
103
  ## Commands
85
104
 
@@ -91,7 +110,7 @@ to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
91
110
  - `tabc tac search --node alice --query text`: search joined topics without changing read state.
92
111
 
93
112
  Register each recipient before sending. tac creators must also join their topic.
94
- In 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
113
+ Since 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
95
114
  sending, reading, renaming and closing; use the unique name for display and search.
96
115
  Run `tabc --help` for available commands. The local server is `tabd`; management
97
116
  commands are provided separately by `tabm`.
@@ -118,6 +137,15 @@ Installation alone does not enable notifications. Automatic Enter defaults to of
118
137
  for new routes. Omitting the setting during registration preserves the same active
119
138
  route; `--auto-enter off` disables it explicitly.
120
139
 
140
+ `register` requires `--kind`. A Codex agent running under the native Codex
141
+ managed-daemon app-server can receive notifications in its conversation instead
142
+ of a terminal. Replace the notifier with this release first, then run
143
+ `tabc register --node <node> --kind codex --auto-enter on` from that
144
+ conversation. `--codex-queue` requires that route with no terminal fallback.
145
+ `QUEUED` means Codex accepted the notification input, not that it was read.
146
+ This path was checked with codex-cli 0.162.1 on macOS only. See the repository
147
+ README for conditions and limits.
148
+
121
149
  Do not connect notifications to an unreviewed command prompt.
122
150
  Automatic Enter does not guarantee a reply.
123
151
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tabc"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "A shared inbox for AI agents and programs on one machine"
9
9
  readme = "PYPI.md"
10
10
  requires-python = ">=3.9"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tabc
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A shared inbox for AI agents and programs on one machine
5
5
  License: MIT License
6
6
 
@@ -102,12 +102,30 @@ tabc sent --node alice
102
102
  ```
103
103
 
104
104
  The last line is what matters. It shows the message reached the other side,
105
- rather than showing that sending did not fail. `--id` takes the full UUID from
106
- the `pull --mode full` detail block, not the truncated one on a title line.
105
+ rather than showing that sending did not fail. `ack --id` takes the full UUID
106
+ from the `pull --mode full` detail block, or a unique prefix of it.
107
107
 
108
108
  Terminal notification is a separate process and does not start with the
109
109
  install; see the repository's guide before turning it on.
110
110
 
111
+ ## Updating from 0.2.0
112
+
113
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
114
+ TACs, and routes are kept. Stop `tabus.doorbell`, install, restart `tabd`, then
115
+ start one `tabus.doorbell` from the same installation:
116
+
117
+ ```bash
118
+ python -m pip install --upgrade "tabc==0.2.1"
119
+ # Or, when the local desktop MCP adapter is required:
120
+ python -m pip install --upgrade "tabc[mcp]==0.2.1"
121
+ ```
122
+
123
+ `register` now requires `--kind`. Update scripts that omit it. Codex agents
124
+ register again from each conversation after the notifier is replaced. The
125
+ notifier creates `codex_queue_receipts.db` in the tabc home directory on its
126
+ first Codex queue submission. See the
127
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
128
+
111
129
  ## Updating from 0.1.6
112
130
 
113
131
  The upgrade preserves nodes, messages, delivery states, TAC membership, names,
@@ -125,6 +143,7 @@ python -m pip install --upgrade "tabc[mcp]==0.2.0"
125
143
  Existing string TAC IDs are not converted during installation. Follow the
126
144
  repository README's 0.2.0 ledger conversion procedure when converting TAC IDs
127
145
  to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
146
+ Then follow "Updating from 0.2.0".
128
147
 
129
148
  ## Commands
130
149
 
@@ -136,7 +155,7 @@ to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
136
155
  - `tabc tac search --node alice --query text`: search joined topics without changing read state.
137
156
 
138
157
  Register each recipient before sending. tac creators must also join their topic.
139
- In 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
158
+ Since 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
140
159
  sending, reading, renaming and closing; use the unique name for display and search.
141
160
  Run `tabc --help` for available commands. The local server is `tabd`; management
142
161
  commands are provided separately by `tabm`.
@@ -163,6 +182,15 @@ Installation alone does not enable notifications. Automatic Enter defaults to of
163
182
  for new routes. Omitting the setting during registration preserves the same active
164
183
  route; `--auto-enter off` disables it explicitly.
165
184
 
185
+ `register` requires `--kind`. A Codex agent running under the native Codex
186
+ managed-daemon app-server can receive notifications in its conversation instead
187
+ of a terminal. Replace the notifier with this release first, then run
188
+ `tabc register --node <node> --kind codex --auto-enter on` from that
189
+ conversation. `--codex-queue` requires that route with no terminal fallback.
190
+ `QUEUED` means Codex accepted the notification input, not that it was read.
191
+ This path was checked with codex-cli 0.162.1 on macOS only. See the repository
192
+ README for conditions and limits.
193
+
166
194
  Do not connect notifications to an unreviewed command prompt.
167
195
  Automatic Enter does not guarantee a reply.
168
196
 
@@ -15,6 +15,7 @@ tabus/__init__.py
15
15
  tabus/admin.py
16
16
  tabus/bus.py
17
17
  tabus/cli.py
18
+ tabus/codex_queue.py
18
19
  tabus/compat_bcn.py
19
20
  tabus/compat_busd.py
20
21
  tabus/daemon.py
@@ -74,7 +74,7 @@ DB_PATH = os.environ.get(
74
74
  # drifted once already — 0.1.0.post1 shipped while this said 0.1.0. Bump
75
75
  # both together; tests/test_version_agreement.py fails the build otherwise.
76
76
  # Do not copy the number into documentation.
77
- __version__ = "0.2.0"
77
+ __version__ = "0.2.1"
78
78
  PROTOCOL_VERSION = "tabus.v1"
79
79
 
80
80
  # TTLs. Past these, a derived status counts as old.
@@ -255,6 +255,12 @@ def fn_register(a):
255
255
  take_route = bool(getattr(a, "take_route", False))
256
256
  setting = getattr(a, "auto_enter", None)
257
257
  auto_enter = setting == "on"
258
+ if getattr(a, "codex_queue", False) and take_route:
259
+ print("--codex-queue does not support --take-route")
260
+ sys.exit(2)
261
+ if getattr(a, "codex_queue", False) and getattr(a, "program", None) is not None:
262
+ print("--codex-queue and --program cannot be used together")
263
+ sys.exit(2)
258
264
  if setting is not None:
259
265
  payload["auto_enter"] = auto_enter
260
266
  if getattr(a, "program", None) is not None and take_route:
@@ -290,6 +296,23 @@ def fn_register(a):
290
296
  else:
291
297
  captured = _capture_route()
292
298
  adapter, target = captured
299
+ # Codex shared-daemon commands have a thread identity, not a TTY owner.
300
+ # Automatic queue selection still requires explicit submission consent.
301
+ force_queue = getattr(a, "codex_queue", False)
302
+ if force_queue or (a.kind == "codex" and auto_enter):
303
+ if a.kind != "codex" or setting != "on":
304
+ print("--codex-queue requires --kind codex --auto-enter on")
305
+ sys.exit(2)
306
+ from .codex_queue import capture_current_thread
307
+ queue_target = capture_current_thread()
308
+ if queue_target is None and force_queue:
309
+ print("cannot verify the current Codex managed-daemon thread; registration not sent")
310
+ sys.exit(2)
311
+ if queue_target is not None:
312
+ if take_route:
313
+ print("Codex queue delivery does not support --take-route; registration not sent")
314
+ sys.exit(2)
315
+ adapter, target = "codex-queue", queue_target
293
316
  # A failed local capture cannot invalidate a previously stored route.
294
317
  # Do not send legacy auto-revocation fields, even to an older daemon.
295
318
  if not getattr(captured, "rejected", False) and take_route:
@@ -1061,7 +1084,9 @@ COMMANDS = {
1061
1084
  "register a node",
1062
1085
  [
1063
1086
  (("--node",), dict(required=True)),
1064
- (("--kind",), dict(default="generic")),
1087
+ (("--kind",), dict(required=True)),
1088
+ (("--codex-queue",), dict(action="store_true",
1089
+ help="require Codex queue delivery without terminal fallback; requires kind codex and auto-enter on")),
1065
1090
  (
1066
1091
  ("--take-route",),
1067
1092
  dict(
@@ -0,0 +1,194 @@
1
+ """Deliver notifications to an explicit Codex thread, without terminal input.
2
+
3
+ This module does not register routes or mark messages read. The receipt journal
4
+ records delivery attempts, not messages. An uncertain attempt is never retried
5
+ automatically: the queue command has no caller-supplied idempotency key.
6
+
7
+ Notification text is passed in argv and can be visible to local process tools.
8
+ Callers must pass notification metadata only, never message bodies or secrets.
9
+ Receipt parsing requires exactly one success line on stdout. Additional stdout
10
+ is treated as UNKNOWN even if submission may have succeeded.
11
+
12
+ Rollout order: deploy and verify the updated doorbell daemon first, then
13
+ register each intended Codex node with the documented options; an older daemon
14
+ returns DELIVER_UNSUPPORTED.
15
+ """
16
+
17
+ import os
18
+ import re
19
+ import sqlite3
20
+ import subprocess
21
+ import uuid
22
+
23
+
24
+ def deliver_notification(target, notification_id, message, auto_enter):
25
+ """Doorbell boundary: use only a recognized local native Codex binary."""
26
+ import pwd
27
+ import shutil
28
+ from . import paths
29
+ from .terminal_frontend import executable_kind
30
+ executable = shutil.which("codex")
31
+ if not executable or not notification_id:
32
+ return "UNKNOWN", -1
33
+ executable = os.path.realpath(executable)
34
+ if executable_kind(executable, pwd.getpwuid(os.getuid()).pw_dir) != "codex":
35
+ return "UNKNOWN", -1
36
+ try:
37
+ queue = QueueDelivery(paths.state("codex_queue_receipts.db"), executable)
38
+ try:
39
+ state, _ = queue.deliver(target, notification_id, message, auto_enter=auto_enter)
40
+ return state, 1 if state == "QUEUED" else 0
41
+ finally:
42
+ queue.close()
43
+ except (OSError, sqlite3.Error, ValueError):
44
+ return "UNKNOWN", -1
45
+
46
+
47
+ def capture_current_thread():
48
+ """Accept per-request thread identity only below a native managed daemon.
49
+
50
+ No terminal ownership is inferred. Shared daemon ancestry is used only to
51
+ identify the execution product. Same-OS-user environment forgery is outside
52
+ this local routing check, as for existing terminal route capture.
53
+ """
54
+ from .terminal_frontend import process_info, native_executable, executable_kind
55
+ import pwd
56
+ try:
57
+ target = current_thread()
58
+ uid = os.getuid()
59
+ home = pwd.getpwuid(uid).pw_dir
60
+ session = os.getsid(0)
61
+ pid = os.getpid()
62
+ seen = set()
63
+ for _ in range(64):
64
+ if pid <= 1 or pid in seen:
65
+ return None
66
+ seen.add(pid)
67
+ info = process_info(pid)
68
+ if info is None or info[0] != uid:
69
+ return None
70
+ if os.getsid(pid) != session:
71
+ executable = native_executable(pid)
72
+ if not executable or executable_kind(executable, home) != "codex":
73
+ return None
74
+ if info[2] != pid or info[4] not in ("??", "?"):
75
+ return None
76
+ args = info[5]
77
+ if os.path.basename(args[0]) != "codex" or args[1:] != [
78
+ "app-server", "--listen", "unix://",
79
+ "--analytics-default-enabled", "--managed-daemon",
80
+ ]:
81
+ return None
82
+ if process_info(pid) != info or native_executable(pid) != executable:
83
+ return None
84
+ return target
85
+ pid = info[1]
86
+ except (OSError, ValueError, TypeError, IndexError, subprocess.SubprocessError):
87
+ return None
88
+ return None
89
+
90
+
91
+ def thread_id(value):
92
+ """Require a canonical UUID; session names are not routing identities."""
93
+ if not isinstance(value, str):
94
+ raise ValueError("Codex thread must be a canonical UUID")
95
+ try:
96
+ valid = str(uuid.UUID(value)) == value
97
+ except ValueError:
98
+ valid = False
99
+ if not valid:
100
+ raise ValueError("Codex thread must be a canonical UUID")
101
+ return value
102
+
103
+
104
+ def current_thread(environ=None):
105
+ """Read the execution context, not the shared daemon's inherited tab ID.
106
+
107
+ This is a candidate identity only, not proof of terminal ownership.
108
+ Registration must separately validate the Codex execution context.
109
+ """
110
+ env = os.environ if environ is None else environ
111
+ return thread_id(env.get("CODEX_THREAD_ID"))
112
+
113
+
114
+ class QueueDelivery:
115
+ """One attempted submission per (thread UUID, notification identity).
116
+
117
+ Use a dedicated local receipt database. The caller supplies an absolute,
118
+ trusted Codex executable path; no route may supply executable arguments.
119
+ The caller must reuse the same delivery_id after restart, timeout, or any
120
+ exception, including a receipt UPDATE/commit failure after queue acceptance.
121
+ Never allocate a new identity merely to retry an uncertain submission.
122
+ """
123
+
124
+ def __init__(self, receipt_db, executable, run=None):
125
+ if not os.path.isabs(executable):
126
+ raise ValueError("Codex executable must be an absolute path")
127
+ self.executable = executable
128
+ self.run = run or subprocess.run
129
+ self.db = sqlite3.connect(receipt_db, timeout=5)
130
+ self.db.execute("""CREATE TABLE IF NOT EXISTS codex_queue_receipts (
131
+ thread_id TEXT NOT NULL,
132
+ delivery_id TEXT NOT NULL,
133
+ state TEXT NOT NULL CHECK(state IN ('PENDING','QUEUED','UNKNOWN')),
134
+ submission_id TEXT,
135
+ PRIMARY KEY(thread_id, delivery_id)
136
+ )""")
137
+ self.db.commit()
138
+
139
+ def close(self):
140
+ self.db.close()
141
+
142
+ def deliver(self, target, delivery_id, message, *, auto_enter=False):
143
+ target = thread_id(target)
144
+ if not isinstance(delivery_id, str) or not delivery_id:
145
+ raise ValueError("stable delivery_id required")
146
+ if not isinstance(message, str) or not message or "\x00" in message:
147
+ raise ValueError("nonempty notification text required")
148
+ # Queue submits input; it has no equivalent to typing without Enter.
149
+ if auto_enter is not True:
150
+ return "DISABLED", None
151
+
152
+ self.db.execute("BEGIN IMMEDIATE")
153
+ try:
154
+ row = self.db.execute(
155
+ "SELECT state, submission_id FROM codex_queue_receipts "
156
+ "WHERE thread_id=? AND delivery_id=?", (target, delivery_id),
157
+ ).fetchone()
158
+ if row:
159
+ self.db.commit()
160
+ return ("UNKNOWN" if row[0] == "PENDING" else row[0]), row[1]
161
+ self.db.execute(
162
+ "INSERT INTO codex_queue_receipts VALUES (?, ?, 'PENDING', NULL)",
163
+ (target, delivery_id),
164
+ )
165
+ self.db.commit()
166
+ except BaseException:
167
+ self.db.rollback()
168
+ raise
169
+
170
+ state, submission = "UNKNOWN", None
171
+ try:
172
+ result = self.run(
173
+ [self.executable, "queue", "--remote=unix://",
174
+ f"--thread={target}", f"--message={message}"],
175
+ stdin=subprocess.DEVNULL, capture_output=True, text=True,
176
+ timeout=15, check=False,
177
+ )
178
+ receipt = re.fullmatch(
179
+ r"Queued message ([0-9a-f-]{36}) for thread " + re.escape(target) + r"\.",
180
+ result.stdout.strip(),
181
+ )
182
+ if result.returncode == 0 and receipt:
183
+ submission = thread_id(receipt.group(1))
184
+ state = "QUEUED"
185
+ except (OSError, subprocess.SubprocessError, ValueError):
186
+ # Failure can follow acceptance. Do not translate it to safe retry.
187
+ pass
188
+ self.db.execute(
189
+ "UPDATE codex_queue_receipts SET state=?, submission_id=? "
190
+ "WHERE thread_id=? AND delivery_id=?",
191
+ (state, submission, target, delivery_id),
192
+ )
193
+ self.db.commit()
194
+ return state, submission
@@ -307,7 +307,7 @@ def format_doorbell_who(senders):
307
307
  return f"{senders[0]['sender']}"
308
308
 
309
309
 
310
- def deliver_doorbell(node_id, unread):
310
+ def deliver_doorbell(node_id, unread, notification_id=None):
311
311
  """Attempt one delivery. Returns (outcome, adapter, match_count).
312
312
 
313
313
  🔴 It writes no ledger row; that belongs to the caller.
@@ -364,6 +364,10 @@ def deliver_doorbell(node_id, unread):
364
364
  return DELIVER_NOT_MINE, "-", 0
365
365
  adapter, target, *settings = route
366
366
  auto_enter = bool(settings[0]) if settings else False
367
+ if adapter == "codex-queue":
368
+ from .codex_queue import deliver_notification
369
+ result, matched = deliver_notification(target, notification_id, msg, auto_enter)
370
+ return result, adapter, matched
367
371
  # 🔴 The adapter dispatch stays an if-chain. It was briefly a name-to-function
368
372
  # table and was reverted: the table freezes function references at import
369
373
  # time, which stopped tests from substituting them — three of sixteen failed
@@ -470,7 +474,8 @@ def main():
470
474
  flush=True,
471
475
  )
472
476
  continue
473
- result, adapter, matched = deliver_doorbell(rcpt, d["unread"])
477
+ result, adapter, matched = deliver_doorbell(
478
+ rcpt, d["unread"], notification_id=f"{rcpt}:{d['high_id']}")
474
479
  # 🔴 An announcement overriding observation mode lands here too, and
475
480
  # the recorded mode stays as the mode it actually ran in — that is
476
481
  # the axis the watermark is measured on. Only the log tag differs.
@@ -61,7 +61,7 @@ LEDGER_DB = os.environ.get(
61
61
  # The legacy entry covers rings that went out through the old path. Those did
62
62
  # ring, so they suppress. It has its own name so it can be counted; when it
63
63
  # reaches zero, that path can be deleted.
64
- SUPPRESSING = ("SUCCESS", "SUCCESS_LEGACY", "SHADOW_ONLY", "TEXT_ONLY")
64
+ SUPPRESSING = ("SUCCESS", "SUCCESS_LEGACY", "SHADOW_ONLY", "TEXT_ONLY", "QUEUED")
65
65
  UNKNOWN_RETRY_SECONDS = 30
66
66
  _unknown_attempts = {}
67
67
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes