@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
@@ -1,16 +1,12 @@
1
+ import config from "../../agent/config.js";
1
2
  import msg from "../../agent/messages.js";
3
+ import { parseRetryAfter } from "../../llm/errors.js";
2
4
 
3
- const FETCH_TIMEOUT = Number(process.env.RUMMY_FETCH_TIMEOUT);
4
- if (!FETCH_TIMEOUT) throw new Error("RUMMY_FETCH_TIMEOUT must be set");
5
+ const { FETCH_TIMEOUT } = config;
5
6
 
6
7
  const PROVIDER = "xai";
7
8
 
8
- /**
9
- * xAI (Grok) LLM provider plugin. Registers with hooks.llm.providers if
10
- * XAI_BASE_URL is set; inert otherwise. Handles model aliases of the
11
- * form `xai/{modelName}`. Normalizes xAI's distinct response shape
12
- * into the common OpenAI-shaped envelope.
13
- */
9
+ // Inert unless XAI_BASE_URL set; xai/{model} aliases; normalizes to OpenAI envelope.
14
10
  export default class Xai {
15
11
  #baseUrl;
16
12
  #apiKey;
@@ -39,6 +35,11 @@ export default class Xai {
39
35
  const body = { model, input: messages };
40
36
  if (options.temperature !== undefined)
41
37
  body.temperature = options.temperature;
38
+ // xAI auto-caches per-server; stable prompt_cache_key keeps a multi-
39
+ // turn run pinned to the same backend so the cached prefix actually
40
+ // hits. Without this, requests load-balance and cache_tokens stays
41
+ // near-zero. See https://docs.x.ai/developers/advanced-api-usage/prompt-caching.
42
+ if (options.runAlias) body.prompt_cache_key = options.runAlias;
42
43
 
43
44
  const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT);
44
45
  const signal = options.signal
@@ -56,15 +57,27 @@ export default class Xai {
56
57
  });
57
58
 
