@possumtech/rummy 0.5.0 → 2.0.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 (157) hide show
  1. package/.env.example +42 -5
  2. package/PLUGINS.md +389 -194
  3. package/README.md +25 -8
  4. package/SPEC.md +934 -373
  5. package/bin/demo.js +166 -0
  6. package/bin/rummy.js +9 -3
  7. package/biome/no-fallbacks.grit +50 -0
  8. package/lang/en.json +2 -2
  9. package/migrations/001_initial_schema.sql +88 -37
  10. package/package.json +13 -11
  11. package/scriptify/ask_run.js +77 -0
  12. package/service.js +50 -9
  13. package/src/agent/AgentLoop.js +476 -335
  14. package/src/agent/ContextAssembler.js +4 -4
  15. package/src/agent/Entries.js +676 -0
  16. package/src/agent/ProjectAgent.js +30 -18
  17. package/src/agent/TurnExecutor.js +232 -421
  18. package/src/agent/XmlParser.js +99 -33
  19. package/src/agent/budget.js +56 -0
  20. package/src/agent/errors.js +22 -0
  21. package/src/agent/httpStatus.js +39 -0
  22. package/src/agent/known_checks.sql +8 -4
  23. package/src/agent/known_queries.sql +9 -13
  24. package/src/agent/known_store.sql +280 -125
  25. package/src/agent/materializeContext.js +104 -0
  26. package/src/agent/runs.sql +29 -7
  27. package/src/agent/schemes.sql +14 -3
  28. package/src/agent/tokens.js +6 -0
  29. package/src/agent/turns.sql +9 -9
  30. package/src/hooks/HookRegistry.js +6 -5
  31. package/src/hooks/Hooks.js +44 -3
  32. package/src/hooks/PluginContext.js +29 -21
  33. package/src/{server → hooks}/RpcRegistry.js +2 -1
  34. package/src/hooks/RummyContext.js +139 -35
  35. package/src/hooks/ToolRegistry.js +21 -16
  36. package/src/llm/LlmProvider.js +66 -89
  37. package/src/llm/errors.js +21 -0
  38. package/src/llm/retry.js +63 -0
  39. package/src/plugins/ask_user/README.md +1 -1
  40. package/src/plugins/ask_user/ask_user.js +37 -12
  41. package/src/plugins/ask_user/ask_userDoc.js +2 -25
  42. package/src/plugins/ask_user/ask_userDoc.md +10 -0
  43. package/src/plugins/budget/README.md +27 -25
  44. package/src/plugins/budget/budget.js +306 -88
  45. package/src/plugins/cp/README.md +2 -2
  46. package/src/plugins/cp/cp.js +29 -11
  47. package/src/plugins/cp/cpDoc.js +2 -15
  48. package/src/plugins/cp/cpDoc.md +7 -0
  49. package/src/plugins/engine/README.md +2 -2
  50. package/src/plugins/engine/engine.sql +4 -4
  51. package/src/plugins/engine/turn_context.sql +10 -10
  52. package/src/plugins/env/README.md +20 -5
  53. package/src/plugins/env/env.js +45 -6
  54. package/src/plugins/env/envDoc.js +2 -23
  55. package/src/plugins/env/envDoc.md +13 -0
  56. package/src/plugins/error/README.md +16 -0
  57. package/src/plugins/error/error.js +151 -0
  58. package/src/plugins/file/README.md +6 -6
  59. package/src/plugins/file/file.js +15 -2
  60. package/src/plugins/get/README.md +1 -1
  61. package/src/plugins/get/get.js +103 -48
  62. package/src/plugins/get/getDoc.js +2 -32
  63. package/src/plugins/get/getDoc.md +36 -0
  64. package/src/plugins/hedberg/README.md +1 -2
  65. package/src/plugins/hedberg/hedberg.js +8 -4
  66. package/src/plugins/hedberg/matcher.js +16 -17
  67. package/src/plugins/hedberg/normalize.js +0 -48
  68. package/src/plugins/helpers.js +42 -2
  69. package/src/plugins/index.js +146 -123
  70. package/src/plugins/instructions/README.md +35 -9
  71. package/src/plugins/instructions/instructions.js +244 -9
  72. package/src/plugins/instructions/instructions.md +33 -0
  73. package/src/plugins/instructions/instructions_104.md +7 -0
  74. package/src/plugins/instructions/instructions_105.md +38 -0
  75. package/src/plugins/instructions/instructions_106.md +21 -0
  76. package/src/plugins/instructions/instructions_107.md +10 -0
  77. package/src/plugins/instructions/instructions_108.md +0 -0
  78. package/src/plugins/instructions/protocol.js +12 -0
  79. package/src/plugins/known/README.md +2 -2
  80. package/src/plugins/known/known.js +68 -36
  81. package/src/plugins/known/knownDoc.js +2 -17
  82. package/src/plugins/known/knownDoc.md +8 -0
  83. package/src/plugins/log/README.md +48 -0
  84. package/src/plugins/log/log.js +129 -0
  85. package/src/plugins/mv/README.md +2 -2
  86. package/src/plugins/mv/mv.js +55 -22
  87. package/src/plugins/mv/mvDoc.js +2 -18
  88. package/src/plugins/mv/mvDoc.md +10 -0
  89. package/src/plugins/ollama/README.md +15 -0
  90. package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
  91. package/src/plugins/openai/README.md +17 -0
  92. package/src/plugins/openai/openai.js +120 -0
  93. package/src/plugins/openrouter/README.md +27 -0
  94. package/src/plugins/openrouter/openrouter.js +121 -0
  95. package/src/plugins/persona/README.md +20 -0
  96. package/src/plugins/persona/persona.js +9 -16
  97. package/src/plugins/policy/README.md +21 -0
  98. package/src/plugins/policy/policy.js +29 -14
  99. package/src/plugins/prompt/README.md +1 -1
  100. package/src/plugins/prompt/prompt.js +64 -16
  101. package/src/plugins/rm/README.md +1 -1
  102. package/src/plugins/rm/rm.js +56 -12
  103. package/src/plugins/rm/rmDoc.js +2 -20
  104. package/src/plugins/rm/rmDoc.md +13 -0
  105. package/src/plugins/rpc/README.md +2 -2
  106. package/src/plugins/rpc/rpc.js +525 -296
  107. package/src/plugins/set/README.md +1 -1
  108. package/src/plugins/set/set.js +318 -75
  109. package/src/plugins/set/setDoc.js +2 -35
  110. package/src/plugins/set/setDoc.md +22 -0
  111. package/src/plugins/sh/README.md +28 -5
  112. package/src/plugins/sh/sh.js +50 -6
  113. package/src/plugins/sh/shDoc.js +2 -23
  114. package/src/plugins/sh/shDoc.md +13 -0
  115. package/src/plugins/skill/README.md +23 -0
  116. package/src/plugins/skill/skill.js +14 -18
  117. package/src/plugins/stream/README.md +101 -0
  118. package/src/plugins/stream/stream.js +290 -0
  119. package/src/plugins/telemetry/README.md +1 -1
  120. package/src/plugins/telemetry/telemetry.js +129 -80
  121. package/src/plugins/think/README.md +1 -1
  122. package/src/plugins/think/think.js +12 -0
  123. package/src/plugins/think/thinkDoc.js +2 -15
  124. package/src/plugins/think/thinkDoc.md +7 -0
  125. package/src/plugins/unknown/README.md +3 -3
  126. package/src/plugins/unknown/unknown.js +47 -19
  127. package/src/plugins/unknown/unknownDoc.js +2 -21
  128. package/src/plugins/unknown/unknownDoc.md +11 -0
  129. package/src/plugins/update/README.md +1 -1
  130. package/src/plugins/update/update.js +83 -5
  131. package/src/plugins/update/updateDoc.js +2 -30
  132. package/src/plugins/update/updateDoc.md +8 -0
  133. package/src/plugins/xai/README.md +23 -0
  134. package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
  135. package/src/plugins/yolo/yolo.js +192 -0
  136. package/src/server/ClientConnection.js +64 -37
  137. package/src/server/SocketServer.js +23 -10
  138. package/src/server/protocol.js +11 -0
  139. package/src/sql/v_model_context.sql +27 -31
  140. package/src/sql/v_run_log.sql +9 -14
  141. package/EXCEPTIONS.md +0 -46
  142. package/FIDELITY_CONTRACT.md +0 -172
  143. package/src/agent/KnownStore.js +0 -337
  144. package/src/agent/ResponseHealer.js +0 -241
  145. package/src/llm/OpenAiClient.js +0 -100
  146. package/src/llm/OpenRouterClient.js +0 -100
  147. package/src/plugins/budget/recovery.js +0 -47
  148. package/src/plugins/instructions/preamble.md +0 -45
  149. package/src/plugins/performed/README.md +0 -15
  150. package/src/plugins/performed/performed.js +0 -45
  151. package/src/plugins/previous/README.md +0 -16
  152. package/src/plugins/previous/previous.js +0 -56
  153. package/src/plugins/progress/README.md +0 -16
  154. package/src/plugins/progress/progress.js +0 -43
  155. package/src/plugins/summarize/README.md +0 -19
  156. package/src/plugins/summarize/summarize.js +0 -32
  157. package/src/plugins/summarize/summarizeDoc.js +0 -27
