@possumtech/rummy 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.env.example +31 -5
  2. package/BENCH_ENVIRONMENT.md +230 -0
  3. package/CLIENT_INTERFACE.md +396 -0
  4. package/PLUGINS.md +93 -1
  5. package/SPEC.md +389 -28
  6. package/bin/postinstall.js +2 -2
  7. package/bin/rummy.js +2 -2
  8. package/last_run.txt +5617 -0
  9. package/migrations/001_initial_schema.sql +2 -1
  10. package/package.json +13 -9
  11. package/scriptify/ask_run.js +77 -0
  12. package/scriptify/cache_probe.js +66 -0
  13. package/scriptify/cache_probe_grok.js +74 -0
  14. package/service.js +22 -11
  15. package/src/agent/AgentLoop.js +62 -157
  16. package/src/agent/ContextAssembler.js +2 -9
  17. package/src/agent/Entries.js +54 -98
  18. package/src/agent/ProjectAgent.js +4 -11
  19. package/src/agent/TurnExecutor.js +48 -83
  20. package/src/agent/XmlParser.js +247 -273
  21. package/src/agent/budget.js +5 -28
  22. package/src/agent/config.js +38 -0
  23. package/src/agent/errors.js +7 -13
  24. package/src/agent/httpStatus.js +1 -19
  25. package/src/agent/known_queries.sql +1 -1
  26. package/src/agent/known_store.sql +12 -2
  27. package/src/agent/materializeContext.js +15 -18
  28. package/src/agent/pathEncode.js +5 -0
  29. package/src/agent/rummyHome.js +9 -0
  30. package/src/agent/runs.sql +37 -0
  31. package/src/agent/tokens.js +7 -7
  32. package/src/hooks/HookRegistry.js +1 -16
  33. package/src/hooks/Hooks.js +8 -33
  34. package/src/hooks/PluginContext.js +3 -21
  35. package/src/hooks/RpcRegistry.js +1 -4
  36. package/src/hooks/RummyContext.js +6 -16
  37. package/src/hooks/ToolRegistry.js +5 -15
  38. package/src/llm/LlmProvider.js +41 -33
  39. package/src/llm/errors.js +41 -4
  40. package/src/llm/openaiStream.js +125 -0
  41. package/src/llm/retry.js +109 -0
  42. package/src/plugins/budget/budget.js +55 -76
  43. package/src/plugins/cli/README.md +87 -0
  44. package/src/plugins/cli/bin.js +61 -0
  45. package/src/plugins/cli/cli.js +120 -0
  46. package/src/plugins/env/README.md +2 -1
  47. package/src/plugins/env/env.js +4 -6
  48. package/src/plugins/env/envDoc.md +2 -2
  49. package/src/plugins/error/error.js +23 -23
  50. package/src/plugins/file/file.js +2 -22
  51. package/src/plugins/get/get.js +12 -34
  52. package/src/plugins/get/getDoc.md +8 -6
  53. package/src/plugins/hedberg/edits.js +1 -11
  54. package/src/plugins/hedberg/hedberg.js +3 -26
  55. package/src/plugins/hedberg/normalize.js +1 -5
  56. package/src/plugins/hedberg/patterns.js +4 -15
  57. package/src/plugins/hedberg/sed.js +1 -7
  58. package/src/plugins/helpers.js +28 -20
  59. package/src/plugins/index.js +25 -41
  60. package/src/plugins/instructions/README.md +18 -0
  61. package/src/plugins/instructions/instructions.js +97 -38
  62. package/src/plugins/instructions/instructions.md +24 -15
  63. package/src/plugins/instructions/instructions_104.md +5 -4
  64. package/src/plugins/instructions/instructions_105.md +29 -36
  65. package/src/plugins/instructions/instructions_106.md +22 -0
  66. package/src/plugins/instructions/instructions_107.md +17 -0
  67. package/src/plugins/instructions/instructions_108.md +0 -8
  68. package/src/plugins/known/README.md +26 -6
  69. package/src/plugins/known/known.js +37 -34
  70. package/src/plugins/log/README.md +2 -2
  71. package/src/plugins/log/log.js +27 -34
  72. package/src/plugins/ollama/ollama.js +50 -66
  73. package/src/plugins/openai/openai.js +26 -44
  74. package/src/plugins/openrouter/openrouter.js +28 -52
  75. package/src/plugins/policy/README.md +8 -2
  76. package/src/plugins/policy/policy.js +8 -21
  77. package/src/plugins/prompt/README.md +22 -0
  78. package/src/plugins/prompt/prompt.js +14 -16
  79. package/src/plugins/rm/rm.js +5 -2
  80. package/src/plugins/rm/rmDoc.md +4 -4
  81. package/src/plugins/rpc/README.md +2 -1
  82. package/src/plugins/rpc/rpc.js +62 -48
  83. package/src/plugins/set/README.md +5 -1
  84. package/src/plugins/set/set.js +23 -33
  85. package/src/plugins/set/setDoc.md +1 -1
  86. package/src/plugins/sh/README.md +2 -1
  87. package/src/plugins/sh/sh.js +5 -11
  88. package/src/plugins/sh/shDoc.md +2 -2
  89. package/src/plugins/stream/README.md +6 -5
  90. package/src/plugins/stream/stream.js +6 -35
  91. package/src/plugins/telemetry/telemetry.js +26 -19
  92. package/src/plugins/think/think.js +4 -7
  93. package/src/plugins/unknown/unknown.js +8 -13
  94. package/src/plugins/update/update.js +42 -25
  95. package/src/plugins/update/updateDoc.md +3 -3
  96. package/src/plugins/xai/xai.js +30 -20
  97. package/src/plugins/yolo/yolo.js +159 -0
  98. package/src/server/ClientConnection.js +17 -47
  99. package/src/server/SocketServer.js +14 -14
  100. package/src/server/protocol.js +1 -10
  101. package/src/sql/functions/slugify.js +5 -7
  102. package/src/sql/v_model_context.sql +4 -11
  103. package/turns/cli_1777462658211/turn_001.txt +772 -0
  104. package/turns/cli_1777462658211/turn_002.txt +606 -0
  105. package/turns/cli_1777462658211/turn_003.txt +667 -0
  106. package/turns/cli_1777462658211/turn_004.txt +297 -0
  107. package/turns/cli_1777462658211/turn_005.txt +301 -0
  108. package/turns/cli_1777462658211/turn_006.txt +262 -0
  109. package/turns/cli_1777465095132/turn_001.txt +715 -0
  110. package/turns/cli_1777465095132/turn_002.txt +236 -0
  111. package/turns/cli_1777465095132/turn_003.txt +287 -0
  112. package/turns/cli_1777465095132/turn_004.txt +694 -0
  113. package/turns/cli_1777465095132/turn_005.txt +422 -0
  114. package/turns/cli_1777465095132/turn_006.txt +365 -0
  115. package/turns/cli_1777465095132/turn_007.txt +885 -0
  116. package/turns/cli_1777465095132/turn_008.txt +1277 -0
  117. package/turns/cli_1777465095132/turn_009.txt +736 -0
