@possumtech/rummy 0.2.7 → 0.2.8

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 (59) hide show
  1. package/.env.example +1 -2
  2. package/PLUGINS.md +105 -82
  3. package/migrations/001_initial_schema.sql +53 -68
  4. package/package.json +4 -6
  5. package/service.js +1 -1
  6. package/src/agent/AgentLoop.js +91 -58
  7. package/src/agent/ContextAssembler.js +2 -2
  8. package/src/agent/KnownStore.js +30 -11
  9. package/src/agent/ProjectAgent.js +1 -3
  10. package/src/agent/TurnExecutor.js +119 -31
  11. package/src/agent/XmlParser.js +20 -0
  12. package/src/agent/known_checks.sql +5 -4
  13. package/src/agent/known_queries.sql +4 -3
  14. package/src/agent/known_store.sql +29 -15
  15. package/src/agent/loops.sql +63 -0
  16. package/src/agent/runs.sql +7 -7
  17. package/src/agent/schemes.sql +2 -2
  18. package/src/agent/turns.sql +3 -3
  19. package/src/hooks/PluginContext.js +1 -10
  20. package/src/hooks/RummyContext.js +16 -8
  21. package/src/plugins/ask_user/ask_user.js +3 -2
  22. package/src/plugins/cp/cp.js +7 -7
  23. package/src/plugins/current/current.js +3 -4
  24. package/src/plugins/engine/engine.sql +5 -3
  25. package/src/plugins/engine/turn_context.sql +9 -4
  26. package/src/plugins/env/docs.md +2 -0
  27. package/src/plugins/env/env.js +3 -2
  28. package/src/plugins/file/file.js +9 -19
  29. package/src/plugins/get/docs.md +7 -3
  30. package/src/plugins/get/get.js +22 -6
  31. package/src/plugins/hedberg/docs.md +0 -9
  32. package/src/plugins/hedberg/hedberg.js +2 -5
  33. package/src/plugins/hedberg/matcher.js +1 -1
  34. package/src/plugins/hedberg/patterns.js +6 -6
  35. package/src/plugins/helpers.js +2 -2
  36. package/src/plugins/index.js +28 -15
  37. package/src/plugins/instructions/instructions.js +1 -1
  38. package/src/plugins/known/known.js +9 -11
  39. package/src/plugins/mv/mv.js +7 -7
  40. package/src/plugins/previous/previous.js +6 -5
  41. package/src/plugins/progress/progress.js +6 -0
  42. package/src/plugins/prompt/prompt.js +9 -10
  43. package/src/plugins/rm/docs.md +3 -1
  44. package/src/plugins/rm/rm.js +24 -7
  45. package/src/plugins/rpc/rpc.js +33 -42
  46. package/src/plugins/set/docs.md +3 -1
  47. package/src/plugins/set/set.js +22 -16
  48. package/src/plugins/sh/sh.js +3 -2
  49. package/src/plugins/skills/skills.js +3 -4
  50. package/src/plugins/store/docs.md +2 -1
  51. package/src/plugins/store/store.js +14 -3
  52. package/src/plugins/summarize/summarize.js +1 -1
  53. package/src/plugins/telemetry/telemetry.js +17 -7
  54. package/src/plugins/unknown/unknown.js +3 -2
  55. package/src/plugins/update/update.js +1 -1
  56. package/src/server/ClientConnection.js +3 -5
  57. package/src/sql/v_model_context.sql +20 -23
  58. package/src/sql/v_run_log.sql +3 -3
  59. package/src/agent/prompt_queue.sql +0 -39
package/.env.example CHANGED
@@ -57,7 +57,6 @@ RUMMY_X_TITLE=RUMMY
57
57
  # Web Search
58
58
  # RUMMY_SEARXNG_URL="http://127.0.0.1:8888"
59
59
 
60
- # required for web fetch and search
60
+ # External plugins: npm i -g <package>, then uncomment
61
61
  # RUMMY_PLUGIN_WEB="@possumtech/rummy.web"
62
- # required for git repo and repomap/symbol functionality
63
62
  # RUMMY_PLUGIN_REPO="@possumtech/rummy.repo"
package/PLUGINS.md CHANGED
@@ -8,13 +8,21 @@ constructor receives `core` (a PluginContext) — the plugin's complete
8
8
  interface with the system.
9
9
 