package/bin/demo.js ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ // Fast inspector for the most recent (or a named) run.
3
+ //
4
+ // Pulls the packet the model actually saw (system://N, user://N), the
5
+ // model's response (assistant://N, reasoning://N), the log entries for
6
+ // the turn, and optional summaries across the whole run.
7
+ //
8
+ // Usage:
9
+ // npm run test:demo # latest run, latest turn
10
+ // npm run test:demo -- --turn 5 # latest run, turn 5
11
+ // npm run test:demo -- --run alias # specific run, its latest turn
12
+ // npm run test:demo -- --all # all turns' headers + final packet
13
+ // npm run test:demo -- --packet # only dump system/user/assistant
14
+
15
+ import { parseArgs } from "node:util";
16
+ import { DatabaseSync } from "node:sqlite";
17
+ import { readFileSync } from "node:fs";
18
+
19
+ const args = parseArgs({
20
+ options: {
21
+ run: { type: "string" },
22
+ turn: { type: "string" },
23
+ all: { type: "boolean", default: false },
24
+ packet: { type: "boolean", default: false },
25
+ db: { type: "string", default: "rummy_dev.db" },
26
+ },
27
+ allowPositionals: false,
28
+ }).values;
29
+
30
+ const db = new DatabaseSync(args.db);
31
+
32
+ const runRow = args.run
33
+ ? db.prepare("SELECT * FROM runs WHERE alias = ?").get(args.run)
34
+ : db.prepare("SELECT * FROM runs ORDER BY id DESC LIMIT 1").get();
35
+
36
+ if (!runRow) {
37
+ console.error(args.run ? `run ${args.run} not found` : "no runs in db");
38
+ process.exit(1);
39
+ }
40
+
41
+ const runId = runRow.id;
42
+ const alias = runRow.alias;
43
+
44
+ const turns = db
45
+ .prepare(
46
+ "SELECT sequence, context_tokens, prompt_tokens, completion_tokens, reasoning_tokens, total_tokens, cost FROM turns WHERE run_id = ? ORDER BY sequence",
47
+ )
48
+ .all(runId);
49
+
50
+ const selectedTurn = args.turn ? Number(args.turn) : turns.at(-1)?.sequence;
51
+
52
+ const banner = (s) => console.log(`\n━━━ ${s} ${"━".repeat(Math.max(4, 70 - s.length))}`);
53
+
54
+ const loadBody = (path) => {
55
+ const row = db
56
+ .prepare(
57
+ "SELECT e.body, e.attributes, rv.state, rv.outcome, rv.visibility FROM entries e JOIN run_views rv ON rv.entry_id = e.id WHERE rv.run_id = ? AND e.path = ?",
58
+ )
59
+ .get(runId, path);
60
+ return row ? row : null;
61
+ };
62
+
63
+ // ── header ─────────────────────────────────────────────────────────
64
+ const totalCost = turns.reduce((s, t) => s + (t.cost || 0), 0);
65
+ banner(`run ${alias} (id=${runId}, status=${runRow.status})`);
66
+ console.log(
67
+ `model=${runRow.model} turns=${turns.length} cost=${totalCost.toFixed(6)}`,
68
+ );
69
+
70
+ if (!args.packet) {
71
+ banner("turns");
72
+ for (const t of turns) {
73
+ const marker = t.sequence === selectedTurn ? "▶" : " ";
74
+ console.log(
75
+ `${marker} turn ${String(t.sequence).padStart(2)} ctx=${String(t.context_tokens).padStart(6)} in=${String(t.prompt_tokens).padStart(6)} out=${String(t.completion_tokens).padStart(5)} reason=${String(t.reasoning_tokens).padStart(5)}`,
76
+ );
77
+ }
78
+ }
79
+
80
+ // ── packet for the selected turn ───────────────────────────────────
81
+ if (selectedTurn) {
82
+ const system = loadBody(`system://${selectedTurn}`);
83
+ const user = loadBody(`user://${selectedTurn}`);
84
+ const assistant = loadBody(`assistant://${selectedTurn}`);
85
+ const reasoning = loadBody(`reasoning://${selectedTurn}`);
86
+
87
+ banner(`turn ${selectedTurn} — system://`);
88
+ console.log(system ? system.body : "(not recorded)");
89
+ banner(`turn ${selectedTurn} — user://`);
90
+ console.log(user ? user.body : "(not recorded)");
91
+ banner(`turn ${selectedTurn} — assistant://`);
92
+ console.log(assistant ? assistant.body : "(not recorded)");
93
+ if (reasoning) {
94
+ banner(`turn ${selectedTurn} — reasoning://`);
95
+ console.log(reasoning.body);
96
+ }
97
+ }
98
+
99
+ if (args.packet) process.exit(0);
100
+
101
+ // ── log entries for the selected turn ──────────────────────────────
102
+ if (selectedTurn) {
103
+ banner(`turn ${selectedTurn} — log entries`);
104
+ const logs = db
105
+ .prepare(
106
+ "SELECT e.path, e.body, e.attributes, rv.state, rv.outcome, rv.visibility FROM entries e JOIN run_views rv ON rv.entry_id = e.id WHERE rv.run_id = ? AND rv.turn = ? AND e.path LIKE 'log://%' ORDER BY e.id",
107
+ )
108
+ .all(runId, selectedTurn);
109
+ if (logs.length === 0) console.log("(none)");
110
+ for (const l of logs) {
111
+ const attrs = l.attributes ? JSON.parse(l.attributes) : {};
112
+ const body = l.body ? l.body.replace(/\n/g, "⏎").slice(0, 100) : "";
113
+ console.log(
114
+ ` ${l.state.padEnd(9)} ${l.visibility.padEnd(10)} ${l.path}`,
115
+ );
116
+ if (Object.keys(attrs).length > 0)
117
+ console.log(` attrs: ${JSON.stringify(attrs).slice(0, 200)}`);
118
+ if (body) console.log(` body: ${body}`);
119
+ }
120
+ }
121
+
122
+ // ── unresolved (proposals awaiting client) ─────────────────────────
123
+ banner("unresolved");
124
+ const pending = db
125
+ .prepare(
126
+ "SELECT e.path, substr(e.attributes,1,200) AS attrs, rv.turn FROM entries e JOIN run_views rv ON rv.entry_id = e.id WHERE rv.run_id = ? AND rv.state IN ('proposed','streaming') ORDER BY e.id",
127
+ )
128
+ .all(runId);
129
+ if (pending.length === 0) console.log("(none)");
130
+ for (const p of pending) console.log(` turn ${p.turn} ${p.path}\n ${p.attrs}`);
131
+
132
+ // ── unknowns + knowns tally ────────────────────────────────────────
133
+ const unknowns = db
134
+ .prepare(
135
+ "SELECT e.path, e.body FROM entries e JOIN run_views rv ON rv.entry_id = e.id WHERE rv.run_id = ? AND e.scheme = 'unknown' ORDER BY e.id",
136
+ )
137
+ .all(runId);
138
+ banner(`unknowns (${unknowns.length})`);
139
+ for (const u of unknowns)
140
+ console.log(` ${u.path}\n ${u.body.slice(0, 120)}`);
141
+
142
+ const knowns = db
143
+ .prepare(
144
+ "SELECT e.path FROM entries e JOIN run_views rv ON rv.entry_id = e.id WHERE rv.run_id = ? AND e.scheme = 'known' ORDER BY e.id",
145
+ )
146
+ .all(runId);
147
+ banner(`knowns (${knowns.length})`);
148
+ for (const k of knowns) console.log(` ${k.path}`);
149
+
150
+ // ── all turns (if --all) ───────────────────────────────────────────
151
+ if (args.all) {
152
+ for (const t of turns) {
153
+ if (t.sequence === selectedTurn) continue;
154
+ const system = loadBody(`system://${t.sequence}`);
155
+ const user = loadBody(`user://${t.sequence}`);
156
+ const assistant = loadBody(`assistant://${t.sequence}`);
157
+ banner(`turn ${t.sequence} — system://`);
158
+ console.log(system ? system.body : "(not recorded)");
159
+ banner(`turn ${t.sequence} — user://`);
160
+ console.log(user ? user.body : "(not recorded)");
161
+ banner(`turn ${t.sequence} — assistant://`);
162
+ console.log(assistant ? assistant.body : "(not recorded)");
163
+ }
164
+ }
165
+
166
+ db.close();
package/bin/rummy.js CHANGED
@@ -10,9 +10,15 @@ const packageRoot = join(__dirname, "..");
10
10
 