58
59
  if (!response.ok) {
59
- const error = await response.text();
60
+ const errorBody = await response.text();
61
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
60
62
  if (response.status === 401 || response.status === 403) {
61
- throw new Error(
62
- msg("error.xai_auth", { status: `${response.status} - ${error}` }),
63
+ const err = new Error(
64
+ msg("error.xai_auth", {
65
+ status: `${response.status} - ${errorBody}`,
66
+ }),
63
67
  );
68
+ err.status = response.status;
69
+ err.body = errorBody;
70
+ throw err;
64
71
  }
65
- throw new Error(
66
- msg("error.xai_api", { status: `${response.status} - ${error}` }),
72
+ const err = new Error(
73
+ msg("error.xai_api", {
74
+ status: `${response.status} - ${errorBody}`,
75
+ }),
67
76
  );
77
+ err.status = response.status;
78
+ err.body = errorBody;
79
+ err.retryAfter = retryAfter;
80
+ throw err;
68
81
  }
69
82
 
70
83
  return this.#normalize(await response.json());
@@ -133,12 +146,11 @@ export default class Xai {
133
146
  const modelsUrl = this.#baseUrl.replace(/\/responses$/, "/models");
134
147
  const res = await fetch(modelsUrl, {
135
148
  headers: { Authorization: `Bearer ${this.#apiKey}` },
136
- signal: AbortSignal.timeout(5000),
149
+ signal: AbortSignal.timeout(FETCH_TIMEOUT),
137
150
  });
138
151
  if (res.ok) {
139
152
  const data = await res.json();
140
- // xAI's /models returns either { data: [...] } or { models: [...] }
141
- // depending on the API version; accept either and crash otherwise.
153
+ // xAI /models response shape varies by API version.
142
154
  let models;
143
155
  if (data.data) models = data.data;
144
156
  else if (data.models) models = data.models;
@@ -156,12 +168,10 @@ export default class Xai {
156
168
  /\/responses$/,
157
169
  `/language-models/${model}`,
158
170
  );
159
- // Optional endpoint probe. If the network call fails (404 on older
160
- // API versions, timeout, etc.) we fall through to the next strategy
161
- // below; a terminal throw fires if no strategy resolves.
171
+ // Optional probe; failure falls through to terminal throw below.
162
172
  const langRes = await fetch(langUrl, {
163
173
  headers: { Authorization: `Bearer ${this.#apiKey}` },
164
- signal: AbortSignal.timeout(5000),
174
+ signal: AbortSignal.timeout(FETCH_TIMEOUT),
165
175
  }).catch(() => null);
166
176
  if (langRes?.ok) {
167
177
  const langData = await langRes.json();
@@ -0,0 +1,159 @@
1
+ import { spawn } from "node:child_process";
2
+ import { logPathToDataBase } from "../helpers.js";
3
+
4
+ const SH_PATH_RE = /^log:\/\/turn_\d+\/(sh|env)\//;
5
+
6
+ // Auto-resolves proposals + spawns sh/env locally for runs started with yolo:true. SPEC #yolo_mode.
7
+ export default class Yolo {
8
+ constructor(core) {
9
+ this.core = core;
10
+ core.hooks.proposal.pending.on(this.#onPending.bind(this));
11
+ }
12
+
13
+ async #onPending({ proposed, rummy }) {
14
+ if (!rummy?.yolo) return;
15
+ for (const p of proposed) {
16
+ // Resolve first so sh/env's post-accept seeds channels before we stream into them.
17
+ await this.#serverResolve(rummy, p.path);
18
+ if (SH_PATH_RE.test(p.path)) {
19
+ await this.#executeShellProposal(rummy, p.path);
20
+ }
21
+ }
22
+ }
23
+
24
+ // Inline mirror of AgentLoop.resolve()'s accept path.
25
+ async #serverResolve(rummy, path) {
26
+ const runId = rummy.runId;
27
+ const entries = rummy.entries;
28
+ const db = rummy.db;
29
+ const runRow = await db.get_run_by_id.get({ id: runId });
30
+ const project = await db.get_project_by_id.get({ id: runRow.project_id });
31
+ const attrs = await entries.getAttributes(runId, path);
32
+ const ctx = {
33
+ runId,
34
+ runRow,
35
+ projectId: runRow.project_id,
36
+ projectRoot: project?.project_root,
37
+ path,
38
+ attrs,
39
+ output: "",
40
+ db,
41
+ entries,
42
+ };
43
+
44
+ const veto = await this.core.hooks.proposal.accepting.filter(null, ctx);
45
+ if (veto?.allow === false) {
46
+ await entries.set({
47
+ runId,
48
+ path,
49
+ state: "failed",
50
+ outcome: veto.outcome,
51
+ body: veto.body,
52
+ });
53
+ return;
54
+ }
55
+
56
+ const resolvedBody = await this.core.hooks.proposal.content.filter("", ctx);
57
+ const existing = await entries.getState(runId, path);
58
+ const existingTurn = existing?.turn === undefined ? 0 : existing.turn;
59
+ await entries.set({
60
+ runId,
61
+ turn: existingTurn,
62
+ path,
63
+ state: "resolved",
64
+ body: resolvedBody,
65
+ });
66
+ await this.core.hooks.proposal.accepted.emit({ ...ctx, resolvedBody });
67
+ }
68
+
69
+ // Spawn locally and stream into {dataBase}_{1,2}; mirrors stream/stream-completed RPC.
70
+ async #executeShellProposal(rummy, logPath) {
71
+ const runId = rummy.runId;
72
+ const entries = rummy.entries;
73
+ const db = rummy.db;
74
+ const runRow = await db.get_run_by_id.get({ id: runId });
75
+ const project = await db.get_project_by_id.get({ id: runRow.project_id });
76
+ const projectRoot = project?.project_root;
77
+ if (!projectRoot) return;
78
+
79
+ const attrs = await entries.getAttributes(runId, logPath);
80
+ const command = attrs?.command || attrs?.summary;
81
+ if (!command) return;
82
+
83
+ const dataBase = logPathToDataBase(logPath);
84
+ if (!dataBase) return;
85
+ const stdoutPath = `${dataBase}_1`;
86
+ const stderrPath = `${dataBase}_2`;
87
+
88
+ const start = Date.now();
89
+ const child = spawn("bash", ["-lc", command], {
90
+ cwd: projectRoot,
91
+ env: process.env,
92
+ });
93
+ // Buffer + write-once-on-exit; async appends would race the terminal-state transition.
94
+ const stdoutChunks = [];
95
+ const stderrChunks = [];
96
+ child.stdout.on("data", (data) => stdoutChunks.push(data.toString()));
97
+ child.stderr.on("data", (data) => stderrChunks.push(data.toString()));
98
+
99
+ await new Promise((resolve) => {
100
+ child.on("close", async (code) => {
101
+ const stdoutBody = stdoutChunks.join("");
102
+ const stderrBody = stderrChunks.join("");
103
+ if (stdoutBody) {
104
+ try {
105
+ await entries.set({
106
+ runId,
107
+ path: stdoutPath,
108
+ body: stdoutBody,
109
+ append: true,
110
+ });
111
+ } catch {}
112
+ }
113
+ if (stderrBody) {
114
+ try {
115
+ await entries.set({
116
+ runId,
117
+ path: stderrPath,
118
+ body: stderrBody,
119
+ append: true,
120
+ });
121
+ } catch {}
122
+ }
123
+ const exitCode = code === null ? 130 : code;
124
+ const duration = `${Math.round((Date.now() - start) / 1000)}s`;
125
+ const terminalState = exitCode === 0 ? "resolved" : "failed";
126
+ const outcome = exitCode === 0 ? null : `exit:${exitCode}`;
127
+ // body=undefined preserves streamed content; body="" would wipe it.
128
+ for (const path of [stdoutPath, stderrPath]) {
129
+ try {
130
+ await entries.set({
131
+ runId,
132
+ path,
133
+ state: terminalState,
134
+ outcome,
135
+ });
136
+ } catch {}
137
+ }
138
+ try {
139
+ const channels = await entries.getEntriesByPattern(
140
+ runId,
141
+ `${dataBase}_*`,
142
+ null,
143
+ );
144
+ const summary = channels
145
+ .map((c) => `${c.path} (${c.tokens} tokens)`)
146
+ .join(", ");
147
+ const exitLabel = exitCode === 0 ? "exit=0" : `exit=${exitCode}`;
148
+ await entries.set({
149
+ runId,
150
+ path: logPath,
151
+ state: "resolved",
152
+ body: `ran '${command}', ${exitLabel} (${duration}). Output: ${summary}`,
153
+ });
154
+ } catch {}
155
+ resolve();
156
+ });
157
+ });
158
+ }
159
+ }
@@ -23,8 +23,7 @@ export default class ClientConnection {
23
23
 
24
24
  this.#ws.on("message", (data) => this.#handleMessage(data));
25
25
  this.#ws.on("close", () => {
26
- // Fire-and-forget: the Promise is cached by `shutdown()` so
27
- // server-initiated close can await the same work.
26
+ // Fire-and-forget; shutdown() caches the Promise for server-initiated close to await.
28
27
  this.shutdown().catch((err) => {
29
28
  console.warn(`[RUMMY] shutdown on ws close failed: ${err.message}`);
30
29
  });
@@ -33,25 +32,6 @@ export default class ClientConnection {
33
32
  this.#setupNotifications();
34
33
  }
35
34
 
36
- #onProgress = (payload) => {
37
- if (payload.projectId === this.#context.projectId) {
38
- this.#sendNotification("run/progress", {
39
- run: payload.run,
40
- turn: payload.turn,
41
- status: payload.status,
42
- });
43
- }
44
- };
45
-
46
- #onProposal = (payload) => {
47
- if (payload.projectId === this.#context.projectId) {
48
- this.#sendNotification("run/proposal", {
49
- run: payload.run,
50
- proposed: payload.proposed,
51
- });
52
- }
53
- };
54
-
55
35
  #onRender = (payload) => {