10
10
  ```js
11
+ import { readFileSync } from "node:fs";
12
+
11
13
  export default class MyTool {
12
14
  #core;
13
15
 
14
16
  constructor(core) {
15
17
  this.#core = core;
18
+ core.registerScheme();
16
19
  core.on("handler", this.handler.bind(this));
17
20
  core.on("full", this.full.bind(this));
21
+ core.on("summary", this.summary.bind(this));
22
+ const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
23
+ core.filter("instructions.toolDocs", async (content) =>
24
+ content ? `${content}\n\n${docs}` : docs,
25
+ );
18
26
  }
19
27
 
20
28
  async handler(entry, rummy) {
@@ -22,13 +30,17 @@ export default class MyTool {
22
30
  }
23
31
 
24
32
  full(entry) {
25
- // What the model sees at full fidelity
26
33
  return `# mytool ${entry.path}\n${entry.body}`;
27
34
  }
35
+
36
+ summary(entry) {
37
+ return entry.body;
38
+ }
28
39
  }
29
40
  ```
30
41
 
31
42
  File naming: `src/plugins/mytool/mytool.js`. Class name = file name.
43
+ Tool docs: `src/plugins/mytool/docs.md`.
32
44
 
33
45
  External plugins install via npm and load via `RUMMY_PLUGIN_*` env vars:
34
46
 
@@ -51,18 +63,36 @@ Plugin: rummy.rm({ path: "file.txt" })
51
63
 
52
64
  ## Registration
53
65
 
54
- All registration happens in the constructor via `core.on()` and
55
- `core.filter()`. No static methods. No direct hook manipulation.
66
+ All registration happens in the constructor via `core.on()`,
67
+ `core.filter()`, and `core.registerScheme()`.
68
+
69
+ ### core.registerScheme(config?)
70
+
71
+ Registers this plugin's scheme in the database. Called once in the
72
+ constructor. Defaults are third-party friendly:
73
+
74
+ ```js
75
+ core.registerScheme({
76
+ fidelity: "full", // "full", "turn", or "null"
77
+ modelVisible: 1, // 1 or 0
78
+ validStates: ["full", "proposed", "pass", "rejected", "error"],
79
+ category: "result", // "result", "file", "knowledge", "structural", "audit", "tool"
80
+ });
81
+ ```
82
+
83
+ All fields optional. `core.registerScheme()` with no args gives a
84
+ sensible result-type scheme.
56
85
 
57
86
  ### core.on(event, callback, priority?)
58
87
 
59
88
  | Event | Purpose |
60
89
  |-------|---------|
61
90
  | `"handler"` | Tool handler — called when model/client invokes this tool |
62
- | `"full"` | Full projection — what the model sees at full fidelity |
63
- | `"summary"` | Summary projectioncondensed view under token pressure |
64
- | `"docs"` | Tool documentationincluded in model prompt |
65
- | `"turn"` | Turn processorruns before context materialization |
91
+ | `"full"` | Full fidelity — what the model sees in `<current>` |
92
+ | `"summary"` | Summary fidelitywhat the model sees in `<previous>` |
93
+ | `"turn.started"` | Turn beginningwrite prompt/progress/instructions entries |
94
+ | `"turn.response"` | LLM respondedwrite audit entries, commit usage |
95
+ | `"turn.proposing"` | All dispatches done — materialize file edit proposals |
66
96
  | `"entry.created"` | Entry created during dispatch |
67
97
  | `"entry.changed"` | File entries changed on disk |
68
98
  | Any `"dotted.name"` | Resolves to the matching hook in the hook tree |
@@ -71,12 +101,25 @@ All registration happens in the constructor via `core.on()` and
71
101
 
72
102
  | Filter | Purpose |
73
103
  |--------|---------|
104
+ | `"instructions.toolDocs"` | Append tool documentation to model prompt |
74
105
  | `"assembly.system"` | Contribute to system message |
75
106
  | `"assembly.user"` | Contribute to user message |
76
107
  | `"llm.messages"` | Transform final messages before LLM call |
77
108
  | `"llm.response"` | Transform LLM response |
78
109
  | Any `"dotted.name"` | Resolves to the matching filter in the hook tree |
79
110
 
111
+ ### Tool Docs
112
+
113
+ Each tool plugin has a `docs.md` file with model-facing documentation.
114
+ Registered via the `instructions.toolDocs` filter in the constructor:
115
+
116
+ ```js
117
+ const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
118
+ core.filter("instructions.toolDocs", async (content) =>
119
+ content ? `${content}\n\n${docs}` : docs,
120
+ );
121
+ ```
122
+
80
123
  ### handler(entry, rummy)
81
124
 
82
125
  The handler receives the parsed command entry and a per-turn RummyContext:
@@ -98,23 +141,24 @@ to stop the chain.
98
141
  ### full(entry) / summary(entry)
99
142
 
100
143
  Returns the string the model sees for this tool's entries at the given
101
- fidelity. Called during materialization. Every tool MUST register `full`.
102
- `summary` is optional if unregistered, the entry is invisible at summary
103
- fidelity (no fallback).
144
+ fidelity. `full` renders in `<current>` (active loop). `summary` renders
145
+ in `<previous>` (completed loops). Every tool MUST register `full`.
146
+ `summary` is optional — if unregistered, the entry is empty at summary
147
+ fidelity.
104
148
 
105
149
  ## Two Objects
106
150
 
107
151
  Plugins interact with two objects at different scopes:
108
152
 
109
153
  **PluginContext** (`this.#core`) — startup-scoped. Created once per plugin.