11
11
  const rummyHome = process.env.RUMMY_HOME || join(homedir(), ".rummy");
12
12
 
13
- // Load defaults, then user overrides
14
- process.loadEnvFile(join(packageRoot, ".env.example"));
15
- const userEnv = join(rummyHome, ".env");
13
+ // Base dir for env files: cwd if it has .env.example, else $RUMMY_HOME.
14
+ // The package's own .env.example is never consulted — silent package-
15
+ // root defaults break the project-as-context model and hide behavior
16
+ // from the user.
17
+ const cwd = process.cwd();
18
+ const baseDir = existsSync(join(cwd, ".env.example")) ? cwd : rummyHome;
19
+
20
+ process.loadEnvFile(join(baseDir, ".env.example"));
21
+ const userEnv = join(baseDir, ".env");
16
22
  if (existsSync(userEnv)) process.loadEnvFile(userEnv);
17
23
 
18
24
  // Resolve RUMMY_HOME and make DB path absolute relative to it
@@ -0,0 +1,50 @@
1
+ // No silent fallbacks outside hedberg.
2
+ // Rule: interiors crash on contract violation, boundaries validate.
3
+ // Patterns like `|| 0`, `?? ""` silently mask missing data.
4
+ // hedberg is the stochastic-interpretation boundary — fallbacks
5
+ // there are legitimate and filtered out below.
6
+ //
7
+ // Two classes caught:
8
+ // 1. Falsy-literal defaults: `$_ || 0` / `$_ ?? ""` / etc.
9
+ // 2. Env-var defaults: `process.env.X || <anything>`. These mask
10
+ // a missing documented env var; the right shape is either a
11
+ // naked `Number(process.env.X)` (NaN propagates on absence)
12
+ // or `process.env.X` directly.
13
+
14
+ language js
15
+
16
+ or {
17
+ `$_ || 0`,
18
+ `$_ || ""`,
19
+ `$_ || ''`,
20
+ `$_ || null`,
21
+ `$_ || false`,
22
+ `$_ || []`,
23
+ `$_ || {}`,
24
+ `$_ ?? 0`,
25
+ `$_ ?? ""`,
26
+ `$_ ?? ''`,
27
+ `$_ ?? null`,
28
+ `$_ ?? false`,
29
+ `$_ ?? []`,
30
+ `$_ ?? {}`,
31
+ `process.env.$_ || $_`,
32
+ `process.env.$_ ?? $_`,
33
+ `Number(process.env.$_) || $_`,
34
+ `Number(process.env.$_) ?? $_`,
35
+ `parseInt(process.env.$_, $_) || $_`,
36
+ `parseInt(process.env.$_, $_) ?? $_`,
37
+ `Number.parseInt(process.env.$_, $_) || $_`,
38
+ `Number.parseInt(process.env.$_, $_) ?? $_`,
39
+ `Number.parseFloat(process.env.$_) || $_`,
40
+ `Number.parseFloat(process.env.$_) ?? $_`
41
+ } as $match where {
42
+ $filename <: not includes "src/plugins/hedberg/",
43
+ $filename <: not includes "src/agent/XmlParser.js",
44
+ $filename <: not includes "/test/",
45
+ $filename <: not includes ".test.js",
46
+ register_diagnostic(
47
+ span = $match,
48
+ message = "Silent fallback outside hedberg masks contract violations — fix the contract instead."
49
+ )
50
+ }
package/lang/en.json CHANGED
@@ -27,8 +27,8 @@
27
27
  "error.tool_already_registered": "Tool '{name}' already registered.",
