beads-ui 0.3.0 → 0.4.0

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 (61) hide show
  1. package/CHANGES.md +26 -0
  2. package/README.md +15 -6
  3. package/app/main.bundle.js +617 -0
  4. package/app/main.bundle.js.map +7 -0
  5. package/bin/bdui.js +2 -1
  6. package/package.json +27 -16
  7. package/server/app.js +39 -35
  8. package/server/bd.js +6 -2
  9. package/server/cli/commands.js +12 -8
  10. package/server/cli/daemon.js +20 -5
  11. package/server/cli/index.js +19 -31
  12. package/server/cli/open.js +3 -0
  13. package/server/cli/usage.js +4 -2
  14. package/server/config.js +3 -2
  15. package/server/db.js +9 -6
  16. package/server/index.js +10 -4
  17. package/server/list-adapters.js +9 -3
  18. package/server/logging.js +23 -0
  19. package/server/subscriptions.js +12 -0
  20. package/server/validators.js +2 -0
  21. package/server/watcher.js +10 -5
  22. package/server/ws.js +31 -10
  23. package/app/data/list-selectors.js +0 -98
  24. package/app/data/providers.js +0 -76
  25. package/app/data/sort.js +0 -45
  26. package/app/data/subscription-issue-store.js +0 -161
  27. package/app/data/subscription-issue-stores.js +0 -102
  28. package/app/data/subscriptions-store.js +0 -219
  29. package/app/main.js +0 -702
  30. package/app/protocol.js +0 -196
  31. package/app/protocol.md +0 -66
  32. package/app/router.js +0 -114
  33. package/app/state.js +0 -103
  34. package/app/utils/issue-id-renderer.js +0 -71
  35. package/app/utils/issue-id.js +0 -10
  36. package/app/utils/issue-type.js +0 -27
  37. package/app/utils/issue-url.js +0 -9
  38. package/app/utils/markdown.js +0 -22
  39. package/app/utils/priority-badge.js +0 -47
  40. package/app/utils/priority.js +0 -1
  41. package/app/utils/status-badge.js +0 -32
  42. package/app/utils/status.js +0 -23
  43. package/app/utils/toast.js +0 -34
  44. package/app/utils/type-badge.js +0 -33
  45. package/app/views/board.js +0 -535
  46. package/app/views/detail.js +0 -1249
  47. package/app/views/epics.js +0 -280
  48. package/app/views/issue-dialog.js +0 -163
  49. package/app/views/issue-row.js +0 -190
  50. package/app/views/list.js +0 -464
  51. package/app/views/nav.js +0 -67
  52. package/app/views/new-issue-dialog.js +0 -345
  53. package/app/ws.js +0 -279
  54. package/docs/adr/001-push-only-lists.md +0 -134
  55. package/docs/adr/002-per-subscription-stores-and-full-issue-push.md +0 -200
  56. package/docs/architecture.md +0 -194
  57. package/docs/data-exchange-subscription-plan.md +0 -198
  58. package/docs/db-watching.md +0 -30
  59. package/docs/migration-v2.md +0 -54
  60. package/docs/protocol/issues-push-v2.md +0 -179
  61. package/docs/subscription-issue-store.md +0 -112