110
- Used for registration (`on()`, `filter()`), database access, store queries.
111
- Lives for the lifetime of the service. This is `rummy.core` — the
112
- plugin-only tier that clients cannot reach.
154
+ Used for registration (`on()`, `filter()`, `registerScheme()`), database
155
+ access, store queries. This is `rummy.core` — the plugin-only tier that
156
+ clients cannot reach.
113
157
 
114
158
  **RummyContext** (`rummy` argument) — turn-scoped. Passed to handlers
115
159
  per-invocation. Has tool verbs, per-turn state (runId, turn, mode).
116
160
 
117
- ### Tool Verbs (available on both objects)
161
+ ### Tool Verbs (on RummyContext)
118
162
 
119
163
  | Method | Effect |
120
164
  |--------|--------|
@@ -146,26 +190,42 @@ per-invocation. Has tool verbs, per-turn state (runId, turn, mode).
146
190
  | `rummy.projectId` | Current project ID |
147
191
  | `rummy.sequence` | Current turn number (RummyContext only) |
148
192
 
149
- ## Events & Filters
193
+ ## Hedberg
150
194
 
151
- **Events** are fire-and-forget. All handlers run. Return values ignored.
195
+ The hedberg plugin exposes pattern matching and interpretation utilities
196
+ on `core.hooks.hedberg` for all plugins to use:
152
197
 
153
198
  ```js
154
- hooks.entry.changed.on(async ({ rummy, runId, turn, paths }) => {
155
- // React to file changes
156
- }, priority);
199
+ const { match, search, replace, parseSed, parseEdits, normalizeAttrs, generatePatch }
200
+ = core.hooks.hedberg;
157
201
  ```
158
202
 
159
- **Filters** transform data through a chain. Each handler receives the value
160
- and context, returns the (possibly modified) value.
203
+ | Method | Purpose |
204
+ |--------|---------|
205
+ | `match(pattern, string)` | Full-string pattern match (glob, regex, literal) |
206
+ | `search(pattern, string)` | Substring search, returns `{ found, match, index }` |
207
+ | `replace(body, search, replacement, opts?)` | Apply replacement (sed → literal → heuristic) |
208
+ | `parseSed(input)` | Parse sed syntax into `[{ search, replace, flags, sed }]` |
209
+ | `parseEdits(content)` | Detect edit format (merge conflict, udiff, Claude XML) |
210
+ | `normalizeAttrs(attrs)` | Heal model attribute names |
211
+ | `generatePatch(path, old, new)` | Generate unified diff |
161
212
 
162
- ```js
163
- hooks.llm.messages.addFilter(async (messages, context) => {
164
- return [{ role: "system", content: "Extra" }, ...messages];
165
- }, priority);
166
- ```
213
+ Pattern types (auto-detected):
214
+
215
+ | Syntax | Type | Example |
216
+ |--------|------|---------|
217
+ | `s/old/new/flags` | Sed replace | `s/3000/8080/g` |
218
+ | `/pattern/flags` | Regex | `/\d+/g` |
219
+ | `$.path` | JSONPath | `$.config.port` |
220
+ | `//element` | XPath | `//div[@class]` |
221
+ | `*glob*` | Glob | `src/**/*.js` |
222
+ | Everything else | Literal | `port = 3000` |
167
223
 
168
- Lower priority runs first. All hooks are async.
224
+ ## Events & Filters
225
+
226
+ **Events** are fire-and-forget. All handlers run. Return values ignored.
227
+ **Filters** transform data through a chain. Lower priority runs first.
228
+ All hooks are async.
169
229
 
170
230
  ### Project Lifecycle
171
231
 
@@ -193,7 +253,7 @@ Lower priority runs first. All hooks are async.
193
253
  | `act.started` | event | `{ projectId, model, prompt, run }` | Run requested in act mode |
194
254
  | `run.config` | filter | Config object, `{ projectId }` | Before run config applied |
195
255
  | `run.progress` | event | `{ run, turn, status }` | Status change (thinking, processing) |
196
- | `run.state` | event | `{ run, turn, status, summary, history, unknowns, proposed, telemetry }` | After each turn — full state snapshot |
256
+ | `run.state` | event | `{ run, turn, status, summary, history, unknowns, proposed, telemetry }` | After each turn |
197
257
  | `run.step.completed` | event | `{ run, turn, flags }` | Turn resolved, no proposals pending |