28
28
  "error.rpc_already_registered": "RPC method '{name}' already registered.",
29
29
  "error.resolution_invalid": "Invalid resolution action: {action}. Use 'accept' or 'reject'.",
30
- "error.xai_base_url_missing": "x.ai/ model requested but XAI_BASE_URL is not set.",
31
- "error.xai_api_key_missing": "x.ai/ model requested but XAI_API_KEY is not set.",
30
+ "error.xai_base_url_missing": "xai/ model requested but XAI_BASE_URL is not set.",
31
+ "error.xai_api_key_missing": "xai/ model requested but XAI_API_KEY is not set.",
32
32
  "error.xai_auth": "xAI Authentication Error: {status}. Please check your XAI_API_KEY.",
33
33
  "error.xai_api": "xAI API error: {status}"
34
34
  }
@@ -4,13 +4,19 @@ 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
- -- 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.
7
+ -- writable_by: JSON array of {system, plugin, client, model} four writer tiers.
8
+ -- capability_class: optional restriction group (e.g. "shell", "files", "web")
9
+ -- so the policy plugin can compute the effective toolset from a run's
10
+ -- restriction list. Null means the scheme is always available.
10
11
  CREATE TABLE IF NOT EXISTS schemes (
11
12
  name TEXT PRIMARY KEY
12
13
  , model_visible BOOLEAN NOT NULL DEFAULT 1
13
14
  , category TEXT
15
+ , default_scope TEXT NOT NULL DEFAULT 'run'
16
+ CHECK (default_scope IN ('run', 'project', 'global'))
17
+ , writable_by JSON NOT NULL DEFAULT '["model","plugin"]'
18
+ CHECK (json_valid(writable_by))
19
+ , capability_class TEXT
14
20
  );