@@ -218,7 +218,8 @@ CREATE TABLE IF NOT EXISTS turn_context (
218
218
  state IN ('proposed', 'streaming', 'resolved', 'failed', 'cancelled')
219
219
  )
220
220
  , outcome TEXT
221
- , visibility TEXT NOT NULL CHECK (visibility IN ('visible', 'summarized'))
221
+ -- 'archived' permitted; see prompt plugin README for the exception.
222
+ , visibility TEXT NOT NULL CHECK (visibility IN ('visible', 'summarized', 'archived'))
222
223
  , body TEXT NOT NULL DEFAULT ''
223
224
  , attributes JSON NOT NULL DEFAULT '{}' CHECK (json_valid(attributes))
224
225
  , category TEXT NOT NULL DEFAULT 'logging'
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@possumtech/rummy",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Relational Unknowns Memory Management Yoke",
5
5
  "keywords": [
6
6
  "llm"
7
7
  ],
8
8
  "bin": {
9
- "rummy": "./bin/rummy.js"
9
+ "rummy": "./bin/rummy.js",
10
+ "rummy-cli": "./src/plugins/cli/bin.js"
10
11
  },
11
12
  "publishConfig": {
12
13
  "access": "public"
@@ -41,16 +42,19 @@
41
42
  "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
43
  "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
44
  "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' --",
45
+ "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
46
  "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
47
  "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/*/",
48
+ "test:swe:setup": "bash test/swe/setup.sh",
49
+ "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",
50
+ "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' --",
51
+ "test:swe:eval": "bash -c 'cd test/swe && source .venv/bin/activate && python evaluate.py \"$@\"' --",
52
+ "test:swe:baseline": "bash -c 'cd test/swe && source .venv/bin/activate && python baseline.py \"$@\"' --",
53
53
  "test:lme:clean": "rm -rf test/lme/results/*/",
54
+ "test:swe:clean": "rm -rf test/swe/results/*/ test/swe/repos/",
55
+ "test:tbench:setup": "bash -c 'set -a; source .env.tbench; set +a; bash test/tbench/setup.sh'",
56
+ "test:tbench": "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.tbench test/tbench/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/tbench_$(date +%Y%m%dT%H%M%S).log' --",
57
+ "test:tbench:clean": "rm -rf test/tbench/results/*/",
54
58
  "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
59
  "test:demo": "node --env-file-if-exists=.env.example --env-file-if-exists=.env bin/demo.js",
56
60
  "test:spec": "node test/spec-coverage.js"
@@ -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();
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ // Probe llama-server cache behavior. Send variations of the same request
3
+ // and inspect cached_tokens in the response usage block to determine
4
+ // whether caching is token-prefix or message-hash level.
5
+
6
+ const URL = "http://127.0.0.1:11435/v1/chat/completions";
7
+ const MODEL = "gemma-4-26B-A4B-it-UD-Q3_K_XL.gguf";
8
+
9
+ async function probe(label, system, user) {
10
+ const body = {
11
+ model: MODEL,
12
+ messages: [
13
+ { role: "system", content: system },
14
+ { role: "user", content: user },
15
+ ],
16
+ think: true,
17
+ temperature: 0.5,
18
+ };
19
+ const res = await fetch(URL, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json" },
22
+ body: JSON.stringify(body),
23
+ });
24
+ const data = await res.json();
25
+ const u = data.usage || {};
26
+ const cached =
27
+ u.prompt_tokens_details?.cached_tokens ??
28
+ u.cached_tokens ??
29
+ 0;
30
+ console.log(
31
+ `[${label}] prompt_tokens=${u.prompt_tokens ?? "?"} cached_tokens=${cached} system_chars=${system.length} user_chars=${user.length}`,
32
+ );
33
+ }
34
+
35
+ const STATIC_SYSTEM_BASE = `You are a helpful assistant.
36
+
37
+ Tools available:
38
+ - foo: does foo
39
+ - bar: does bar
40
+ - baz: does baz
41
+
42
+ Always be concise.`;
43
+
44
+ const ADDITION_A = "\n\n<context>\n<known path=\"k1\">first known fact</known>\n</context>";
45
+ const ADDITION_B = "\n\n<context>\n<known path=\"k1\">first known fact</known>\n<known path=\"k2\">second known fact</known>\n</context>";
46
+ const ADDITION_C = "\n\n<context>\n<known path=\"k2\">second known fact</known>\n<known path=\"k1\">first known fact</known>\n</context>";
47
+
48
+ const USER_A = "Hello.";
49
+
50
+ console.log("=== Run 1: baseline (cold, then immediate repeat) ===");
51
+ await probe("1a baseline cold", STATIC_SYSTEM_BASE, USER_A);
52
+ await probe("1b same-as-1a ", STATIC_SYSTEM_BASE, USER_A);
53
+
54
+ console.log("\n=== Run 2: same base, then base + appended context (prefix unchanged) ===");
55
+ await probe("2a base only ", STATIC_SYSTEM_BASE, USER_A);
56
+ await probe("2b base + 1 entry", STATIC_SYSTEM_BASE + ADDITION_A, USER_A);
57
+ await probe("2c base + 2 entries", STATIC_SYSTEM_BASE + ADDITION_B, USER_A);
58
+
59
+ console.log("\n=== Run 3: prefix change (entries reordered, same body) ===");
60
+ await probe("3a base + 2 entries (k1,k2)", STATIC_SYSTEM_BASE + ADDITION_B, USER_A);
61
+ await probe("3b base + 2 entries (k2,k1) reordered", STATIC_SYSTEM_BASE + ADDITION_C, USER_A);
62
+
63
+ console.log("\n=== Run 4: small mid-prefix change ===");
64
+ const MIDDIFF = STATIC_SYSTEM_BASE.replace("baz", "qux");
65
+ await probe("4a stable base ", STATIC_SYSTEM_BASE, USER_A);
66
+ await probe("4b changed baz→qux", MIDDIFF, USER_A);
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ // Same probe as cache_probe.js but against OpenRouter's grok endpoint.
3
+ // If cached_tokens behaves sanely (incremental matches preserve prefix),
4
+ // then llama-server's behavior was the local anomaly.
5
+
6
+ const URL = `${process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1"}/chat/completions`;
7
+ const MODEL = "x-ai/grok-4.1-fast";
8
+
9
+ if (!process.env.OPENROUTER_API_KEY) {
10
+ console.error("OPENROUTER_API_KEY required");
11
+ process.exit(1);
12
+ }
13
+
14
+ async function probe(label, system, user) {
15
+ const body = {
16
+ model: MODEL,
17
+ messages: [
18
+ { role: "system", content: system },
19
+ { role: "user", content: user },
20
+ ],
21
+ include_reasoning: true,
22
+ temperature: 0.5,
23
+ };
24
+ const res = await fetch(URL, {
25
+ method: "POST",
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
29
+ },
30
+ body: JSON.stringify(body),
31
+ });
32
+ const data = await res.json();
33
+ const u = data.usage || {};
34
+ const cached =
35
+ u.prompt_tokens_details?.cached_tokens ??
36
+ u.cached_tokens ??
37
+ u.cache_read_input_tokens ??
38
+ 0;
39
+ console.log(
40
+ `[${label}] prompt_tokens=${u.prompt_tokens ?? "?"} cached_tokens=${cached} system_chars=${system.length}`,
41
+ );
42
+ }
43
+
44
+ const STATIC_SYSTEM_BASE = `You are a helpful assistant.
45
+
46
+ Tools available:
47
+ - foo: does foo
48
+ - bar: does bar
49
+ - baz: does baz
50
+
51
+ Always be concise.`;
52
+
53
+ const ADDITION_A = "\n\n<context>\n<known path=\"k1\">first known fact</known>\n</context>";
54
+ const ADDITION_B = "\n\n<context>\n<known path=\"k1\">first known fact</known>\n<known path=\"k2\">second known fact</known>\n</context>";
55
+ const ADDITION_C = "\n\n<context>\n<known path=\"k2\">second known fact</known>\n<known path=\"k1\">first known fact</known>\n</context>";
56
+
57
+ const USER = "Hello.";
58
+
59
+ console.log("=== Run 1: baseline (cold, then immediate repeat) ===");
60
+ await probe("1a baseline cold", STATIC_SYSTEM_BASE, USER);
61
+ await probe("1b same-as-1a ", STATIC_SYSTEM_BASE, USER);
62
+
63
+ console.log("\n=== Run 2: appended context (prefix unchanged) ===");
64
+ await probe("2a base + 1 ", STATIC_SYSTEM_BASE + ADDITION_A, USER);
65
+ await probe("2b base + 2 ", STATIC_SYSTEM_BASE + ADDITION_B, USER);
66
+
67
+ console.log("\n=== Run 3: reordered (entries shuffled) ===");
68
+ await probe("3a (k1,k2) ", STATIC_SYSTEM_BASE + ADDITION_B, USER);
69
+ await probe("3b (k2,k1) ", STATIC_SYSTEM_BASE + ADDITION_C, USER);
70
+
71
+ console.log("\n=== Run 4: mid-prefix character change ===");
72
+ const MIDDIFF = STATIC_SYSTEM_BASE.replace("baz", "qux");
73
+ await probe("4a stable base ", STATIC_SYSTEM_BASE, USER);
74
+ await probe("4b baz→qux ", MIDDIFF, USER);
package/service.js CHANGED
@@ -43,11 +43,7 @@ if (!rummyHome) {
43
43
  }