198
258
  | `ask.completed` | event | `{ projectId, run, status, turn }` | Ask run finished |
199
259
  | `act.completed` | event | `{ projectId, run, status, turn }` | Act run finished |
@@ -204,14 +264,16 @@ Hooks fire in this order every turn:
204
264
 
205
265
  | Hook | Type | Payload | When |
206
266
  |------|------|---------|------|
207
- | `entry.changed` | event | `{ rummy, runId, turn, paths }` | Files changed on disk since last turn |
267
+ | `turn.started` | event | `{ rummy, mode, prompt, isContinuation }` | Plugins write prompt/progress/instructions entries |
268
+ | `entry.changed` | event | `{ rummy, runId, turn, paths }` | Files changed on disk (repo plugin) |
208
269
  | `onTurn` | processor | `(rummy)` | Plugin turn setup, before context assembly |
209
- | `assembly.system` | filter | `(content, { rows, loopStartTurn, type, tools })` | Build system message — plugins append sections |
210
- | `assembly.user` | filter | `(content, { rows, loopStartTurn, type, tools })` | Build user message — plugins append sections |
211
- | `llm.messages` | filter | `messages[], { model, projectId, runId }` | Before LLM call — modify final messages |
270
+ | `assembly.system` | filter | `(content, { rows, loopStartTurn, type, contextSize })` | Build system message |
271
+ | `assembly.user` | filter | `(content, { rows, loopStartTurn, type, contextSize })` | Build user message |
272
+ | `llm.messages` | filter | `messages[], { model, projectId, runId }` | Before LLM call |
212
273
  | `llm.request.started` | event | `{ model, turn }` | LLM call about to fire |
213
- | `llm.response` | filter | `response, { model, projectId, runId }` | Raw LLM response — normalize, transform |
274
+ | `llm.response` | filter | `response, { model, projectId, runId }` | Raw LLM response |
214
275
  | `llm.request.completed` | event | `{ model, turn, usage }` | LLM call finished |
276
+ | `turn.response` | event | `{ rummy, turn, result, responseMessage, content, ... }` | Plugins write audit entries |
215
277
  | `tools.dispatch` | handler | `(entry, rummy)` | Per command — handler chain executes |
216
278
  | `entry.created` | event | `{ scheme, path, body, attributes, state, resultPath }` | After each command dispatched |
217
279
  | `turn.proposing` | event | `{ rummy, recorded }` | All dispatches done — materialize proposals |
@@ -223,46 +285,6 @@ Hooks fire in this order every turn:
223
285
  | `ui.render` | event | `{ text, append }` | Text for client display |
224
286
  | `ui.notify` | event | `{ text, level }` | Status notification |
225
287
 
226
- ## RPC Registration
227
-
228
- ```js
229
- hooks.rpc.registry.register("myMethod", {
230
- handler: async (params, ctx) => {
231
- // ctx.projectAgent, ctx.db, ctx.projectId, ctx.projectRoot
232
- return { result: "value" };
233
- },
234
- description: "What this method does",
235
- params: { arg1: "description" },
236
- requiresInit: true,
237
- longRunning: true, // for methods that call the model
238
- });
239
- ```
240
-
241
- ## Hedberg Pattern Library
242
-
243
- Available in JS and SQL. Five pattern types, auto-detected:
244
-
245
- | Syntax | Type | Example |
246
- |--------|------|---------|
247
- | `s/old/new/flags` | Sed replace | `s/3000/8080/g` |
248
- | `/pattern/flags` | Regex | `/\d+/g` |
249
- | `$.path` | JSONPath | `$.config.port` |
250
- | `//element` | XPath | `//div[@class]` |
251
- | `*glob*` | Glob | `src/**/*.js` |
252
- | Everything else | Literal | `port = 3000` |
253
-
254
- JS API:
255
-
256
- ```js
257
- import { hedmatch, hedsearch, hedreplace } from "./sql/functions/hedberg.js";
258
-
259
- hedmatch(pattern, string) // → boolean (full string match)
260
- hedsearch(pattern, string) // → { found, match, index }
261
- hedreplace(pattern, replacement, string) // → new string or null
262
- ```
263
-
264
- SQL functions: `hedmatch()`, `hedsearch()`, `hedreplace()`.
265
-
266
288
  ## Bundled Plugins
267
289
 
268
290
  Each plugin has its own README at `src/plugins/{name}/README.md`.
@@ -271,7 +293,7 @@ Each plugin has its own README at `src/plugins/{name}/README.md`.
271
293
  |--------|------|-------------|
