@possumtech/rummy 2.1.0 → 2.2.1

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 (140) hide show
  1. package/.env.example +40 -15
  2. package/.xai.key +1 -0
  3. package/PLUGINS.md +169 -53
  4. package/README.md +38 -32
  5. package/SPEC.md +366 -179
  6. package/bin/digest.js +1097 -0
  7. package/biome/no-fallbacks.grit +2 -2
  8. package/gemini.key +1 -0
  9. package/lang/en.json +10 -1
  10. package/migrations/001_initial_schema.sql +9 -2
  11. package/package.json +19 -8
  12. package/service.js +1 -0
  13. package/src/agent/AgentLoop.js +76 -26
  14. package/src/agent/ContextAssembler.js +2 -0
  15. package/src/agent/Entries.js +238 -60
  16. package/src/agent/ProjectAgent.js +44 -0
  17. package/src/agent/TurnExecutor.js +99 -30
  18. package/src/agent/XmlParser.js +206 -111
  19. package/src/agent/errors.js +35 -0
  20. package/src/agent/known_queries.sql +1 -1
  21. package/src/agent/known_store.sql +3 -42
  22. package/src/agent/materializeContext.js +30 -1
  23. package/src/agent/runs.sql +8 -18
  24. package/src/agent/tokens.js +0 -1
  25. package/src/agent/turns.sql +1 -0
  26. package/src/hooks/Hooks.js +26 -0
  27. package/src/hooks/RummyContext.js +12 -1
  28. package/src/lib/hedberg/README.md +60 -0
  29. package/src/lib/hedberg/hedberg.js +60 -0
  30. package/src/lib/hedberg/marker.js +158 -0
  31. package/src/{plugins → lib}/hedberg/matcher.js +1 -2
  32. package/src/llm/LlmProvider.js +41 -3
  33. package/src/llm/openaiStream.js +17 -0
  34. package/src/plugins/ask_user/ask_user.js +12 -2
  35. package/src/plugins/ask_user/ask_userDoc.md +1 -5
  36. package/src/plugins/budget/README.md +29 -24
  37. package/src/plugins/budget/budget.js +166 -110
  38. package/src/plugins/cli/README.md +3 -4
  39. package/src/plugins/cli/cli.js +31 -5
  40. package/src/plugins/cloudflare/cloudflare.js +136 -0
  41. package/src/plugins/cp/cp.js +41 -4
  42. package/src/plugins/cp/cpDoc.md +5 -6
  43. package/src/plugins/engine/engine.sql +1 -1
  44. package/src/plugins/env/README.md +5 -4
  45. package/src/plugins/env/env.js +7 -4
  46. package/src/plugins/env/envDoc.md +7 -8
  47. package/src/plugins/error/error.js +56 -15
  48. package/src/plugins/file/README.md +12 -3
  49. package/src/plugins/file/file.js +2 -2
  50. package/src/plugins/get/get.js +59 -36
  51. package/src/plugins/get/getDoc.md +10 -34
  52. package/src/plugins/google/google.js +115 -0
  53. package/src/plugins/hedberg/hedberg.js +13 -56
  54. package/src/plugins/helpers.js +66 -12
  55. package/src/plugins/index.js +1 -2
  56. package/src/plugins/instructions/README.md +44 -47
  57. package/src/plugins/instructions/instructions-system.md +44 -0
  58. package/src/plugins/instructions/instructions-user.md +53 -0
  59. package/src/plugins/instructions/instructions.js +58 -189
  60. package/src/plugins/known/README.md +6 -7
  61. package/src/plugins/known/known.js +24 -30
  62. package/src/plugins/log/log.js +41 -32
  63. package/src/plugins/mv/mv.js +40 -1
  64. package/src/plugins/mv/mvDoc.md +1 -8
  65. package/src/plugins/ollama/ollama.js +4 -3
  66. package/src/plugins/openai/openai.js +4 -3
  67. package/src/plugins/openrouter/openrouter.js +14 -4
  68. package/src/plugins/persona/README.md +11 -13
  69. package/src/plugins/persona/default.md +29 -0
  70. package/src/plugins/persona/persona.js +10 -66
  71. package/src/plugins/policy/policy.js +23 -22
  72. package/src/plugins/prompt/README.md +37 -27
  73. package/src/plugins/prompt/prompt.js +13 -19
  74. package/src/plugins/rm/rm.js +18 -0
  75. package/src/plugins/rm/rmDoc.md +5 -6
  76. package/src/plugins/rpc/rpc.js +3 -3
  77. package/src/plugins/set/set.js +205 -323
  78. package/src/plugins/set/setDoc.md +47 -17
  79. package/src/plugins/sh/README.md +6 -5
  80. package/src/plugins/sh/sh.js +8 -5
  81. package/src/plugins/sh/shDoc.md +7 -8
  82. package/src/plugins/skill/README.md +37 -14
  83. package/src/plugins/skill/skill.js +200 -101
  84. package/src/plugins/skill/skillDoc.js +3 -0
  85. package/src/plugins/skill/skillDoc.md +9 -0
  86. package/src/plugins/stream/README.md +7 -6
  87. package/src/plugins/stream/finalize.js +100 -0
  88. package/src/plugins/stream/stream.js +13 -45
  89. package/src/plugins/telemetry/telemetry.js +27 -4
  90. package/src/plugins/think/think.js +2 -3
  91. package/src/plugins/think/thinkDoc.md +2 -4
  92. package/src/plugins/unknown/README.md +1 -1
  93. package/src/plugins/unknown/unknown.js +17 -19
  94. package/src/plugins/update/update.js +4 -51
  95. package/src/plugins/update/updateDoc.md +21 -6
  96. package/src/plugins/xai/xai.js +68 -102
  97. package/src/plugins/yolo/yolo.js +102 -75
  98. package/src/sql/functions/hedmatch.js +1 -1
  99. package/src/sql/functions/hedreplace.js +1 -1
  100. package/src/sql/functions/hedsearch.js +1 -1
  101. package/src/sql/functions/slugify.js +16 -2
  102. package/BENCH_ENVIRONMENT.md +0 -230
  103. package/CLIENT_INTERFACE.md +0 -396
  104. package/last_run.txt +0 -5617
  105. package/scriptify/ask_run.js +0 -77
  106. package/scriptify/cache_probe.js +0 -66
  107. package/scriptify/cache_probe_grok.js +0 -74
  108. package/src/agent/budget.js +0 -33
  109. package/src/agent/config.js +0 -38
  110. package/src/plugins/hedberg/README.md +0 -71
  111. package/src/plugins/hedberg/docs.md +0 -0
  112. package/src/plugins/hedberg/edits.js +0 -55
  113. package/src/plugins/hedberg/normalize.js +0 -17
  114. package/src/plugins/hedberg/sed.js +0 -49
  115. package/src/plugins/instructions/instructions.md +0 -34
  116. package/src/plugins/instructions/instructions_104.md +0 -8
  117. package/src/plugins/instructions/instructions_105.md +0 -39
  118. package/src/plugins/instructions/instructions_106.md +0 -22
  119. package/src/plugins/instructions/instructions_107.md +0 -17
  120. package/src/plugins/instructions/instructions_108.md +0 -0
  121. package/src/plugins/known/knownDoc.js +0 -3
  122. package/src/plugins/known/knownDoc.md +0 -8
  123. package/src/plugins/unknown/unknownDoc.js +0 -3
  124. package/src/plugins/unknown/unknownDoc.md +0 -11
  125. package/turns/cli_1777462658211/turn_001.txt +0 -772
  126. package/turns/cli_1777462658211/turn_002.txt +0 -606
  127. package/turns/cli_1777462658211/turn_003.txt +0 -667
  128. package/turns/cli_1777462658211/turn_004.txt +0 -297
  129. package/turns/cli_1777462658211/turn_005.txt +0 -301
  130. package/turns/cli_1777462658211/turn_006.txt +0 -262
  131. package/turns/cli_1777465095132/turn_001.txt +0 -715
  132. package/turns/cli_1777465095132/turn_002.txt +0 -236
  133. package/turns/cli_1777465095132/turn_003.txt +0 -287
  134. package/turns/cli_1777465095132/turn_004.txt +0 -694
  135. package/turns/cli_1777465095132/turn_005.txt +0 -422
  136. package/turns/cli_1777465095132/turn_006.txt +0 -365
  137. package/turns/cli_1777465095132/turn_007.txt +0 -885
  138. package/turns/cli_1777465095132/turn_008.txt +0 -1277
  139. package/turns/cli_1777465095132/turn_009.txt +0 -736
  140. /package/src/{plugins → lib}/hedberg/patterns.js +0 -0