@@ -1,198 +0,0 @@
1
- # Data Exchange Model — Subscription‑Based Updates (Full‑Issue)
2
-
3
- ```
4
- Date: 2025-10-25
5
- Status: Implemented
6
- Owner: agent
7
- ```
8
-
9
- ## Goals
10
-
11
- - Replace ad-hoc list fetching with subscription-based incremental updates.
12
- - Minimize complexity; send full‑issue payloads in envelopes targeted to a
13
- specific subscription key.
14
- - Ensure consistent, race-free updates around user-triggered mutations.
15
- - Keep UI models per-subscription to simplify rendering and memory usage.
16
-
17
- ## Scope
18
-
19
- - Server and client for `beads-ui`.
20
- - Uses `bd` CLI for data access; no DB schema changes.
21
-
22
- ## Subscription Types
23
-
24
- - `all-issues`
25
- - `epics` // Removed: `issues-for-epic` (use `issue-detail` for the epic and
26
- render its `dependents`)
27
- - `blocked-issues`
28
- - `ready-issues`
29
- - `in-progress-issues`
30
- - `closed-issues` (special filtering noted below)
31
-
32
- ## Server Architecture
33
-
34
- ### Subscription Registry (Issue List Subscriptions)
35
-
36
- - Keyed by `subscriptionKey = type + JSON.stringify(params)`.
37
- - Value:
38
- `{ itemsById: Map<string, { updated_at: string, closed_at: string|null }>, subscribers: Set<SubscriberId>, lastRunAt?: number }`.
39
- - Each subscribe request either attaches to an existing registry entry or
40
- creates a new one.
41
- - No TTL: subscriptions are evicted only on WebSocket disconnect. Unsubscribe
42
- removes a subscriber from the set but keeps the registry entry until the
43
- connection closes.
44
-
45
- ### Mapping to `bd` Commands
46
-
47
- - `all-issues` → `bd list` (default/open)
48
- - `epics` → `bd list --type epic` (or equivalent)
49
- - `detail:{id}` → `bd show <id> --json` (use `dependents` from the epic detail
50
- for children)
51
- - `blocked-issues` → `bd list --blocked`
52
- - `ready-issues` → `bd ready --limit 1000`
53
- - `in-progress-issues` → `bd list --status in_progress`
54
- - `closed-issues` → `bd list --status closed` (then filter first; see Special
55
- Cases)
56
-
57
- Notes:
58
-
59
- - Exact flags depend on `bd`; create adapters that encapsulate CLI details and
60
- normalize results.
61
-
62
- ### Refresh Algorithm (per run)
63
-
64
- 1. Execute mapped `bd` command to get the full list of `issues` for the spec.
65
- 2. If subscription is `closed-issues` with a filter, apply it before step 3.
66
- 3. Compare with the registry’s last known items for this subscription key.
67
- 4. For new or changed items, emit `upsert` envelopes with the full issue payload
68
- to all subscribers of the key on the current connection.
69
- 5. For removed items, emit `delete` envelopes with `issue_id`.
70
- 6. Update the registry’s state for the key.
71
-
72
- ### Special Case: Closed Issues Filtering
73
-
74
- - Apply `since` filter (epoch milliseconds) before diffing to avoid spurious
75
- updates when reloading older closed items. Only items with
76
- `closed_at >= since` are included. Invalid or non-positive `since` values are
77
- ignored.
78
- - Filters are part of subscription params to keep deterministic diffing.
79
-
80
- ### Migration
81
-
82
- This change replaces request/response list reads and id‑only deltas with
83
- subscription‑based, full‑issue push envelopes.
84
-
85
- Client migration steps:
86
-
87
- - Replace list fetch calls with `subscribe-list`/`unsubscribe-list` messages.
88
- - Maintain a per‑subscription local store keyed by the client `id`.
89
- - Apply `snapshot`/`upsert`/`delete` envelopes in revision order; render from
90
- `store.snapshot()`.
91
- - Remove any legacy polling timers; updates now arrive via server push.
92
- - For closed issue feeds, pass a `params.since` value (epoch ms) that reflects
93
- the UI’s filter horizon if needed server‑side.
94
-
95
- ### Watcher Integration (DB Updates)
96
-
97
- - A file/DB watcher signals any data change.
98
- - On signal, for each active subscription: re-run its mapped `bd` command → diff
99
- → push deltas to all subscribers.
100
- - Backpressure: coalesce multiple watcher events into a single run per
101
- subscription (leading-edge, with trailing-edge within 50–100ms).
102
-
103
- ### User Mutations (Race Control)
104
-
105
- When client requests a change (e.g., update status):
106
-
107
- 1. Execute the explicit protocol mutation (mapped to a concrete `bd` command
108
- under the hood; no arbitrary commands allowed).
109
- 2. In parallel, attach a once-listener to the watcher that resolves on the next
110
- change event (no debounce) or a 500ms timeout, whichever occurs first.
111
- 3. After the promise resolves, for each affected subscription, run the standard
112
- refresh/diff/push routine exactly once.
113
- 4. During the pending mutation window, suppress watcher-triggered refreshes for
114
- affected subscriptions to avoid duplicate pushes.
115
-
116
- ### Error Handling
117
-
118
- - Validate subscription params; return structured errors.
119
- - For `bd` failures, include stderr and exit code; do not crash subscriptions.
120
- - If a subscriber disconnects mid-push, drop silently and clean up.
121
-
122
- ## Client Architecture
123
-
124
- ### Local Store per Subscription
125
-
126
- - Keyed by `subscriptionKey`.
127
- - Value: `{ itemsById: Map<string, Issue>, lastAppliedAt: number }`.
128
- - On `{ added, updated, removed }`, update `itemsById` accordingly and request
129
- view re-render.
130
- - Tabs and epic expansion toggle subscribe/unsubscribe appropriately.
131
-
132
- ### UI Flow
133
-
134
- - Tab switch: unsubscribe previous, subscribe new.
135
- - Epic toggle: subscribe/unsubscribe `detail:{id}` with
136
- `{ type: 'issue-detail', params: { id } }`.
137
- - Components derive view state from the local store snapshot.
138
-
139
- ## Wire Protocol (vNext)
140
-
141
- ### Messages: Client → Server
142
-
143
- - `subscribe-list` `{ id: string, type: string, params?: object }`
144
- - `unsubscribe-list` `{ id: string }`
145
- - Explicit mutation messages (enumerated in the protocol; no generic command
146
- pipe). The set mirrors the main protocol (update-status, edit-text,
147
- update-priority, update-assignee, create-issue, dep-add/remove,
148
- label-add/remove).
149
-
150
- ### Messages: Server → Client (Per‑Subscription)
151
-
152
- All envelopes include a per‑subscription `revision` (monotonic, starting at 1),
153
- and the client subscription `id`.
154
-
155
- - `snapshot` `{ id, schema, revision, issues: Issue[] }`
156
- - `upsert` `{ id, schema, revision, issue: Issue }`
157
- - `delete` `{ id, schema, revision, issue_id: string }`
158
-
159
- Notes
160
-
161
- - Initial subscribe triggers a single `snapshot` for the requesting `id` only.
162
- - Subsequent refresh runs emit `upsert`/`delete` events to all subscribers of
163
- the same subscription key on that connection.
164
- - Clients MUST apply envelopes in `revision` order and ignore stale revisions.
165
-
166
- ## Concurrency & Ordering Guarantees
167
-
168
- - Per-subscription ordering: server serializes diff runs per key.
169
- - Deltas are applied in order on the client; no interleaving for a given `id`.
170
- - Mutations provide “eventually up-to-date” guarantee via the once-listener +
171
- timeout.
172
-
173
- ## Observability
174
-
175
- - Basic development logging only; no telemetry collection for message rates.
176
-
177
- ## Security
178
-
179
- - Only explicit mutation operations are implemented by the protocol; no
180
- arbitrary commands from clients.
181
- - Reject unknown subscription types; enforce param schemas.
182
-
183
- ## Testing Strategy
184
-
185
- - Unit: diffing, registry, adapter mapping, filter logic.
186
- - Integration: watcher → refresh → push flow; mutation window once-only
187
- behavior.
188
- - E2E: tab switching, epic expansion, status changes while updates stream.
189
-
190
- ## Release Notes
191
-
192
- - Breaking change: Clients must adopt `snapshot`/`upsert`/`delete` envelopes and
193
- per‑subscription stores. Previous polling and id‑only list deltas are removed.
194
-
195
- ## Open Questions
196
-
197
- - Exact `bd` flags for each list type; confirm and codify.
198
- - Closed-issue filter semantics (date range vs. other criteria).
@@ -1,30 +0,0 @@
1
- # DB Watching and Resolution
2
-
3
- The server watches the active beads SQLite database file for changes and
4
- schedules a refresh of active list subscriptions. Clients receive
5
- `snapshot`/`upsert`/`delete` envelopes for their active subscriptions.
6
-
7
- ## Resolution Order
8
-
9
- The DB path is resolved to match beads CLI precedence:
10
-
11
- 1. `--db <path>` flag (when forced by the server configuration)
12
- 2. `BEADS_DB` environment variable
13
- 3. Nearest `.beads/*.db` by walking up from the server `root_dir`
14
- 4. `~/.beads/default.db` fallback
15
-
16
- The resolved path is injected into all `bd` CLI invocations via `--db` to ensure
17
- the watcher and CLI operate on the same database.
18
-
19
- ## Behavior When Missing
20
-
21
- If no database exists at the resolved path (e.g., before `bd init`), the server
22
- will still attempt to bind a watcher on the containing directory and log a clear
23
- warning. Initialize a database with one of:
24
-
25
- - `bd --db /path/to/file.db init`
26
- - `export BEADS_DB=/path/to/file.db && bd init`
27
- - `bd init` in a workspace with a `.beads/` directory
28
-
29
- After initialization, changes will be detected without restarting the server.
30
- The watcher can rebind when the workspace or configuration changes at runtime.
@@ -1,54 +0,0 @@
1
- # Migration: Push‑Only Per‑Subscription Stores (Breaking)
2
-
3
- ```
4
- Date: 2025-10-26
5
- Status: Final
6
- Owner: agent
7
- ```
8
-
9
- This release replaces legacy list reads and id‑only list deltas with
10
- per‑subscription push envelopes that carry full issue payloads. There is no
11
- compatibility mode and no feature flags.
12
-
13
- ## Required Versions
14
-
15
- - beads‑ui: 0.2.0 or later (includes the server)
16
- - Node.js: >= 22 (see `package.json` engines)
17
-
18
- Upgrade:
19
-
20
- ```sh
21
- npm i -g beads-ui@latest
22
- ```
23
-
24
- ## What Changed
25
-
26
- - New protocol: `snapshot` / `upsert` / `delete` envelopes and a
27
- per‑subscription `revision`.
28
- - One store per list: views render from a `SubscriptionIssueStore` created for
29
- each active subscription id.
30
- - Removed: central issues store and delta fan‑out.
31
- - Removed: legacy read RPCs `list-issues` and `epic-status`.
32
-
33
- ## Migration Checklist
34
-
35
- - Replace list reads with `subscribe-list`/`unsubscribe-list`.
36
- - Create a `SubscriptionIssueStore` at view mount and wire the WS client to call
37
- `store.applyPush(payload)` for `snapshot`/`upsert`/`delete`.
38
- - Render from `store.snapshot()`; remove code paths that read from a central
39
- issue cache.
40
- - Delete dead selectors/helpers that depended on the central cache.
41
- - Verify reconnect flows: a fresh `snapshot` (rev 1) replaces state cleanly.
42
-
43
- ## Notes
44
-
45
- - No telemetry or phased rollout was implemented; ensure the UI and server are
46
- updated together. Older clients will not function with the new server.
47
- - For closed‑issues feeds, prefer passing a `since` param where applicable to
48
- keep snapshots small.
49
-
50
- ## References
51
-
52
- - `docs/protocol/issues-push-v2.md`
53
- - `docs/subscription-issue-store.md`
54
- - ADR 002 — Per‑Subscription Stores and Full‑Issue Push
@@ -1,179 +0,0 @@
1
- # Subscription Push Protocol — per‑subscription full‑issue envelopes (Breaking)
2
-
3
- ```
4
- Date: 2025-10-26
5
- Status: Implemented
6
- Owner: agent
7
- ```
8
-
9
- This document specifies the push‑only protocol used by beads‑ui to deliver list
10
- updates from the local server to the client. It replaces the legacy
11
- notify‑then‑fetch model. There is no version negotiation or fallback.
12
-
13
- ## Overview
14
-
15
- - Transport: single WebSocket connection per client
16
- - Encoding: JSON text frames
17
- - Subscriptions: one client‑chosen `id` per active list subscription
18
- - Delivery: per‑subscription envelopes with full issue payloads
19
- - Messages: `snapshot` | `upsert` | `delete`
20
- - Ordering: strictly increasing `revision` per subscription key and connection
21
-
22
- ## Envelopes
23
-
24
- ```ts
25
- export type SnapshotEnvelope = {
26
- type: 'snapshot';
27
- id: string; // client subscription id
28
- revision: number; // starts at 1 and increments per envelope
29
- issues: Issue[]; // full list for this subscription
30
- };
31
-
32
- export type UpsertEnvelope = {
33
- type: 'upsert';
34
- id: string;
35
- revision: number;
36
- issue: Issue; // full issue payload
37
- };
38
-
39
- export type DeleteEnvelope = {
40
- type: 'delete';
41
- id: string;
42
- revision: number;
43
- issue_id: string; // id only
44
- };
45
- ```
46
-
47
- Notes
48
-
49
- - Server serializes refresh runs per subscription key and emits envelopes in
50
- `revision` order. Clients MUST ignore any envelope with `revision <=` the last
51
- applied for the same `id`.
52
- - Clients SHOULD treat `upsert` as idempotent and MAY additionally guard on an
53
- `issue.updated_at` timestamp to ignore stale updates racing with local state.
54
-
55
- ## Handshake (subscribe‑list)
56
-
57
- Client subscribes to a list with a chosen `id`, a `type`, and optional `params`.
58
-
59
- Client → Server
60
-
61
- ```json
62
- {
63
- "id": "req-1",
64
- "type": "subscribe-list",
65
- "payload": { "id": "ready", "type": "ready-issues" }
66
- }
67
- ```
68
-
69
- Server → Client
70
-
71
- ```json
72
- {
73
- "id": "req-1",
74
- "ok": true,
75
- "type": "subscribe-list",
76
- "payload": { "id": "ready", "key": "ready-issues:{}" }
77
- }
78
- ```
79
-
80
- Immediately after the ack, the server sends a `snapshot` envelope containing the
81
- full list for that subscription `id` with `revision: 1`.
82
-
83
- ```json
84
- {
85
- "id": "evt-1730000000000",
86
- "ok": true,
87
- "type": "snapshot",
88
- "payload": {
89
- "type": "snapshot",
90
- "id": "ready",
91
- "revision": 1,
92
- "issues": [{ "id": "UI-1", "title": "..." }]
93
- }
94
- }
95
- ```
96
-
97
- ## Updates
98
-
99
- Subsequent refreshes emit `upsert` and `delete` envelopes as the list changes.
100
-
101
- ```json
102
- {
103
- "id": "evt-1730000000100",
104
- "ok": true,
105
- "type": "upsert",
106
- "payload": {
107
- "type": "upsert",
108
- "id": "ready",
109
- "revision": 2,
110
- "issue": { "id": "UI-2", "status": "in_progress" }
111
- }
112
- }
113
- ```
114
-
115
- ```json
116
- {
117
- "id": "evt-1730000000200",
118
- "ok": true,
119
- "type": "delete",
120
- "payload": {
121
- "type": "delete",
122
- "id": "ready",
123
- "revision": 3,
124
- "issue_id": "UI-9"
125
- }
126
- }
127
- ```
128
-
129
- ## Reconnect Behavior
130
-
131
- - On reconnect, clients resubscribe using the same `id` values as needed. The
132
- server treats this as a new connection and sends a fresh `snapshot` with
133
- `revision: 1` for each active subscription.
134
-
135
- ## Diagrams
136
-
137
- ```mermaid
138
- sequenceDiagram
139
- participant C as Client
140
- participant S as Server
141
- C->>S: subscribe-list { id, type, params }
142
- S-->>C: ack { id, key }
143
- S-->>C: snapshot { id, schema, revision:1, issues:[...] }
144
- S-->>C: upsert/delete { id, schema, revision:n, ... }
145
- ```
146
-
147
- ```mermaid
148
- stateDiagram-v2
149
- [*] --> Idle
150
- Idle --> Subscribed: subscribe-list(id)
151
- Subscribed --> Subscribed: snapshot(rev=1)
152
- Subscribed --> Subscribed: upsert/delete(rev++)
153
- Subscribed --> Idle: unsubscribe-list(id) / disconnect
154
- ```
155
-
156
- ## Client Responsibilities
157
-
158
- - Maintain one `SubscriptionIssueStore` per active subscription `id`.
159
- - Apply envelopes strictly in `revision` order; ignore stale revisions.
160
- - Render list components from `store.snapshot()` (deterministic order).
161
- - Dispose stores on route/tab changes.
162
-
163
- Detail view
164
-
165
- - Detail pages use the same mechanism with a single‑item subscription, e.g.
166
- `{ type: 'issue-detail', params: { id: 'UI-1' } }` under a client id like
167
- `detail:UI-1`. The server returns a one‑element list for `snapshot` and
168
- `upsert` events.
169
-
170
- ## Rollout and Compatibility
171
-
172
- - Breaking change: no flags and no compatibility layer with the legacy
173
- notify‑then‑fetch flow. Update both client and server together.
174
-
175
- ## See Also
176
-
177
- - ADR 002 — Per‑Subscription Stores and Full‑Issue Push
178
- - `docs/data-exchange-subscription-plan.md` (server refresh and publish model)
179
- - `docs/subscription-issue-store.md` (store API and usage examples)
@@ -1,112 +0,0 @@
1
- # SubscriptionIssueStore — API and Usage Examples
2
-
3
- ```
4
- Date: 2025-10-26
5
- Status: Implemented
6
- Owner: agent
7
- ```
8
-
9
- The `SubscriptionIssueStore` is a per‑subscription in‑memory store that owns the
10
- issues for a single list subscription. It applies server push envelopes
11
- (`snapshot`/`upsert`/`delete`) in revision order and exposes a deterministic,
12
- read‑only snapshot for rendering.
13
-
14
- See also: `docs/protocol/issues-push-v2.md` for the wire protocol.
15
-
16
- ## API
17
-
18
- Factory: `app/data/subscription-issue-store.js`
19
-
20
- ```js
21
- import { createSubscriptionIssueStore } from '../app/data/subscription-issue-store.js';
22
-
23
- // Create at view mount (id is client-chosen)
24
- const store = createSubscriptionIssueStore('ready');
25
-
26
- // Listen for changes
27
- const unsubscribe = store.subscribe(() => {
28
- render(store.snapshot());
29
- });
30
-
31
- // Apply push envelopes from the WebSocket client
32
- ws.on('message', (evt) => {
33
- const msg = JSON.parse(evt.data);
34
- if (msg && msg.ok === true && msg.payload && msg.payload.id === 'ready') {
35
- // payload has { type, id, schema, revision, ... }
36
- store.applyPush(msg.payload);
37
- }
38
- });
39
-
40
- // Read helpers
41
- store.size(); // number of issues
42
- store.getById('UI-1'); // lookup by id
43
-
44
- // Dispose on unmount
45
- unsubscribe();
46
- store.dispose();
47
- ```
48
-
49
- Options: deterministic sort can be customized per list via the optional
50
- `{ sort(a,b) }` parameter when constructing the store.
51
-
52
- ## Subscribing to a List
53
-
54
- Pair store creation with the subscribe‑list handshake. The server will send a
55
- `snapshot` immediately after the ack, followed by `upsert`/`delete`.
56
-
57
- ```js
58
- // Request a subscription
59
- socket.send(
60
- JSON.stringify({
61
- id: 'req-1',
62
- type: 'subscribe-list',
63
- payload: { id: 'ready', type: 'ready-issues' }
64
- })
65
- );
66
-
67
- socket.addEventListener('message', (ev) => {
68
- const frame = JSON.parse(ev.data);
69
- if (frame.ok && frame.type === 'snapshot' && frame.payload.id === 'ready') {
70
- store.applyPush(frame.payload);
71
- }
72
- if (frame.ok && frame.type === 'upsert' && frame.payload.id === 'ready') {
73
- store.applyPush(frame.payload);
74
- }
75
- if (frame.ok && frame.type === 'delete' && frame.payload.id === 'ready') {
76
- store.applyPush(frame.payload);
77
- }
78
- });
79
- ```
80
-
81
- ## Rendering Pattern (List component)
82
-
83
- ```js
84
- /** @param {{ store: ReturnType<typeof createSubscriptionIssueStore> }} props */
85
- export function ListView({ store }) {
86
- let items = store.snapshot();
87
-
88
- const un = store.subscribe(() => {
89
- items = store.snapshot();
90
- requestRender();
91
- });
92
-
93
- // framework-specific teardown
94
- onUnmount(() => un());
95
-
96
- return html`<ul>
97
- ${items.map((it) => html`<li data-id=${it.id}>${it.title}</li>`)}
98
- </ul>`;
99
- }
100
- ```
101
-
102
- ## Ordering and Identity
103
-
104
- - Default sort: priority asc, then `created_at` desc, then id asc.
105
- - When upserting, the store preserves object identity for existing ids by
106
- mutating fields in place. This reduces unnecessary re‑renders.
107
-
108
- ## Reconnects
109
-
110
- - On reconnect, repeat the subscribe‑list call. The server sends a fresh
111
- `snapshot` with `revision: 1`. The store ignores stale envelopes using the
112
- `revision` guard.