272
294
  | [`get`](src/plugins/get/) | Core tool | Load file/entry into context |
273
295
  | [`set`](src/plugins/set/) | Core tool | Edit file/entry |
274
- | [`known`](src/plugins/known/) | Core tool | Save knowledge, render `<known>` |
296
+ | [`known`](src/plugins/known/) | Core tool + Assembly | Save knowledge, render `<knowns>` section |
275
297
  | [`store`](src/plugins/store/) | Core tool | Remove from context |
276
298
  | [`rm`](src/plugins/rm/) | Core tool | Delete permanently |
277
299
  | [`mv`](src/plugins/mv/) | Core tool | Move entry |
@@ -281,22 +303,23 @@ Each plugin has its own README at `src/plugins/{name}/README.md`.
281
303
  | [`ask_user`](src/plugins/ask_user/) | Core tool | Ask the user |
282
304
  | [`summarize`](src/plugins/summarize/) | Structural | Signal completion |
283
305
  | [`update`](src/plugins/update/) | Structural | Signal continued work |
284
- | [`unknown`](src/plugins/unknown/) | Structural | Register unknowns, render `<unknowns>` |
306
+ | [`unknown`](src/plugins/unknown/) | Structural + Assembly | Register unknowns, render `<unknowns>` |
285
307
  | [`previous`](src/plugins/previous/) | Assembly | Render `<previous>` loop history |
286
308
  | [`current`](src/plugins/current/) | Assembly | Render `<current>` active loop work |
287
- | [`progress`](src/plugins/progress/) | Assembly | Render `<progress>` bridge text |
309
+ | [`progress`](src/plugins/progress/) | Assembly | Render `<progress>` telemetry + bridge |
288
310
  | [`prompt`](src/plugins/prompt/) | Assembly | Render `<ask>`/`<act>` prompt tag |
289
- | [`instructions`](src/plugins/instructions/) | Internal | System prompt assembly |
290
- | [`file`](src/plugins/file/) | Internal | File projections, constraints, scanning |
311
+ | [`hedberg`](src/plugins/hedberg/) | Utility | Pattern matching, interpretation, normalization |
312
+ | [`instructions`](src/plugins/instructions/) | Internal | Preamble + tool docs + persona assembly |
313
+ | [`file`](src/plugins/file/) | Internal | File entry projections and constraints |
291
314
  | [`rpc`](src/plugins/rpc/) | Internal | RPC method registration |
292
315
  | [`skills`](src/plugins/skills/) | Internal | Skill/persona management |
293
- | [`telemetry`](src/plugins/telemetry/) | Internal | Debug logging |
316
+ | [`telemetry`](src/plugins/telemetry/) | Internal | Audit entries, usage stats, last_run.txt |
294
317
 
295
318
  ## External Plugins
296
319
 
297
320
  | Plugin | Package | Description |
298
321
  |--------|---------|-------------|
299
- | Web | `@possumtech/rummy.web` | Search and URL fetching |
300
- | Repo | `@possumtech/rummy.repo` | Symbol extraction |
322
+ | Repo | `@possumtech/rummy.repo` | Git-aware file scanning and symbol extraction |
323
+ | Web | `@possumtech/rummy.web` | Web search and URL fetching via searxng |
301
324
 
302
- Loaded via `RUMMY_PLUGIN_*` env vars. Graceful failure if not installed.
325
+ Loaded via `RUMMY_PLUGIN_*` env vars.
@@ -4,13 +4,12 @@ PRAGMA mmap_size = $mmap_size;
4
4
  -- INIT: initial_schema
5
5
 
6
6
  -- Scheme registry: single source of truth for all scheme metadata.
7
- -- fidelity: 'full' = always visible, 'turn' = visible when turn>0, 'null' = never visible.
8
- -- valid_states: JSON array of allowed state values for this scheme.
7
+ -- Status codes are HTTP: 2xx success, 3xx redirect, 4xx model error, 5xx system error.
8
+ -- No valid_states HTTP semantics are universal.
9
+ -- No fidelity — entries don't decide their own importance.
9
10
  CREATE TABLE IF NOT EXISTS schemes (
10
11
  name TEXT PRIMARY KEY
11
- , fidelity TEXT NOT NULL CHECK (fidelity IN ('full', 'turn', 'null'))
12
12
  , model_visible BOOLEAN NOT NULL DEFAULT 1
13
- , valid_states TEXT NOT NULL
14
13
  , category TEXT
15
14
  );
16
15
 
@@ -37,15 +36,14 @@ CREATE TABLE IF NOT EXISTS models (
37
36
  );
38
37
 
39
38
  -- Runs: execution units belonging to a project.