@@ -1,396 +0,0 @@
1
- # CLIENT_INTERFACE
2
-
3
- Wire-protocol contract for any client that drives a rummy server (nvim,
4
- CLI, tbench harness, future GUIs). Pulse + query model: the server
5
- emits a content-free `run/changed` notification when entries land;
6
- clients reconcile against the entry store on demand.
7
-
8
- The entry store is the only source of truth for run progress. The
9
- server tells the client *that* something changed — never *what*.
10
-
11
- ---
12
-
13
- ## TL;DR
14
-
15
- 1. Connect a JSON-RPC websocket; `rummy/hello` to register the project.
16
- 2. `set run://...` (or omit alias) to start a run; you receive `{ alias }`.
17
- 3. Subscribe to **`run/changed`** pulses (notification from server).
18
- 4. On each pulse, call **`getEntries(run, { since, pattern })`** to fetch
19
- what's new. The reply is a flat list of insertion-ordered entries.
20
- Pass `withBody: true` if you want the body inline (otherwise omit and
21
- pull bodies via `getRun` or per-row).
22
- 5. Track the highest `id` you've seen per run; pass it as `since` next pulse.
23
- 6. Resolve any `state: "proposed"` entries by writing back via `set`
24
- with `state: "resolved" | "cancelled" | "failed"`.
25
- 7. Drive UI from the entry stream + `runs.status`; the run is complete
26
- when its row reaches a terminal status (200/204/413/422/499/500).
27
-
28
- No typed payloads. No "render this widget" hints. The store is the
29
- narrative; the client decides what to show.
30
-
31
- ---
32
-
33
- ## 1. Connection & handshake
34
-
35
- WebSocket JSON-RPC 2.0. Default port `3044`. Send:
36
-
37
- ```json
38
- { "jsonrpc": "2.0", "method": "rummy/hello", "params": {
39
- "name": "my-client", "projectRoot": "/abs/path",
40
- "clientVersion": "2.0.0"
41
- }, "id": 1 }
42
- ```
43
-
44
- Reply: `{ rummyVersion, projectId, projectRoot }`. The server enforces
45
- **MAJOR-version match** between client and server protocol versions and
46
- rejects on mismatch.
47
-
48
- After `rummy/hello`, every subsequent RPC carries the project context
49
- implicitly — the server knows which project this socket belongs to.
50
-
51
- ---
52
-
53
- ## 2. Starting a run
54
-
55
- ```json
56
- { "method": "set", "params": {
57
- "path": "run://",
58
- "body": "Write a brief OC_RIVERS.md ...",
59
- "attributes": { "model": "fast", "mode": "act", "yolo": false }
60
- } }
61
- ```
62
-
63
- The server returns `{ alias }` immediately and kicks off the run async.
64
- `mode` is `"ask"` or `"act"`. `yolo: true` opts out of client proposal
65
- resolution (server auto-accepts everything and materializes file edits
66
- to disk).
67
-
68
- Aliases are formatted `<modelAlias>_<unixMs>` (e.g.
69
- `gfast_1777422716094`). The format is **not a stable public contract**
70
- — treat the alias as an opaque string and don't parse it. To recover
71
- the model, read `runs.model` via `getRun`.
72
-
73
- To **cancel**:
74
- ```json
75
- { "method": "set", "params": {
76
- "path": "run://gfast_1777422716094", "state": "cancelled"
77
- } }
78
- ```
79
-
80
- To **inject a continuation prompt** into an existing run, write to its
81
- `run://` path with a `body` and `attributes.mode`. To **fork** a run,
82
- include `attributes.fork: true`.
83
-
84
- ---
85
-
86
- ## 3. The `run/changed` pulse
87
-
88
- The server emits this notification any time an entry write occurs in
89
- the project. Payload is intentionally minimal:
90
-
91
- ```json
92
- { "method": "run/changed", "params": {
93
- "run": "gfast_1777422716094",
94
- "runId": 42,
95
- "path": "log://turn_3/set/notes.md",
96
- "changeType": "insert"
97
- } }
98
- ```
99
-
100
- The pulse is **content-free** — it does not carry the entry body, the
101
- run status, telemetry, or render hints. It is only a hint that the
102
- store has moved. Treat it as a debounce signal.
103
-
104
- **Identifiers.** Both `run` (string alias, what you pass to other
105
- RPCs) and `runId` (integer, the SQLite primary key) are included.
106
- Clients should key UI state by `run`. `runId` is informational and may
107
- be useful when multi-tenancy / cross-project bookkeeping requires a
108
- globally unique key, but you never pass it back to the server.
109
-
110
- **Delivery.** Pulses are best-effort and may be coalesced server-side
111
- during burst writes. Use `since` (§4) for catch-up — a missed pulse is
112
- recovered on the next reliable pulse by `getEntries(run, { since })`
113
- returning every entry that landed in the gap. Do not assume one pulse
114
- per write.
115
-
116
- ---
117
-
118
- ## 4. Reconciling via `getEntries`
119
-
120
- After receiving (or coalescing) one or more pulses for a given run,
121
- query for the diff:
122
-
123
- ```json
124
- { "method": "getEntries", "params": {
125
- "run": "gfast_1777422716094",
126
- "pattern": "**",
127
- "since": 1234,
128
- "limit": 200,
129
- "withBody": false
130
- } }
131
- ```
132
-
133
- **Parameters:**
134
-
135
- - **`run`** — alias from `rummy/hello`-then-`set run://`.
136
- - **`pattern`** — glob over entry path. Default `"*"`. Use `"**"` to
137
- mirror everything, `"log://**"` for the audit trail, etc.
138
- - **`since`** — the highest `id` you've already processed for this run
139
- (or `0` / omit on first call). Server returns only entries with
140
- `id > since`, ordered by `id` ASC (insertion order).
141
- - **`limit`** — cap result count; chunk catch-up by re-querying with
142
- the new high-water mark.
143
- - **`scheme`**, **`state`**, **`visibility`** — exact-match filters.
144
- - **`bodyFilter`** — substring/glob match against entry body content;
145
- filters which **rows** are returned by their body. *Not* a body-
146
- inclusion knob — for that, see `withBody`.
147
- - **`withBody`** — when `true`, each returned row carries `body`
148
- inline. Default `false` to keep pulse-reconcile traffic lean.
149
-
150
- **Returned row shape:**
151
-
152
- | Field | Type | Notes |
153
- |--------------|--------------------------------|-------------------------------------------------|
154
- | `id` | integer | Monotonic insertion id; the `since` cursor key. |
155
- | `path` | string | URI-encoded; e.g. `log://turn_1/update/done`. |
156
- | `scheme` | string \| null | URI scheme of `path`, or `null` for bare files.|
157
- | `state` | string | `proposed` / `resolved` / `cancelled` / `failed` / `streaming`. |
158
- | `outcome` | string \| null | Free-form outcome label (e.g. `not_found`). |
159
- | `visibility` | string | `visible` / `summarized` / `archived`. |
160
- | `turn` | integer | Turn this entry was written in. |
161
- | `tokens` | integer | `countTokens(body)` — included even when body isn't. |
162
- | `attributes` | object | Always parsed JSON object (never a string). |
163
- | `body` | string (only with `withBody`) | Full entry body. Omitted by default. |
164
-
165
- **Important:** when `since` is set, results are ordered by `id` ASC
166
- (insertion order — what catch-up streams want). When `since` is
167
- omitted, results are ordered by `path` ASC (browse mode — what
168
- inventory walks want). Pick the mode that matches your use case.
169
-
170
- ---
171
-
172
- ## 5. Resolving proposals
173
-
174
- Some entries land in `state: "proposed"` — the run is parked waiting
175
- for the client to decide. Examples: file edits (`log://turn_N/set/...`),
176
- shell commands (`log://turn_N/sh/...`), `ask_user` prompts.
177
-
178
- To accept, reject, or fail a proposal, write back through `set`:
179
-
180
- ```json
181
- { "method": "set", "params": {
182
- "run": "gfast_1777422716094",
183
- "path": "log://turn_3/set/notes.md",
184
- "state": "resolved",
185
- "body": ""
186
- } }
187
- ```
188
-
189
- | Resolution | `state` | Meaning |
190
- |------------|--------------|--------------------------------------|
191
- | accept | `resolved` | Apply the proposal; server materializes side effects |
192
- | reject | `cancelled` | Drop the proposal; the run continues |
193
- | error | `failed` | The proposal couldn't be applied; the run aborts |
194
-
195
- For `ask_user` proposals, put the user's answer in `body`.
196
-
197
- The server's response carries `{ status }` reflecting the run's
198
- **current** status (102 mid-run, terminal at completion). Do **not**
199
- treat `status >= 200` from a resolve response as terminal — the run may
200
- still be active. Use the run row's status (via `getRun` or by tracking
201
- pulses) as authoritative.
202
-
203
- If a run was started with `attributes.yolo: true`, you do not need to
204
- register a resolver — the server auto-accepts every proposal
205
- server-side and materializes file edits to disk under `projectRoot`.
206
- For `ask_user` under yolo the server cannot supply a meaningful answer
207
- on the user's behalf; yolo runs that emit `ask_user` proposals park
208
- indefinitely (or until cancelled). Treat `ask_user` + yolo as a client
209
- configuration error.
210
-
211
- ---
212
-
213
- ## 6. Reading bodies and run state
214
-
215
- `getEntries` returns metadata only by default. Two ways to get bodies:
216
-
217
- 1. **`getEntries` with `withBody: true`** — bodies for matched rows
218
- inline. Bandwidth scales with what you query for; bound it with
219
- `pattern` / `limit`.
220
- 2. **`getRun(run)`** — full structured snapshot of one run with
221
- bodies, telemetry, history, latest prompt and summary. Use on
222
- initial open of a run document or after long disconnects.
223
-
224
- ```json
225
- { "method": "getRun", "params": { "run": "gfast_1777422716094" } }
226
- ```
227
-
228
- **`getRun` response shape (pinned):**
229
-
230
- ```json
231
- {
232
- "run": "gfast_1777422716094",
233
- "turn": 4,
234
- "status": 200,
235
- "model": "gfast",
236
- "temperature": null,
237
- "persona": null,
238
- "context_limit": null,
239
- "context": {
240
- "telemetry": {
241
- "prompt_tokens": 1928,
242
- "completion_tokens": 75,
243
- "total_tokens": 2003,
244
- "cost": 0
245
- },
246
- "reasoning": [ { "path": "reasoning://N", "body": "...", "turn": N } ],
247
- "content": [ { "path": "content://N", "body": "...", "turn": N } ],
248
- "history": [ {
249
- "tool": "set",
250
- "path": "log://turn_N/set/notes.md",
251
- "status": 200,
252
- "body": "...",
253
- "attributes": { "action": "set", "status": 200, "...": "..." },
254
- "turn": N
255
- } ]
256
- },
257
- "last_user_prompt": "...",
258
- "last_summary": "..."
259
- }
260
- ```
261
-
262
- - `attributes` on `history` rows is always a parsed object (never a
263
- JSON string).
264
- - `context.reasoning` and `context.content` carry the model's per-turn
265
- reasoning and assistant content respectively. Empty arrays for runs
266
- whose model didn't surface those channels.
267
- - `last_summary` is the body of the most recent `log://turn_N/update/*`
268
- entry. `last_user_prompt` is the body of the most recent `prompt://*`
269
- entry (the active user prompt for this run).
270
-
271
- For incremental updates inside a session, prefer the pulse +
272
- `getEntries` flow over polling `getRun`.
273
-
274
- ---
275
-
276
- ## 7. Terminal detection & telemetry
277
-
278
- A run's `status` field on its row is authoritative. Terminal statuses:
279
- `200, 204, 413, 422, 499, 500`. Any other value is in-flight (typically
280
- `102`).
281
-
282
- **Status updates land at `log://turn_N/update/<slug>` with:**
283
- - `attributes.action = "update"`
284
- - `attributes.status = <int>` — the integer status code (e.g. `145`,
285
- `156`, `167`, `200`)
286
- - `body` — the human-readable summary text
287
-
288
- Latest `update` entry's body is the latest summary. Terminal status is
289
- detected when:
290
- 1. The `runs.status` row reaches a terminal value (read via `getRun`),
291
- **or**
292
- 2. The latest `log://turn_N/update/*` entry's `attributes.status` is in
293
- the terminal set.
294
-
295
- (1) is the authoritative read; (2) is a convenience for UIs that are
296
- already watching the entry stream.
297
-
298
- **Errors land at `log://turn_N/error/<slug>` with:**
299
- - `attributes.action = "error"`
300
- - Body carrying the error detail; `outcome` may carry a short label
301
- (e.g. `not_found`, `validation`).
302
-
303
- There is no separate `update://` or `error://` URI scheme — these are
304
- log channels under the audit trail. Filter via `pattern: "log://**/update/**"`
305
- or `pattern: "log://**/error/**"` if you only want one channel.
306
-
307
- **Per-turn telemetry** (token counts, model alias, cached tokens, etc.)
308
- is in the `turns` table. Surface it by:
309
- - Calling `getRun(run)` and reading `context.telemetry` (aggregated
310
- across all turns of the run), **or**
311
- - Querying the SQLite store directly if your client runs alongside the
312
- server (private optimization, not a wire contract).
313
-
314
- Per-turn breakdowns (rather than aggregated) require a direct DB read
315
- for now; we may add a wire RPC if a need surfaces.
316
-
317
- A common UI pattern:
318
-
319
- - Maintain a `Map<runAlias, lastSeenId>`.
320
- - On `run/changed`: `getEntries(run, { since })`, update `lastSeenId`,
321
- render new entries inline.
322
- - For runs in your foreground UI, periodically (every ~2s, or on
323
- pulse) fetch bodies for newly-arrived rows via `getEntries` with
324
- `withBody: true` filtered by the new `id` range.
325
- - Detect terminal by watching for `attributes.action === "update"`
326
- with `attributes.status` in the terminal set, or by polling
327
- `runs.status` via `getRun` on a low cadence.
328
-
329
- ---
330
-
331
- ## 8. Other notifications
332
-
333
- Beyond `run/changed`, the server emits:
334
-
335
- | Notification | Purpose |
336
- |---------------------|--------------------------------------------------------|
337
- | `ui/render` | **Advisory only.** Streaming model output for live thinking displays. Payload shape and cadence are not part of the wire contract; clients may ignore. The entry stream + bodies is the durable record. |
338
- | `ui/notify` | Toast-level operator messages. `params: { message, level }`. |
339
- | `stream/cancelled` | Server-initiated stream abort; client should kill its local process if it owned the stream. |
340
-
341
- A minimal client can ignore all three and still function — the entry
342
- store carries the durable record.
343
-
344
- ---
345
-
346
- ## 9. Migrating from the typed-notification protocol
347
-
348
- The legacy protocol shipped three typed notifications:
349
-
350
- - `run/state` — fired after each turn with status, summary, history,
351
- unknowns, telemetry.
352
- - `run/progress` — turn-status pings ("thinking", "processing").
353
- - `run/proposal` — pending proposal payload + metadata.
354
-
355
- All three are **gone**. Their information is fully derivable from the
356
- entry store:
357
-
358
- | Old surface | New equivalent |
359
- |-------------------------|------------------------------------------------------------------|
360
- | `run/state.status` | `runs.status` row field (via `getRun`), or latest `log://turn_N/update/*` with `attributes.status` in terminal set |
361
- | `run/state.summary` | latest `log://turn_N/update/*` entry body, or `getRun.last_summary` |
362
- | `run/state.history` | `getEntries(run, { pattern: "log://**" })` |
363
- | `run/state.unknowns` | `getEntries(run, { pattern: "unknown://**" })` |
364
- | `run/state.telemetry` | `getRun(run).context.telemetry` (aggregated) |
365
- | `run/progress` | (drop — pulse cadence is sufficient) |
366
- | `run/proposal.proposed` | `getEntries(run, { state: "proposed", since })` |
367
-
368
- If your client previously closed a document the moment a `run/state`
369
- arrived with `status >= 200`: do **not** apply the same logic to a
370
- resolve-RPC response. Track `runs.status` instead.
371
-
372
- ---
373
-
374
- ## 10. Multi-client semantics
375
-
376
- Multiple clients may connect to the same server simultaneously.
377
- Conflict resolution is **last-write-wins** at the entry-store level —
378
- two clients resolving the same `(run, path)` proposal will both
379
- succeed; the second resolution's `state` and `body` overwrite the
380
- first. The server does not lock or arbitrate.
381
-
382
- For UI safety: implement optimistic local state on resolve, but
383
- re-render from `getEntries` on pulse to absorb any concurrent client's
384
- write. The pulse will always reach you eventually; don't pessimistically
385
- lock.
386
-
387
- ---
388
-
389
- ## 11. Reference
390
-
391
- - Server source of truth: `src/server/ClientConnection.js`,
392
- `src/plugins/rpc/rpc.js`.
393
- - Entry store schema: `migrations/001_initial_schema.sql`.
394
- - Pulse emission point: `hooks.entry.changed` → `run/changed`.
395
- - Protocol version constant: `src/server/protocol.js`
396
- (`RUMMY_PROTOCOL_VERSION`).