56
36
  if (payload.projectId === this.#context.projectId) {
57
37
  this.#sendNotification("ui/render", {
@@ -80,44 +60,35 @@ export default class ClientConnection {
80
60
  }
81
61
  };
82
62
 
83
- #onState = (payload) => {
84
- if (payload.projectId === this.#context.projectId) {
85
- this.#sendNotification("run/state", {
86
- run: payload.run,
87
- turn: payload.turn,
88
- status: payload.status,
89
- summary: payload.summary,
90
- history: payload.history,
91
- unknowns: payload.unknowns,
92
- telemetry: payload.telemetry,
93
- });
94
- }
63
+ // Pulse: any entry write in this client's project. Content-free hint
64
+ // client reconciles via getEntriesByPattern with `since`.
65
+ #onEntryChanged = async ({ runId, path, changeType }) => {
66
+ if (this.#context.projectId == null) return;
67
+ const run = await this.#db.get_run_by_id.get({ id: runId });
68
+ if (!run || run.project_id !== this.#context.projectId) return;
69
+ this.#sendNotification("run/changed", {
70
+ run: run.alias,
71
+ runId,
72
+ path,
73
+ changeType,
74
+ });
95
75
  };
96
76
 
97
77
  #setupNotifications() {
98
- this.#hooks.run.progress.on(this.#onProgress);
99
- this.#hooks.proposal.pending.on(this.#onProposal);
100
78
  this.#hooks.ui.render.on(this.#onRender);