40
- -- Each run has its own config (temperature, persona, context_limit).
39
+ -- Status uses HTTP codes: 100=queued, 102=running, 200=completed,
40
+ -- 202=proposed, 500=failed, 499=aborted.
41
41
  CREATE TABLE IF NOT EXISTS runs (
42
42
  id INTEGER PRIMARY KEY AUTOINCREMENT
43
43
  , project_id INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE
44
44
  , parent_run_id INTEGER REFERENCES runs (id) ON DELETE SET NULL
45
45
  , model TEXT
46
- , status TEXT NOT NULL DEFAULT 'queued' CHECK (
47
- status IN ('queued', 'running', 'proposed', 'completed', 'failed', 'aborted')
48
- )
46
+ , status INTEGER NOT NULL DEFAULT 100 CHECK (status BETWEEN 100 AND 599)
49
47
  , alias TEXT NOT NULL UNIQUE
50
48
  , temperature REAL CHECK (
51
49
  temperature IS NULL OR (temperature >= 0 AND temperature <= 2)
@@ -53,16 +51,39 @@ CREATE TABLE IF NOT EXISTS runs (
53
51
  , persona TEXT
54
52
  , context_limit INTEGER CHECK (context_limit IS NULL OR context_limit >= 1024)
55
53
  , next_turn INTEGER NOT NULL DEFAULT 1 CHECK (next_turn >= 1)
54
+ , next_loop INTEGER NOT NULL DEFAULT 1 CHECK (next_loop >= 1)
56
55
  , created_at DATETIME DEFAULT CURRENT_TIMESTAMP
57
56
  );
58
57
 
59
58
  CREATE INDEX IF NOT EXISTS idx_runs_alias ON runs (alias);
60
59
  CREATE INDEX IF NOT EXISTS idx_runs_project ON runs (project_id);
61
60
 
61
+ -- Loops: execution units within a run. Each ask/act call creates a loop.
62
+ -- Status: 100=pending, 102=running, 200=completed, 202=proposed,
63
+ -- 500=failed, 499=aborted.
64
+ CREATE TABLE IF NOT EXISTS loops (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT
66
+ , run_id INTEGER NOT NULL REFERENCES runs (id) ON DELETE CASCADE
67
+ , sequence INTEGER NOT NULL CHECK (sequence >= 1)
68
+ , mode TEXT NOT NULL CHECK (mode IN ('ask', 'act'))
69
+ , model TEXT
70
+ , prompt TEXT NOT NULL DEFAULT ''
71
+ , status INTEGER NOT NULL DEFAULT 100 CHECK (status BETWEEN 100 AND 599)
72
+ , config JSON
73
+ , result JSON
74
+ , created_at DATETIME DEFAULT CURRENT_TIMESTAMP
75
+ , UNIQUE (run_id, sequence)
76
+ );
77
+ CREATE INDEX IF NOT EXISTS idx_loops_run ON loops (run_id);
78
+ -- Enforce at most one running loop per run.
79
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_loops_one_active
80
+ ON loops (run_id) WHERE status = 102;
81
+
62
82
  -- Turns: usage stats and sequencing (operational, not model-facing)
63
83
  CREATE TABLE IF NOT EXISTS turns (
64
84
  id INTEGER PRIMARY KEY AUTOINCREMENT
65
85
  , run_id INTEGER NOT NULL REFERENCES runs (id) ON DELETE CASCADE
86
+ , loop_id INTEGER NOT NULL REFERENCES loops (id) ON DELETE CASCADE
66
87
  , sequence INTEGER NOT NULL CHECK (sequence >= 1)
67
88
  , prompt_tokens INTEGER NOT NULL DEFAULT 0 CHECK (prompt_tokens >= 0)
68
89
  , cached_tokens INTEGER NOT NULL DEFAULT 0 CHECK (cached_tokens >= 0)
@@ -90,15 +111,20 @@ ON file_constraints (project_id);
90
111
  -- Known K/V Store: the unified state machine.
91
112
  -- Files, knowledge, tool results, audit — everything is a keyed entry.
92
113
  -- scheme: derived from path via schemeOf(). Generated column.
93
- -- State validated by trigger against schemes.valid_states.
114
+ -- status: HTTP status code (2xx success, 4xx model error, 5xx system error).
115
+ -- fidelity: visibility level, independently managed by relevance engine.
94
116
  CREATE TABLE IF NOT EXISTS known_entries (
95
117
  id INTEGER PRIMARY KEY AUTOINCREMENT
96
118
  , run_id INTEGER NOT NULL REFERENCES runs (id) ON DELETE CASCADE
119
+ , loop_id INTEGER REFERENCES loops (id) ON DELETE CASCADE
97
120
  , turn INTEGER NOT NULL DEFAULT 0 CHECK (turn >= 0)
98
121
  , path TEXT NOT NULL
99
122
  , body TEXT NOT NULL DEFAULT ''
100
123
  , scheme TEXT GENERATED ALWAYS AS (schemeOf(path)) STORED
101
- , state TEXT NOT NULL
124
+ , status INTEGER NOT NULL DEFAULT 200 CHECK (status BETWEEN 100 AND 599)
125
+ , fidelity TEXT NOT NULL DEFAULT 'full' CHECK (
126
+ fidelity IN ('full', 'summary', 'index', 'stored')
127
+ )
102
128
  , hash TEXT
103
129
  , attributes JSON NOT NULL DEFAULT '{}' CHECK (json_valid(attributes))
104
130
  , tokens INTEGER NOT NULL DEFAULT 0 CHECK (tokens >= 0)
@@ -110,43 +136,14 @@ CREATE TABLE IF NOT EXISTS known_entries (
110
136
  );
111
137
  CREATE UNIQUE INDEX IF NOT EXISTS idx_known_entries_run_path
112
138
  ON known_entries (run_id, path);
113
- CREATE INDEX IF NOT EXISTS idx_known_entries_scheme_state
114
- ON known_entries (run_id, scheme, state);
139
+ CREATE INDEX IF NOT EXISTS idx_known_entries_scheme_status
140
+ ON known_entries (run_id, scheme, status);
115
141
  CREATE INDEX IF NOT EXISTS idx_known_entries_turn
116
142
  ON known_entries (run_id, turn);
117
143
 
118
- -- Validate state against schemes.valid_states on insert.
119
- CREATE TRIGGER IF NOT EXISTS trg_known_entry_state_insert
120
- BEFORE INSERT ON known_entries
121
- FOR EACH ROW
122
- BEGIN
123
- SELECT RAISE(ABORT, 'invalid state for scheme')
124
- WHERE NOT EXISTS (
125
- SELECT 1
126
- FROM schemes AS s, json_each(s.valid_states) AS j
127
- WHERE
128
- s.name = COALESCE(schemeOf(NEW.path), 'file')
129
- AND j.value = NEW.state
130
- );
131
- END;
132
-
133
- -- Validate state against schemes.valid_states on update.
134
- CREATE TRIGGER IF NOT EXISTS trg_known_entry_state_update
135
- BEFORE UPDATE OF state ON known_entries
136
- FOR EACH ROW
137
- WHEN OLD.state != NEW.state
138
- BEGIN
139
- SELECT RAISE(ABORT, 'invalid state for scheme')
140
- WHERE NOT EXISTS (
141
- SELECT 1
142
- FROM schemes AS s, json_each(s.valid_states) AS j
143
- WHERE
144
- s.name = COALESCE(schemeOf(NEW.path), 'file')
145
- AND j.value = NEW.state
146
- );
147
- END;
144
+ -- No state validation triggers HTTP status codes are universal.
148
145
 
149
- -- UNRESOLVED VIEW: all entries awaiting user action
146
+ -- UNRESOLVED VIEW: all entries awaiting user action (202 Accepted)
150
147
  CREATE VIEW IF NOT EXISTS v_unresolved AS
151
148
  SELECT
152
149
  run_id
@@ -155,19 +152,20 @@ SELECT
155
152
  , attributes
156
153
  , turn
157
154
  FROM known_entries
158
- WHERE state = 'proposed';
155
+ WHERE status = 202;
159
156
 
160
157
  -- Turn context: materialized snapshot of what the model sees each turn.
161
158
  -- known_entries is the warehouse. turn_context is the shipment.
162
159
  CREATE TABLE IF NOT EXISTS turn_context (
163
160
  id INTEGER PRIMARY KEY AUTOINCREMENT
164
161
  , run_id INTEGER NOT NULL REFERENCES runs (id) ON DELETE CASCADE
162
+ , loop_id INTEGER REFERENCES loops (id) ON DELETE CASCADE
165
163
  , turn INTEGER NOT NULL CHECK (turn >= 1)
166
164
  , ordinal INTEGER NOT NULL CHECK (ordinal >= 0)
167
165
  , path TEXT NOT NULL
168
166
  , scheme TEXT GENERATED ALWAYS AS (schemeOf(path)) STORED
167
+ , status INTEGER NOT NULL DEFAULT 200 CHECK (status BETWEEN 100 AND 599)
169
168
  , fidelity TEXT NOT NULL CHECK (fidelity IN ('full', 'summary', 'index'))
170
- , state TEXT NOT NULL DEFAULT 'full'
171
169
  , body TEXT NOT NULL DEFAULT ''
172
170
  , tokens INTEGER NOT NULL DEFAULT 0 CHECK (tokens >= 0)
173
171
  , attributes JSON NOT NULL DEFAULT '{}' CHECK (json_valid(attributes))
@@ -177,8 +175,8 @@ CREATE TABLE IF NOT EXISTS turn_context (
177
175
  CREATE INDEX IF NOT EXISTS idx_turn_context_run_turn
178
176
  ON turn_context (run_id, turn);
179
177
 
180
- -- Enforce valid run state transitions.
181
- -- completed running: continuation (new turn on finished run)
178
+ -- Enforce valid run state transitions (HTTP status codes).
179
+ -- 100=queued, 102=running, 200=completed, 202=proposed, 499=aborted, 500=failed.
182
180
  CREATE TRIGGER IF NOT EXISTS trg_run_state_transition
183
181
  BEFORE UPDATE OF status ON runs
184
182
  FOR EACH ROW
@@ -186,30 +184,17 @@ WHEN OLD.status != NEW.status
186
184
  BEGIN
187
185
  SELECT RAISE(ABORT, 'invalid run state transition')
188
186
  WHERE NOT (
189
- (OLD.status = 'queued' AND NEW.status IN ('running', 'aborted'))
190
- OR (OLD.status = 'running' AND NEW.status IN ('proposed', 'completed', 'failed', 'aborted'))
191
- OR (OLD.status = 'proposed' AND NEW.status IN ('running', 'completed', 'aborted'))
192
- OR (OLD.status = 'completed' AND NEW.status IN ('running', 'aborted'))
193
- OR (OLD.status = 'failed' AND NEW.status IN ('running', 'aborted'))
194
- OR (OLD.status = 'aborted' AND NEW.status IN ('running'))
187
+ (OLD.status = 100 AND NEW.status IN (102, 499))
188
+ OR (OLD.status = 102 AND NEW.status IN (200, 202, 500, 499))
189
+ OR (OLD.status = 202 AND NEW.status IN (102, 200, 499))
190
+ OR (OLD.status = 200 AND NEW.status IN (102, 499))
191
+ OR (OLD.status = 500 AND NEW.status IN (102, 499))
192
+ OR (OLD.status = 499 AND NEW.status IN (102))
195
193
  );
196
194
  END;
197
195
 
198
- -- Prompt queue. All prompts flow through here. Worker consumes FIFO per run.
199
- CREATE TABLE IF NOT EXISTS prompt_queue (
200
- id INTEGER PRIMARY KEY AUTOINCREMENT
201
- , run_id INTEGER NOT NULL REFERENCES runs (id) ON DELETE CASCADE
202
- , mode TEXT NOT NULL CHECK (mode IN ('ask', 'act'))
203
- , model TEXT
204
- , prompt TEXT NOT NULL
205
- , config JSON
206
- , status TEXT NOT NULL DEFAULT 'pending'
207
- CHECK (status IN ('pending', 'active', 'completed', 'aborted'))
208
- , result JSON
209
- , created_at DATETIME DEFAULT CURRENT_TIMESTAMP
210
- );
211
-
212
- CREATE INDEX IF NOT EXISTS idx_prompt_queue_run ON prompt_queue (run_id, status);
196
+ -- Prompt queue is the loops table (defined above).
197
+ -- Each ask/act enqueues a loop (status=pending). Worker claims FIFO per run.
213
198
 
214
199
  -- RPC audit log. Every call recorded unconditionally.
215
200
  CREATE TABLE IF NOT EXISTS rpc_log (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@possumtech/rummy",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Relational Unknowns Memory Management Yoke",
5
5
  "keywords": [
6
6
  "llm"
@@ -47,12 +47,10 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@possumtech/sqlrite": "^3.1.0",
50
+ "@xmldom/xmldom": "^0.9.9",
50
51
  "htmlparser2": "^12.0.0",
51
52
  "tiktoken": "^1.0.22",
52
- "ws": "^8.19.0"
53
- },
54
- "optionalDependencies": {
55
- "@possumtech/rummy.repo": "^0.0.4",
56
- "@possumtech/rummy.web": "^0.0.7"
53
+ "ws": "^8.19.0",
54
+ "xpath": "^0.0.34"
57
55
  }
58
56
  }
package/service.js CHANGED
@@ -115,7 +115,7 @@ async function main() {
115
115
  }
116
116
 
117
117
  // 6b. Abort stuck runs (can't be running if the server just started)
118
- await db.reset_active_prompts.run({});
118
+ await db.reset_active_loops.run({});
119
119
  const aborted = await db.abort_stuck_runs.run({});
120
120
  if (aborted.changes > 0) {
121
121
  console.log(`[RUMMY] Recovered ${aborted.changes} stuck run(s)`);