15
21
 
16
22
  -- Schemes are registered by plugins at startup via core.registerScheme().
@@ -98,7 +104,7 @@ CREATE TABLE IF NOT EXISTS turns (
98
104
  CREATE INDEX IF NOT EXISTS idx_turns_run_seq ON turns (run_id, sequence);
99
105
 
100
106
  -- File constraints: client-set visibility rules, project-scoped.
101
- -- Persists across runs. Orthogonal to fidelity.
107
+ -- Persists across runs. Orthogonal to visibility.
102
108
  CREATE TABLE IF NOT EXISTS file_constraints (
103
109
  id INTEGER PRIMARY KEY AUTOINCREMENT
104
110
  , project_id INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE
@@ -110,50 +116,93 @@ CREATE TABLE IF NOT EXISTS file_constraints (
110
116
  CREATE INDEX IF NOT EXISTS idx_file_constraints_project
111
117
  ON file_constraints (project_id);
112
118
 
113
- -- Known K/V Store: the unified state machine.
114
- -- Files, knowledge, tool results, audit everything is a keyed entry.
119
+ -- Entries: content-addressable by (scope, path). The actual payload.
120
+ -- scope: 'global' | 'project:N' | 'run:N'. Determines read access.
115
121
  -- scheme: derived from path via schemeOf(). Generated column.
116
- -- status: HTTP status code (2xx success, 4xx model error, 5xx system error).
117
- -- fidelity: visibility level, independently managed by relevance engine.
118
- CREATE TABLE IF NOT EXISTS known_entries (
122
+ -- No visibility, status, turn, loop those are view-side concerns.
123
+ CREATE TABLE IF NOT EXISTS entries (
124
+ id INTEGER PRIMARY KEY AUTOINCREMENT
125
+ , scope TEXT NOT NULL
126
+ , path TEXT NOT NULL CHECK (length(path) <= 2048)
127
+ , scheme TEXT GENERATED ALWAYS AS (schemeOf(path)) STORED
128
+ , body TEXT NOT NULL DEFAULT ''
129
+ , attributes JSON NOT NULL DEFAULT '{}' CHECK (json_valid(attributes))
130
+ , hash TEXT
131
+ , created_at DATETIME DEFAULT CURRENT_TIMESTAMP
132
+ , updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
133
+ );
134
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_entries_scope_path
135
+ ON entries (scope, path);
136
+ CREATE INDEX IF NOT EXISTS idx_entries_scope_scheme
137
+ ON entries (scope, scheme);
138
+
139
+ -- Run views: per-run projection of entries. State, visibility, turn live here.
140
+ -- A run has at most one view of any given entry. Absent view = not in context.
141
+ -- state: lifecycle. visibility: what the model sees. Orthogonal axes (SPEC §0.1).
142
+ -- outcome: short reason string when state ∈ {failed, cancelled}; NULL otherwise.
143
+ CREATE TABLE IF NOT EXISTS run_views (
119
144
  id INTEGER PRIMARY KEY AUTOINCREMENT
120
145
  , run_id INTEGER NOT NULL REFERENCES runs (id) ON DELETE CASCADE
146
+ , entry_id INTEGER NOT NULL REFERENCES entries (id) ON DELETE CASCADE
121
147
  , loop_id INTEGER REFERENCES loops (id) ON DELETE CASCADE
122
148
  , turn INTEGER NOT NULL DEFAULT 0 CHECK (turn >= 0)
123
- , path TEXT NOT NULL CHECK (length(path) <= 2048)
124
- , body TEXT NOT NULL DEFAULT ''
125
- , scheme TEXT GENERATED ALWAYS AS (schemeOf(path)) STORED
126
- , status INTEGER NOT NULL DEFAULT 200 CHECK (status BETWEEN 100 AND 599)
127
- , fidelity TEXT NOT NULL DEFAULT 'promoted' CHECK (
128
- fidelity IN ('promoted', 'demoted', 'archived')
149
+ , state TEXT NOT NULL DEFAULT 'resolved' CHECK (
150
+ state IN ('proposed', 'streaming', 'resolved', 'failed', 'cancelled')
151
+ )
152
+ , outcome TEXT
153
+ , visibility TEXT NOT NULL DEFAULT 'visible' CHECK (
154
+ visibility IN ('visible', 'summarized', 'archived')
129
155
  )
130
- , hash TEXT
131
- , attributes JSON NOT NULL DEFAULT '{}' CHECK (json_valid(attributes))
132
- , tokens INTEGER NOT NULL DEFAULT 0 CHECK (tokens >= 0)
133
- , refs INTEGER NOT NULL DEFAULT 0 CHECK (refs >= 0)
134
156
  , write_count INTEGER NOT NULL DEFAULT 1 CHECK (write_count >= 1)
157
+ , refs INTEGER NOT NULL DEFAULT 0 CHECK (refs >= 0)
135
158
  , created_at DATETIME DEFAULT CURRENT_TIMESTAMP
136
159
  , updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
137
160
  );
138
- CREATE UNIQUE INDEX IF NOT EXISTS idx_known_entries_run_path
139
- ON known_entries (run_id, path);
140
- CREATE INDEX IF NOT EXISTS idx_known_entries_scheme_status
141
- ON known_entries (run_id, scheme, status);
142
- CREATE INDEX IF NOT EXISTS idx_known_entries_turn
143
- ON known_entries (run_id, turn);
161
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_run_views_run_entry
162
+ ON run_views (run_id, entry_id);
163
+ CREATE INDEX IF NOT EXISTS idx_run_views_run_turn
164
+ ON run_views (run_id, turn);
165
+ CREATE INDEX IF NOT EXISTS idx_run_views_run_visibility
166
+ ON run_views (run_id, visibility);
144
167
 
145
- -- No state validation triggers HTTP status codes are universal.
168
+ -- Legacy-shape compatibility view. Joins run_views to entries; reads
169
+ -- against this view keep one shape. Writes MUST target entries +
170
+ -- run_views directly.
171
+ CREATE VIEW IF NOT EXISTS known_entries AS
172
+ SELECT
173
+ rv.id AS id
174
+ , rv.run_id AS run_id
175
+ , rv.loop_id AS loop_id
176
+ , rv.turn AS turn
177
+ , e.path AS path
178
+ , e.body AS body
179
+ , e.scheme AS scheme
180
+ , rv.state AS state
181
+ , rv.outcome AS outcome
182
+ , rv.visibility AS visibility
183
+ , e.hash AS hash
184
+ , e.attributes AS attributes
185
+ , rv.refs AS refs
186
+ , rv.write_count AS write_count
187
+ , e.created_at AS created_at
188
+ , rv.updated_at AS updated_at
189
+ , e.id AS entry_id
190
+ , e.scope AS scope
191
+ FROM run_views AS rv
192
+ JOIN entries AS e ON e.id = rv.entry_id;
146
193
 
147
- -- UNRESOLVED VIEW: all entries awaiting user action (202 Accepted)
194
+ -- UNRESOLVED VIEW: entries that haven't reached a terminal state.
195
+ -- Proposed (awaiting user decision) or streaming (in-flight).
148
196
  CREATE VIEW IF NOT EXISTS v_unresolved AS
149
197
  SELECT
150
- run_id
151
- , path
152
- , body
153
- , attributes
154
- , turn
155
- FROM known_entries
156
- WHERE status = 202;
198
+ rv.run_id
199
+ , e.path
200
+ , e.body
201
+ , e.attributes
202
+ , rv.turn
203
+ FROM run_views AS rv
204
+ JOIN entries AS e ON e.id = rv.entry_id
205
+ WHERE rv.state IN ('proposed', 'streaming');
157
206
 
158
207
  -- Turn context: materialized snapshot of what the model sees each turn.
159
208
  -- known_entries is the warehouse. turn_context is the shipment.
@@ -165,10 +214,12 @@ CREATE TABLE IF NOT EXISTS turn_context (
165
214
  , ordinal INTEGER NOT NULL CHECK (ordinal >= 0)
166
215
  , path TEXT NOT NULL
167
216
  , scheme TEXT GENERATED ALWAYS AS (schemeOf(path)) STORED
168
- , status INTEGER NOT NULL DEFAULT 200 CHECK (status BETWEEN 100 AND 599)
169
- , fidelity TEXT NOT NULL CHECK (fidelity IN ('promoted', 'demoted'))
217
+ , state TEXT NOT NULL DEFAULT 'resolved' CHECK (
218
+ state IN ('proposed', 'streaming', 'resolved', 'failed', 'cancelled')
219
+ )
220
+ , outcome TEXT
221
+ , visibility TEXT NOT NULL CHECK (visibility IN ('visible', 'summarized'))
170
222
  , body TEXT NOT NULL DEFAULT ''
171
- , tokens INTEGER NOT NULL DEFAULT 0 CHECK (tokens >= 0)
172
223
  , attributes JSON NOT NULL DEFAULT '{}' CHECK (json_valid(attributes))
173
224
  , category TEXT NOT NULL DEFAULT 'logging'
174
225
  , source_turn INTEGER DEFAULT 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@possumtech/rummy",
3
- "version": "0.5.0",
3
+ "version": "2.0.1",
4
4
  "description": "Relational Unknowns Memory Management Yoke",
5
5
  "keywords": [
6
6
  "llm"
@@ -37,21 +37,23 @@
37
37
  "fix:sql": ". .venv/bin/activate && sqlfluff fix . --force",
38
38
  "test": "npm run lint && npm run test:unit && npm run test:intg",
39
39
  "test:all": "npm run lint && npm run test:unit && npm run test:intg && npm run test:e2e",
40
- "test:unit": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --experimental-test-coverage --test-coverage-lines=50 --test-coverage-branches=50 --test-coverage-functions=50 --test-concurrency=1 --test-force-exit --test $(find src -name '*.test.js')",
41
- "test:intg": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test $(find test/integration -name '*.test.js')",
40
+ "test:unit": "node --env-file-if-exists=.env.example --env-file-if-exists=.env.test --experimental-test-coverage --test-coverage-lines=50 --test-coverage-branches=50 --test-coverage-functions=50 --test-concurrency=1 --test-force-exit --test $(find src -name '*.test.js')",
41
+ "test:intg": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test-force-exit --test $(find test/integration -name '*.test.js')",
42
42
  "test:e2e": "mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test-force-exit --test-reporter=spec --test $(find test/e2e -name '*.test.js') 2>&1 | tee /tmp/rummy_test_diag/e2e_$(date +%Y%m%dT%H%M%S).log",
43
43
  "test:live": "mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test-force-exit --test-reporter=spec --test $(find test/live -name '*.test.js') 2>&1 | tee /tmp/rummy_test_diag/live_$(date +%Y%m%dT%H%M%S).log",
44
- "test:clean": "rm -rf test/lme/results test/mab/results test/tmp /tmp/rummy_test_diag /tmp/rummy_test_*.db /tmp/rummy_test_*.db-shm /tmp/rummy_test_*.db-wal && echo 'Test artifacts cleaned.'",
45
- "test:mab:get": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/mab/download.js",
46
- "test:mab": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/mab/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/mab_$(date +%Y%m%dT%H%M%S).log' --",
47
- "test:grok": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --env-file-if-exists=.env.grok test/mab/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/mab_grok_$(date +%Y%m%dT%H%M%S).log' --",
48
- "test:mab:taxonomy": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/mab/runner.js --split Conflict_Resolution --row 0 --no-questions 2>&1 | tee /tmp/rummy_test_diag/taxonomy_$(date +%Y%m%dT%H%M%S).log' --",
49
- "test:grok:taxonomy": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --env-file-if-exists=.env.grok test/mab/runner.js --split Conflict_Resolution --row 0 --no-questions 2>&1 | tee /tmp/rummy_test_diag/taxonomy_grok_$(date +%Y%m%dT%H%M%S).log' --",
44
+ "test:clean": "rm -rf test/lme/results test/swe/results test/swe/repos test/tmp /tmp/rummy_test_diag /tmp/rummy_test_*.db /tmp/rummy_test_*.db-shm /tmp/rummy_test_*.db-wal && echo 'Test artifacts cleaned.'",
50
45
  "test:lme:get": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/lme/download.js",
51
46
  "test:lme": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/lme/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/lme_$(date +%Y%m%dT%H%M%S).log' --",
52
- "test:mab:clean": "rm -rf test/mab/results/*/",
47
+ "test:swe:setup": "bash test/swe/setup.sh",
48
+ "test:swe:get": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/swe/download.js",
49
+ "test:swe": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/swe/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/swe_$(date +%Y%m%dT%H%M%S).log' --",
50
+ "test:swe:eval": "bash -c 'cd test/swe && source .venv/bin/activate && python evaluate.py \"$@\"' --",
51
+ "test:swe:baseline": "bash -c 'cd test/swe && source .venv/bin/activate && python baseline.py \"$@\"' --",
53
52
  "test:lme:clean": "rm -rf test/lme/results/*/",
54
- "test:clear": "rm -rf /tmp/rummy_test_diag /tmp/rummy_test_*.db /tmp/rummy_test_*.db-shm /tmp/rummy_test_*.db-wal /tmp/rummy-stories-*"
53
+ "test:swe:clean": "rm -rf test/swe/results/*/ test/swe/repos/",
54
+ "test:clear": "rm -rf /tmp/rummy_test_diag /tmp/rummy_test_*.db /tmp/rummy_test_*.db-shm /tmp/rummy_test_*.db-wal /tmp/rummy-stories-*",
55
+ "test:demo": "node --env-file-if-exists=.env.example --env-file-if-exists=.env bin/demo.js",
56
+ "test:spec": "node test/spec-coverage.js"
55
57
  },
56
58
  "devDependencies": {
57
59
  "@biomejs/biome": "^2.4.6"
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Inject a follow-up question into an existing LME run and print the answer.
3
+ *
4
+ * Usage: node scriptify/ask_run.js <db_path> <run_alias> "your question"
5
+ *
6
+ * Reuses the run's full ingested context so the model answers with all
7
+ * its accumulated knowledge. Used as a debugging tool to interrogate
8
+ * the model's reasoning after a benchmark completes.
9
+ */
10
+ import TestDb from "../test/helpers/TestDb.js";
11
+ import TestServer from "../test/helpers/TestServer.js";
12
+ import RpcClient from "../test/helpers/RpcClient.js";
13
+
14
+ const [, , dbPath, alias, ...questionParts] = process.argv;
15
+ const question = questionParts.join(" ");
16
+
17
+ if (!dbPath || !alias || !question) {
18
+ console.error(
19
+ 'Usage: node scriptify/ask_run.js <db_path> <run_alias> "your question"',
20
+ );
21
+ process.exit(1);
22
+ }
23
+
24
+ const tdb = await TestDb.createAt(dbPath);
25
+ const tserver = await TestServer.start(tdb);
26
+ const client = new RpcClient(tserver.url);
27
+ await client.connect();
28
+ await client.call("rummy/hello", {
29
+ name: "ask_run",
30
+ projectRoot: "/tmp/rummy-lme",
31
+ });
32
+
33
+ console.log(`Asking ${alias}: ${question}\n`);
34
+
35
+ const TERMINAL = [200, 204, 413, 422, 499, 500];
36
+ const startRes = await client.call("set", {
37
+ path: `run://${alias}`,
38
+ body: question,
39
+ attributes: {
40
+ model: "grok",
41
+ mode: "ask",
42
+ noRepo: true,
43
+ noInteraction: true,
44
+ noWeb: true,
45
+ noProposals: true,
46
+ },
47
+ });
48
+
49
+ const deadline = Date.now() + 600_000;
50
+ while (Date.now() < deadline) {
51
+ const row = await tdb.db.get_run_by_alias.get({ alias });
52
+ if (TERMINAL.includes(row.status)) break;
53
+ await new Promise((r) => setTimeout(r, 500));
54
+ }
55
+
56
+ const runRow = await tdb.db.get_run_by_alias.get({ alias });
57
+ const entries = await tdb.db.get_known_entries.all({ run_id: runRow.id });
58
+ const reasoning = entries
59
+ .filter((e) => e.scheme === "reasoning")
60
+ .toSorted((a, b) => b.turn - a.turn)[0];
61
+ const assistant = entries
62
+ .filter((e) => e.scheme === "assistant")
63
+ .toSorted((a, b) => b.turn - a.turn)[0];
64
+
65
+ if (reasoning) {
66
+ console.log("=== REASONING ===");
67
+ console.log(reasoning.body);
68
+ console.log("");
69
+ }
70
+ if (assistant) {
71
+ console.log("=== ANSWER ===");
72
+ console.log(assistant.body);
73
+ }
74
+
75
+ await client.close();
76
+ await tserver.stop();
77
+ await tdb.cleanup();
package/service.js CHANGED
@@ -1,9 +1,8 @@
1
- import { mkdirSync, readdirSync } from "node:fs";
1
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { spawnSync } from "node:child_process";
5
5
 
6
- // Helper to expand ~ in paths since node --env-file doesn't do it
7
6
  // 0. Pre-flight Check: Environment and Dependencies
8
7
  const rummyHome = process.env.RUMMY_HOME;
9
8
 
@@ -12,13 +11,54 @@ if (!rummyHome) {
12
11
  process.exit(1);
13
12
  }
14
13
 
14
+ // 0a. Env resolution: local project config wins over RUMMY_HOME.
15
+ //
16
+ // If CWD has a rummy-shaped `.env.example` (contains any RUMMY_* var),
17
+ // this is an authoritative local config — the npm script's
18
+ // --env-file-if-exists flags already loaded it, and RUMMY_HOME would
19
+ // only pollute it with machine-wide config that doesn't belong to this
20
+ // instance.
21
+ //
22
+ // If CWD has no rummy-shaped config, fall back to
23
+ // `${RUMMY_HOME}/.env.example` (canonical defaults, shipped with the
24
+ // package) → `${RUMMY_HOME}/.env` (user overrides). On first run we
25
+ // seed the bundled .env.example into RUMMY_HOME so the user has
26
+ // something to edit.
27
+ //
28
+ // This makes multiple rummy instances on the same box cleanly
29
+ // independent: each owns its own .env.example in its CWD.
30
+ {
31
+ const cwdExample = join(process.cwd(), ".env.example");
32
+ const isLocalRummyConfig =
33
+ existsSync(cwdExample) && /^\s*(#\s*)?RUMMY_\w+\s*=/m.test(readFileSync(cwdExample, "utf8"));
34
+
35
+ if (!isLocalRummyConfig) {
36
+ mkdirSync(rummyHome, { recursive: true });
37
+ const homeExample = join(rummyHome, ".env.example");
38
+ const homeEnv = join(rummyHome, ".env");
39
+ const bundledExample = fileURLToPath(new URL("./.env.example", import.meta.url));
40
+ if (!existsSync(homeExample) && existsSync(bundledExample)) {
41
+ copyFileSync(bundledExample, homeExample);
42
+ console.log(`[RUMMY] Seeded ${homeExample} from package defaults.`);
43
+ }
44
+ for (const path of [homeExample, homeEnv]) {
45
+ if (!existsSync(path)) continue;
46
+ try {
47
+ process.loadEnvFile(path);
48
+ } catch (err) {
49
+ console.warn(`[RUMMY] Failed to load ${path}: ${err.message}`);
50
+ }
51
+ }
52
+ }
53
+ }
54
+
15
55
  // Check for optional system dependencies
16
56
  const gitCheck = spawnSync("git", ["--version"]);
17
57
  if (gitCheck.error || gitCheck.status !== 0) {
18
58
  console.warn("[RUMMY] WARNING: 'git' not found. File tracking will use manual activation only.");
19
59
  }
20
60
 
21
- let SqlRite, SocketServer, registerPlugins, initPlugins, createHooks, RpcRegistry;
61
+ let SqlRite, SocketServer, registerPlugins, initPlugins, createHooks;
22
62
  try {
23
63
  SqlRite = (await import("@possumtech/sqlrite")).default;
24
64
  SocketServer = (await import("./src/server/SocketServer.js")).default;
@@ -26,7 +66,6 @@ try {
26
66
  registerPlugins = pluginIndex.registerPlugins;
27
67
  initPlugins = pluginIndex.initPlugins;
28
68
  createHooks = (await import("./src/hooks/Hooks.js")).default;
29
- RpcRegistry = (await import("./src/server/RpcRegistry.js")).default;
30
69
  } catch (err) {
31
70
  if (err.code === "ERR_MODULE_NOT_FOUND") {
32
71
  console.error("RUMMY Dependency Error: node_modules not found or incomplete.");
@@ -40,7 +79,6 @@ async function main() {
40
79
  // 1. Initialize Hooks (Agnostic Engine)
41
80
  const debug = process.env.RUMMY_DEBUG === "true";
42
81
  const hooks = createHooks(debug);
43
- hooks.rpc.registry = new RpcRegistry();
44
82
 
45
83
  // 2. Resolve Directories
46
84
  const userPluginsDir = join(rummyHome, "plugins");
@@ -50,7 +88,10 @@ async function main() {
50
88
  mkdirSync(userPluginsDir, { recursive: true });
51
89
 
52
90
  // 4. Register Plugins
53
- await registerPlugins([pluginsDir, userPluginsDir], hooks);
91
+ const pluginInstances = await registerPlugins(
92
+ [pluginsDir, userPluginsDir],
93
+ hooks,
94
+ );
54
95
 
55
96
  // 5. Bootstrap Persistence
56
97
  const dbPath = process.env.RUMMY_DB_PATH;
@@ -71,8 +112,8 @@ async function main() {
71
112
  },
72
113
  });
73
114
 
74
- // 6. Initialize plugins (inject DB, register schemes)
75
- await initPlugins(db, null, hooks);
115
+ // 6. Initialize plugins (register schemes)
116
+ await initPlugins(db, hooks, pluginInstances);
76
117
 
77
118
  // 7. Bootstrap models from env vars
78
119
  {
@@ -99,7 +140,7 @@ async function main() {
99
140
  const { statSync } = await import("node:fs");
100
141
  try {
101
142
  const dbSizeBefore = statSync(dbPath).size;
102
- const retentionDays = Number.parseInt(process.env.RUMMY_RETENTION_DAYS || "31", 10);
143
+ const retentionDays = Number.parseInt(process.env.RUMMY_RETENTION_DAYS, 10);
103
144
  await db.purge_old_runs.run({ retention_days: retentionDays });
104
145
  const dbSizeAfter = statSync(dbPath).size;
105
146
  const dbSizeMB = (dbSizeAfter / 1024 / 1024).toFixed(2);