101
79
  this.#hooks.ui.notify.on(this.#onNotify);
102
- this.#hooks.run.state.on(this.#onState);
103
80
  this.#hooks.stream.cancelled.on(this.#onStreamCancelled);
81
+ this.#hooks.entry.changed.on(this.#onEntryChanged);
104
82
  }
105
83
 
106
84
  #teardown() {
107
- this.#hooks.run.progress.off(this.#onProgress);
108
- this.#hooks.proposal.pending.off(this.#onProposal);
109
85
  this.#hooks.ui.render.off(this.#onRender);
110
86
  this.#hooks.ui.notify.off(this.#onNotify);
111
- this.#hooks.run.state.off(this.#onState);
112
87
  this.#hooks.stream.cancelled.off(this.#onStreamCancelled);
88
+ this.#hooks.entry.changed.off(this.#onEntryChanged);
113
89
  }
114
90
 
115
- /**
116
- * Abort in-flight runs on this connection and wait for them to
117
- * settle. Idempotent: `ws.on("close")` and server-initiated close
118
- * both call this; the cached Promise guarantees the work happens
119
- * exactly once and both callers observe the same completion.
120
- */
91
+ // Idempotent abort+drain; cached Promise lets ws.close and server.close share completion.
121
92
  shutdown() {
122
93
  if (!this.#shutdownPromise) {
123
94
  this.#shutdownPromise = (async () => {
@@ -241,8 +212,7 @@ export default class ClientConnection {
241
212
  } catch (error) {
242
213
  console.error(`[RUMMY] RPC Error: ${error.message}`);
243
214
  console.error(`[RUMMY] Stack: ${error.stack}`);
244
- // JSON-RPC: error responses for malformed requests with no id
245
- // MUST carry null per the spec.
215
+ // JSON-RPC requires null id for malformed requests with no id.
246
216
  this.#send({
247
217
  jsonrpc: "2.0",
248
218
  error: { code: -32603, message: error.message },
@@ -15,18 +15,13 @@ export default class SocketServer {
15
15
  this.#wss.on("connection", (ws, _req) => {
16
16
  const conn = new ClientConnection(ws, this.#db, this.#hooks);
17
17
  this.#connections.add(conn);
18
- // Remove from the tracking set only after the connection's
19
- // shutdown drain has fully settled — not on raw ws-close —
20
- // so server close() can still find and await an in-progress
21
- // shutdown kicked off by a client-initiated disconnect.
18
+ // Delete after drain settles so server.close() can await client-initiated shutdowns.
22
19
  ws.on("close", () => {
23
20
  conn.shutdown().finally(() => this.#connections.delete(conn));
24
21
  });
25
22
  });
26
23
 
27
- this.#wss.on("error", (_err) => {
28
- // Proxy to registry or handle locally
29
- });
24
+ this.#wss.on("error", (_err) => {});
30
25
  }
31
26
 
32
27
  address() {
@@ -38,14 +33,19 @@ export default class SocketServer {
38
33
  }
39
34
 
40
35
  async close() {
41
- // Drain in-flight runs on each connection before closing the
42
- // socket otherwise detached kickoff Promises keep the Node
43
- // event loop alive past server shutdown.
44
- const shutdowns = [];
45
- for (const conn of this.#connections) {
46
- shutdowns.push(conn.shutdown().catch(() => {}));
36
+ // Drain in-flight runs first; otherwise detached kickoffs pin the event loop.
37
+ // Best-effort: a single connection failing to shut down cleanly should not
38
+ // prevent the others from closing, but the failure must be visible.
39
+ const results = await Promise.allSettled(
40
+ Array.from(this.#connections, (conn) => conn.shutdown()),
41
+ );
42
+ for (const r of results) {
43
+ if (r.status === "rejected") {
44
+ console.error(
45
+ `[RUMMY] Connection shutdown failed: ${r.reason?.message ?? r.reason}`,
46
+ );
47
+ }
47
48
  }
48
- await Promise.all(shutdowns);
49
49
  this.#connections.clear();
50
50
 
51
51
  await new Promise((resolve) => {
@@ -1,11 +1,2 @@
1
- /**
2
- * Server↔client wire-protocol version. Bumped whenever the RPC shape
3
- * or notification payload shape changes in a way that breaks existing
4
- * clients. Clients pass their own version in `rummy/hello`; server
5
- * rejects MAJOR mismatch. Git commit log is the changelog.
6
- *
7
- * MAJOR — breaking change (removed/renamed method, shape change)
8
- * MINOR — additive change (new method, new optional field)
9
- * PATCH — internal fix visible to the wire shape
10
- */
1
+ // Wire protocol version; rummy/hello rejects MAJOR mismatch. Semver.
11
2
  export const RUMMY_PROTOCOL_VERSION = "2.0.0";
@@ -1,18 +1,16 @@
1
+ import encodeSegment from "../../agent/pathEncode.js";
2
+
1
3
  export const deterministic = true;
2
4
 
3
- // Build URI paths the model can round-trip:
4
- // "history,mongol,khan" "history/mongol/khan" (commas become path separators)
5
- // "contents of Document 1" → "contents_of_Document_1" (spaces become underscores)
6
- // Slice on decoded text, then split-encode-join per segment so / survives as
7
- // a separator while anything URL-unsafe inside a segment gets escaped.
5
+ // commas→/, then encode-per-segment so / survives as separator.
6
+ // encodeSegment handles spaces_ + URL-encode (single rule, used everywhere).
8
7
  export default function slugify(text) {
9
8
  if (!text) return "";
10
9
  return text
11
10
  .slice(0, 80)
12
11
  .replace(/,/g, "/")
13
- .replace(/ /g, "_")
14
12
  .split("/")
15
13
  .filter(Boolean)
16
- .map(encodeURIComponent)
14
+ .map(encodeSegment)
17
15
  .join("/");
18
16
  }
@@ -16,8 +16,8 @@ visible AS (
16
16
  , e.attributes
17
17
  , COALESCE(s.category, 'logging') AS category
18
18
  , CASE
19
- WHEN rv.visibility = 'archived' THEN NULL
20
19
  WHEN s.model_visible = 0 THEN NULL
20
+ WHEN rv.visibility = 'archived' THEN NULL
21
21
  ELSE rv.visibility
22
22
  END AS effective_visibility
23
23
  FROM run_views AS rv
@@ -38,10 +38,7 @@ projected AS (
38
38
  , attributes
39
39
  -- Category comes from schemes table — plugins declare it via registerScheme().
40
40
  , category
41
- , CASE
42
- WHEN effective_visibility IN ('visible', 'summarized') THEN body
43
- ELSE ''
44
- END AS body
41
+ , body
45
42
  FROM visible
46
43
  WHERE effective_visibility IS NOT NULL
47
44
  )
@@ -67,13 +64,9 @@ SELECT
67
64
  WHEN 'prompt' THEN 5
68
65
  ELSE 5
69
66
  END
70
- , CASE scheme WHEN 'skill' THEN 0 ELSE 1 END
71
- , CASE visibility
72
- WHEN 'summarized' THEN 0
73
- ELSE 1
74
- END
67
+ , scheme
75
68
  , turn
76
69
  , updated_at
77
- , id
70
+ , path
78
71
  ) AS ordinal
79
72
  FROM projected;