44
44
  for (const path of [homeExample, homeEnv]) {
45
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
- }
46
+ process.loadEnvFile(path);
51
47
  }
52
48
  }
53
49
  }
@@ -136,11 +132,21 @@ async function main() {
136
132
  }
137
133
  }
138
134
 
139
- // 6b. Database Hygiene
135
+ // 6b. Database Hygiene — opt-in via RUMMY_RETENTION_DAYS.
140
136
  const { statSync } = await import("node:fs");
141
- try {
137
+ const retentionRaw = process.env.RUMMY_RETENTION_DAYS;
138
+ if (retentionRaw == null || retentionRaw === "") {
139
+ const dbSizeMB = (statSync(dbPath).size / 1024 / 1024).toFixed(2);
140
+ console.log(`[RUMMY] DB size: ${dbSizeMB}MB`);
141
+ } else {
142
+ const retentionDays = Number.parseInt(retentionRaw, 10);
143
+ if (!Number.isInteger(retentionDays) || retentionDays < 0) {
144
+ throw new Error(
145
+ `Invalid RUMMY_RETENTION_DAYS=${JSON.stringify(retentionRaw)} ` +
146
+ "(expected non-negative integer)",
147
+ );
148
+ }
142
149
  const dbSizeBefore = statSync(dbPath).size;
143
- const retentionDays = Number.parseInt(process.env.RUMMY_RETENTION_DAYS, 10);
144
150
  await db.purge_old_runs.run({ retention_days: retentionDays });
145
151
  const dbSizeAfter = statSync(dbPath).size;
146
152
  const dbSizeMB = (dbSizeAfter / 1024 / 1024).toFixed(2);
@@ -153,8 +159,6 @@ async function main() {
153
159
  if (dbSizeAfter > 100 * 1024 * 1024) {
154
160
  console.warn(`[RUMMY] WARNING: Database exceeds 100MB. Consider manual cleanup.`);
155
161
  }
156
- } catch (err) {
157
- console.warn(`[RUMMY] Hygiene skipped: ${err.message}`);
158
162
  }
159
163
 
160
164
  // 6b. Abort stuck runs (can't be running if the server just started)
@@ -164,8 +168,15 @@ async function main() {
164
168
  console.log(`[RUMMY] Recovered ${aborted.changes} stuck run(s)`);
165
169
  }
166
170
 
171
+ // 6c. Boot complete — DB open, plugins inited, models loaded,
172
+ // hygiene done. Plugins that need a one-shot post-boot action
173
+ // (e.g. the cli plugin firing a programmatic run) subscribe to
174
+ // this event. Fires BEFORE SocketServer so RPC clients can't
175
+ // race a one-shot run still being set up.
176
+ await hooks.boot.completed.emit({ db, hooks });
177
+
167
178
  // 7. Start RPC Server
168
- const port = Number.parseInt(process.env.PORT);
179
+ const port = Number.parseInt(process.env.RUMMY_PORT);
169
180
  const server = new SocketServer(db, { port, hooks });
170
181
 
171
182
  server.on("error", (err) => {