@possumtech/rummy 0.4.0 → 0.5.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 (55) hide show
  1. package/.env.example +1 -0
  2. package/FIDELITY_CONTRACT.md +172 -0
  3. package/migrations/001_initial_schema.sql +3 -3
  4. package/package.json +1 -1
  5. package/src/agent/AgentLoop.js +1 -2
  6. package/src/agent/ContextAssembler.js +2 -0
  7. package/src/agent/KnownStore.js +1 -2
  8. package/src/agent/ResponseHealer.js +54 -1
  9. package/src/agent/TurnExecutor.js +51 -6
  10. package/src/agent/XmlParser.js +150 -41
  11. package/src/agent/known_store.sql +18 -11
  12. package/src/hooks/PluginContext.js +8 -2
  13. package/src/hooks/RummyContext.js +6 -3
  14. package/src/hooks/ToolRegistry.js +23 -27
  15. package/src/plugins/ask_user/ask_user.js +2 -2
  16. package/src/plugins/ask_user/ask_userDoc.js +4 -2
  17. package/src/plugins/budget/README.md +6 -4
  18. package/src/plugins/budget/budget.js +29 -9
  19. package/src/plugins/cp/cp.js +5 -5
  20. package/src/plugins/cp/cpDoc.js +0 -8
  21. package/src/plugins/engine/engine.sql +1 -1
  22. package/src/plugins/env/env.js +4 -4
  23. package/src/plugins/env/envDoc.js +2 -2
  24. package/src/plugins/file/file.js +2 -7
  25. package/src/plugins/get/get.js +31 -10
  26. package/src/plugins/get/getDoc.js +26 -37
  27. package/src/plugins/helpers.js +2 -2
  28. package/src/plugins/instructions/instructions.js +6 -5
  29. package/src/plugins/instructions/preamble.md +41 -33
  30. package/src/plugins/known/known.js +17 -16
  31. package/src/plugins/known/knownDoc.js +1 -13
  32. package/src/plugins/mv/mv.js +6 -6
  33. package/src/plugins/mv/mvDoc.js +2 -13
  34. package/src/plugins/previous/previous.js +10 -14
  35. package/src/plugins/progress/progress.js +22 -5
  36. package/src/plugins/prompt/prompt.js +14 -11
  37. package/src/plugins/rm/rm.js +4 -4
  38. package/src/plugins/rm/rmDoc.js +4 -8
  39. package/src/plugins/rpc/rpc.js +1 -1
  40. package/src/plugins/set/set.js +10 -12
  41. package/src/plugins/set/setDoc.js +4 -4
  42. package/src/plugins/sh/sh.js +4 -4
  43. package/src/plugins/sh/shDoc.js +2 -2
  44. package/src/plugins/skill/skill.js +2 -1
  45. package/src/plugins/summarize/summarize.js +2 -2
  46. package/src/plugins/summarize/summarizeDoc.js +9 -10
  47. package/src/plugins/telemetry/telemetry.js +36 -11
  48. package/src/plugins/think/think.js +2 -1
  49. package/src/plugins/think/thinkDoc.js +3 -5
  50. package/src/plugins/unknown/unknown.js +21 -14
  51. package/src/plugins/unknown/unknownDoc.js +2 -6
  52. package/src/plugins/update/update.js +2 -2
  53. package/src/plugins/update/updateDoc.js +9 -6
  54. package/src/sql/functions/slugify.js +13 -1
  55. package/src/sql/v_model_context.sql +3 -3
@@ -111,7 +111,7 @@ export default class Rpc {
111
111
  });
112
112
  return { status: "ok" };
113
113
  },
114
- description: "Promote entry to full state.",
114
+ description: "Promote entry fidelity.",
115
115
  params: {
116
116
  path: "string — file path or glob pattern",
117
117
  run: "string — run alias",
@@ -4,7 +4,7 @@ import Hedberg, { generatePatch } from "../hedberg/hedberg.js";
4
4
  import { storePatternResult } from "../helpers.js";
5
5
  import docs from "./setDoc.js";
6
6
 
7
- const VALID_FIDELITY = { archive: 1, summary: 1, full: 1 };
7
+ const VALID_FIDELITY = { archived: 1, demoted: 1, promoted: 1 };
8
8
 
9
9
  // biome-ignore lint/suspicious/noShadowRestrictedNames: tool name is "set"
10
10
  export default class Set {
@@ -14,8 +14,8 @@ export default class Set {
14
14
  this.#core = core;
15
15
  core.registerScheme();
16
16
  core.on("handler", this.handler.bind(this));
17
- core.on("full", this.full.bind(this));
18
- core.on("summary", this.summary.bind(this));
17
+ core.on("promoted", this.full.bind(this));
18
+ core.on("demoted", this.summary.bind(this));
19
19
  core.on("turn.proposing", this.#materializeRevisions.bind(this));
20
20
  core.filter("instructions.toolDocs", async (docsMap) => {
21
21
  docsMap.set = docs;
@@ -27,8 +27,7 @@ export default class Set {
27
27
  const { entries: store, sequence: turn, runId, loopId } = rummy;
28
28
  const attrs = entry.attributes;
29
29
  const fidelityAttr = VALID_FIDELITY[attrs.fidelity] ? attrs.fidelity : null;
30
- const rawSummary =
31
- typeof attrs.summary === "string" ? attrs.summary : null;
30
+ const rawSummary = typeof attrs.summary === "string" ? attrs.summary : null;
32
31
  const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
33
32
 
34
33
  // Pure fidelity/metadata change — no body content
@@ -46,7 +45,7 @@ export default class Set {
46
45
  entry.resultPath,
47
46
  `${target} not found`,
48
47
  404,
49
- { fidelity: "archive", loopId },
48
+ { fidelity: "archived", loopId },
50
49
  );
51
50
  return;
52
51
  }
@@ -58,15 +57,14 @@ export default class Set {
58
57
  });
59
58
  }
60
59
  }
61
- const label =
62
- fidelityAttr === "archive" ? "archived" : `set to ${fidelityAttr}`;
60
+ const label = `set to ${fidelityAttr}`;
63
61
  await store.upsert(
64
62
  runId,
65
63
  turn,
66
64
  entry.resultPath,
67
65
  `${matches.map((m) => m.path).join(", ")} ${label}`,
68
66
  200,
69
- { fidelity: "archive", loopId },
67
+ { fidelity: "archived", loopId },
70
68
  );
71
69
  return;
72
70
  }
@@ -137,7 +135,7 @@ export default class Set {
137
135
  } else {
138
136
  // Direct scheme write
139
137
  await store.upsert(runId, turn, target, entry.body, 200, {
140
- fidelity: fidelityAttr || "full",
138
+ fidelity: fidelityAttr || "promoted",
141
139
  attributes: summaryText ? { summary: summaryText } : null,
142
140
  loopId,
143
141
  });
@@ -169,8 +167,8 @@ export default class Set {
169
167
  return `# set ${file}${tokens}\n${attrs.merge}`;
170
168
  }
171
169
 
172
- summary(entry) {
173
- return entry.attributes.merge || "";
170
+ summary() {
171
+ return "";
174
172
  }
175
173
 
176
174
  async #processEdit(rummy, entry, attrs) {
@@ -6,7 +6,7 @@ const LINES = [
6
6
  '## <set path="[path/to/file]">[content or edit]</set> - Create, edit, or update a file or entry',
7
7
  ],
8
8
  [
9
- 'Example: <set path="known://project/milestones" fidelity="summary" summary="milestone,deadline,2026"/>',
9
+ 'Example: <set path="known://project/milestones" fidelity="demoted" summary="milestone,deadline,2026"/>',
10
10
  "Fidelity control first — most unique capability of set.",
11
11
  ],
12
12
  [
@@ -20,7 +20,7 @@ new text
20
20
  "SEARCH/REPLACE block — primary edit pattern for existing files.",
21
21
  ],
22
22
  [
23
- 'Example: <set path="src/config.js">s/port = 3000/port = 8080/g;s/host = 127.0.0.1/host = localhost/g;</set>',
23
+ `Example: <set path="src/config.js">s/port = 3000/port = 8080/g;s/We're almost done/We're done./g;</set>`,
24
24
  "Sed syntax: chained s/old/new/ patterns with semicolons.",
25
25
  ],
26
26
  [
@@ -28,8 +28,8 @@ new text
28
28
  "Create: body contents are entire file.",
29
29
  ],
30
30
  [
31
- "* YOU MUST NOT use <sh/> or <env/> to list, create, read, or edit files. Use the Tool Commands.",
32
- "Forces file operations through set/get.",
31
+ "* YOU MUST NOT use <sh></sh> or <env></env> to list, create, read, or edit files use <get></get> and <set></set>",
32
+ "Reinforces at the decision point — model reading setDoc for file ops sees the prohibition here, not just buried in shDoc/envDoc which it may not be reading.",
33
33
  ],
34
34
  ];
35
35
 
@@ -7,8 +7,8 @@ export default class Sh {
7
7
  this.#core = core;
8
8
  core.registerScheme();
9
9
  core.on("handler", this.handler.bind(this));
10
- core.on("full", this.full.bind(this));
11
- core.on("summary", this.summary.bind(this));
10
+ core.on("promoted", this.full.bind(this));
11
+ core.on("demoted", this.summary.bind(this));
12
12
  core.filter("instructions.toolDocs", async (docsMap) => {
13
13
  docsMap.sh = docs;
14
14
  return docsMap;
@@ -27,7 +27,7 @@ export default class Sh {
27
27
  return `# sh ${entry.attributes.command || ""}\n${entry.body}`;
28
28
  }
29
29
 
30
- summary(entry) {
31
- return entry.attributes.command || "";
30
+ summary() {
31
+ return "";
32
32
  }
33
33
  }
@@ -12,11 +12,11 @@ const LINES = [
12
12
  "Test execution. Another common side-effect action.",
13
13
  ],
14
14
  [
15
- "* YOU MUST NOT use <sh/> to read, create, or edit files — use <get/> and <set/>",
15
+ "* YOU MUST NOT use <sh></sh> to read, create, or edit files — use <get></get> and <set></set>",
16
16
  "Forces file operations through the entry system.",
17
17
  ],
18
18
  [
19
- "* YOU MUST use <env/> for commands without side effects",
19
+ "* YOU MUST use <env></env> for commands without side effects",
20
20
  "Reinforces the env/sh split. Read = env, mutate = sh.",
21
21
  ],
22
22
  ];
@@ -10,7 +10,8 @@ export default class Skill {
10
10
  name: "skill",
11
11
  category: "data",
12
12
  });
13
- core.hooks.tools.onView("skill", (entry) => entry.body);
13
+ core.hooks.tools.onView("skill", (entry) => entry.body, "promoted");
14
+ core.hooks.tools.onView("skill", () => "", "demoted");
14
15
 
15
16
  const r = core.hooks.rpc.registry;
16
17
 
@@ -8,8 +8,8 @@ export default class Summarize {
8
8
  core.ensureTool();
9
9
  core.registerScheme({ category: "logging" });
10
10
  core.on("handler", this.handler.bind(this));
11
- core.on("full", this.full.bind(this));
12
- core.on("summary", this.summary.bind(this));
11
+ core.on("promoted", this.full.bind(this));
12
+ core.on("demoted", this.summary.bind(this));
13
13
  core.filter("instructions.toolDocs", async (docsMap) => {
14
14
  docsMap.summarize = docs;
15
15
  return docsMap;
@@ -2,25 +2,24 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
- ["## <summarize>[answer or summary]</summarize> - Signal completion"],
6
5
  [
7
- "Example: <summarize>The port is 8080</summarize>",
8
- "Direct answer. Summarize delivers answers.",
6
+ "## <summarize>[answer or final summary]</summarize> - Terminate the run with the final answer",
7
+ "Header teaches consequence (run ends), not just label. Model now knows emitting this stops everything.",
9
8
  ],
10
9
  [
11
- "Example: <summarize>Installed express, updated config</summarize>",
12
- "Task summary. Action completion.",
10
+ "Example: <summarize>The port is 8080</summarize>",
11
+ "Direct answer. Summarize delivers answers.",
13
12
  ],
14
13
  [
15
- "* YOU MUST use <summarize></summarize> when done describes the final state",
16
- "Completion signal.",
14
+ "* Urgent: <summarize></summarize> ENDS THE RUN. After this, no more turns happen.",
15
+ "Direct statement of terminal behavior — the model treating summarize as a generic 'done message' was causing zombie-update loops (model unsure if truly finished, defaulted to update).",
17
16
  ],
18
17
  [
19
- "* YOU MUST NOT use <summarize> if still workinguse <update/> instead",
20
- "Mutual exclusion with update.",
18
+ "* Urgent: YOU MUST NOT include <summarize></summarize> with other tools. Termination is a deliberate, isolated act not a side effect of a turn doing other things.",
19
+ "Prior 'they might fail' rationale was argued around (when set on known:// succeeds, model rationalized bundling). Reframing as architectural ('termination is deliberate') removes the argument surface.",
21
20
  ],
22
21
  [
23
- "* YOU MUST keep <summarize> to <= 80 characters",
22
+ "* YOU MUST keep <summarize></summarize> to <= 80 characters",
24
23
  "Length cap.",
25
24
  ],
26
25
  ];
@@ -1,17 +1,23 @@
1
- import { writeFile } from "node:fs/promises";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  export default class Telemetry {
5
5
  #core;
6
6
  #starts = new Map();
7
7
  #lastRunPath = null;
8
+ #turnsDir = null;
8
9
  #turnLog = [];
10
+ #currentRunAlias = null;
11
+ #currentTurn = null;
9
12
 
10
13
  constructor(core) {
11
14
  this.#core = core;
12
15
 
13
16
  const home = process.env.RUMMY_HOME;
14
- if (home) this.#lastRunPath = join(home, "last_run.txt");
17
+ if (home) {
18
+ this.#lastRunPath = join(home, "last_run.txt");
19
+ this.#turnsDir = join(home, "turns");
20
+ }
15
21
 
16
22
  core.on("rpc.started", this.#onRpcStarted.bind(this));
17
23
  core.on("rpc.completed", this.#onRpcCompleted.bind(this));
@@ -85,20 +91,20 @@ export default class Telemetry {
85
91
  // assistant://N — the model's raw response
86
92
  await store.upsert(runId, turn, `assistant://${turn}`, content, 200, {
87
93
  loopId,
88
- fidelity: "archive",
94
+ fidelity: "archived",
89
95
  });
90
96
 
91
97
  // system://N, user://N — assembled messages as audit
92
98
  if (systemMsg) {
93
99
  await store.upsert(runId, turn, `system://${turn}`, systemMsg, 200, {
94
100
  loopId,
95
- fidelity: "archive",
101
+ fidelity: "archived",
96
102
  });
97
103
  }
98
104
  if (userMsg) {
99
105
  await store.upsert(runId, turn, `user://${turn}`, userMsg, 200, {
100
106
  loopId,
101
- fidelity: "archive",
107
+ fidelity: "archived",
102
108
  });
103
109
  }
104
110
 
@@ -115,7 +121,7 @@ export default class Telemetry {
115
121
  model: result.model || null,
116
122
  }),
117
123
  200,
118
- { loopId, fidelity: "archive" },
124
+ { loopId, fidelity: "archived" },
119
125
  );
120
126
 
121
127
  // reasoning://N
@@ -126,15 +132,18 @@ export default class Telemetry {
126
132
  `reasoning://${turn}`,
127
133
  responseMessage.reasoning_content,
128
134
  200,
129
- { loopId, fidelity: "archive" },
135
+ { loopId, fidelity: "archived" },
130
136
  );
131
137
  }
132
138
 
133
- // content://N — unparsed text
139
+ // content://N — unparsed text. 400 Bad Request because anything in
140
+ // unparsed is text the parser couldn't dispatch (malformed XML, native
141
+ // tool call attempts, reasoning bleed). Visible to the model so it
142
+ // sees the rejection on its next turn and can correct.
134
143
  if (unparsed) {
135
- await store.upsert(runId, turn, `content://${turn}`, unparsed, 200, {
144
+ await store.upsert(runId, turn, `content://${turn}`, unparsed, 400, {
136
145
  loopId,
137
- fidelity: "archive",
146
+ fidelity: "promoted",
138
147
  });
139
148
  }
140
149
 
@@ -168,8 +177,10 @@ export default class Telemetry {
168
177
  }
169
178
 
170
179
  async #logMessages(messages, context) {
180
+ this.#currentRunAlias = context.runAlias || `run_${context.runId}`;
181
+ this.#currentTurn = context.turn ?? null;
171
182
  this.#turnLog.push(
172
- `\n${"=".repeat(60)}\nTURN — model=${context.model} run=${context.runId}\n${"=".repeat(60)}`,
183
+ `\n${"=".repeat(60)}\nTURN ${this.#currentTurn ?? "?"} — model=${context.model} run=${this.#currentRunAlias}\n${"=".repeat(60)}`,
173
184
  );
174
185
  for (const msg of messages) {
175
186
  const label = msg.role.toUpperCase();
@@ -191,6 +202,7 @@ export default class Telemetry {
191
202
  const usage = response.usage || {};
192
203
  this.#turnLog.push(`\n--- USAGE ---\n${JSON.stringify(usage)}`);
193
204
  this.#flush();
205
+ this.#writeTurnFile();
194
206
  return response;
195
207
  }
196
208
 
@@ -200,4 +212,17 @@ export default class Telemetry {
200
212
  () => {},
201
213
  );
202
214
  }
215
+
216
+ async #writeTurnFile() {
217
+ if (!this.#turnsDir || !this.#currentRunAlias || this.#currentTurn == null)
218
+ return;
219
+ const runDir = join(this.#turnsDir, this.#currentRunAlias);
220
+ try {
221
+ await mkdir(runDir, { recursive: true });
222
+ const fileName = `turn_${String(this.#currentTurn).padStart(3, "0")}.txt`;
223
+ await writeFile(join(runDir, fileName), `${this.#turnLog.join("\n")}\n`);
224
+ } catch {
225
+ // best effort — diagnostic feature, don't fail the turn
226
+ }
227
+ }
203
228
  }
@@ -1,7 +1,8 @@
1
1
  import docs from "./thinkDoc.js";
2
2
 
3
3
  const THINK_ENABLED = process.env.RUMMY_THINK;
4
- if (THINK_ENABLED === undefined) throw new Error("RUMMY_THINK must be set (1 or 0)");
4
+ if (THINK_ENABLED === undefined)
5
+ throw new Error("RUMMY_THINK must be set (1 or 0)");
5
6
 
6
7
  export default class Think {
7
8
  constructor(core) {
@@ -2,15 +2,13 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
+ ["## <think>[reasoning]</think> - Think before acting"],
5
6
  [
6
- "## <think>[reasoning]</think> - Think before acting",
7
- ],
8
- [
9
- "* Use <think> before any other tools to plan your approach",
7
+ "* Use <think></think> before any other tools to plan your approach",
10
8
  "Positioning: think first, then act. Prevents degenerate tool-call storms.",
11
9
  ],
12
10
  [
13
- "* Reasoning inside <think> is private — it does not appear in your context",
11
+ "* Reasoning inside <think></think> is private — it does not appear in your context",
14
12
  "Frees the model to reason without consuming context budget.",
15
13
  ],
16
14
  ];
@@ -1,5 +1,3 @@
1
- import docs from "./unknownDoc.js";
2
-
3
1
  export default class Unknown {
4
2
  #core;
5
3
 
@@ -10,13 +8,13 @@ export default class Unknown {
10
8
  category: "unknown",
11
9
  });
12
10
  core.on("handler", this.handler.bind(this));
13
- core.on("full", this.full.bind(this));
14
- core.on("summary", this.summary.bind(this));
11
+ core.on("promoted", this.full.bind(this));
12
+ core.on("demoted", this.summary.bind(this));
15
13
  core.filter("assembly.system", this.assembleUnknowns.bind(this), 300);
16
- core.filter("instructions.toolDocs", async (docsMap) => {
17
- docsMap.unknown = docs;
18
- return docsMap;
19
- });
14
+ // <unknown> is internal — written via <set path="unknown://...">. Hidden
15
+ // from all model-facing tool lists. Handler still dispatches if the
16
+ // model emits <unknown> directly out of habit.
17
+ core.markHidden();
20
18
  }
21
19
 
22
20
  async handler(entry, rummy) {
@@ -29,17 +27,23 @@ export default class Unknown {
29
27
  return;
30
28
  }
31
29
 
32
- // Generate slug path and upsert
33
- const unknownPath = await store.slugPath(runId, "unknown", entry.body);
30
+ // Generate slug path and upsert. Summary (if provided) becomes the
31
+ // path so the model can round-trip it via <get>; body is the fallback.
32
+ const unknownPath = await store.slugPath(
33
+ runId,
34
+ "unknown",
35
+ entry.body,
36
+ entry.attributes?.summary,
37
+ );
34
38
  await store.upsert(runId, turn, unknownPath, entry.body, 200, { loopId });
35
39
  }
36
40
 
37
41
  full(entry) {
38
- return `# unknown\n${entry.body}`;
42
+ return entry.body;
39
43
  }
40
44
 
41
- summary(entry) {
42
- return this.full(entry);
45
+ summary() {
46
+ return "";
43
47
  }
44
48
 
45
49
  async assembleUnknowns(content, ctx) {
@@ -49,7 +53,10 @@ export default class Unknown {
49
53
  const lines = entries.map((u) => {
50
54
  const fidelity = u.fidelity ? ` fidelity="${u.fidelity}"` : "";
51
55
  const tokens = u.tokens ? ` tokens="${u.tokens}"` : "";
52
- return `<unknown path="${u.path}" turn="${u.source_turn || u.turn}"${fidelity}${tokens}>${u.body}</unknown>`;
56
+ if (u.body) {
57
+ return `<unknown path="${u.path}" turn="${u.source_turn || u.turn}"${fidelity}${tokens}>${u.body}</unknown>`;
58
+ }
59
+ return `<unknown path="${u.path}" turn="${u.source_turn || u.turn}"${fidelity}${tokens}/>`;
53
60
  });
54
61
  return `${content}\n\n<unknowns>\n${lines.join("\n")}\n</unknowns>`;
55
62
  }
@@ -3,22 +3,18 @@
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
5
  [
6
- "## <unknown>[specific thing I need to learn]</unknown> - Track open questions",
6
+ "## <unknown>[specific thing I need to learn]</unknown> - Register gaps for research",
7
7
  ],
8
8
  [
9
9
  'Example: <unknown path="unknown://answer">contents of answer.txt</unknown>',
10
10
  "Path form: explicit unknown path for structured tracking.",
11
11
  ],
12
- [
13
- "Example: <unknown>which database adapter is configured</unknown>",
14
- "Body form: question as body, path auto-generated.",
15
- ],
16
12
  [
17
13
  "* Investigate with Tool Commands",
18
14
  "Unknowns drive action — get, env, search, ask_user.",
19
15
  ],
20
16
  [
21
- '* When resolved or irrelevant, remove with <set path="unknown://..." fidelity="archive"/>',
17
+ '* When resolved or irrelevant, remove with <set path="unknown://..." fidelity="archived"/>',
22
18
  "Archive instead of delete — preserves the question for context history.",
23
19
  ],
24
20
  ];
@@ -8,8 +8,8 @@ export default class Update {
8
8
  core.ensureTool();
9
9
  core.registerScheme({ category: "logging" });
10
10
  core.on("handler", this.handler.bind(this));
11
- core.on("full", this.full.bind(this));
12
- core.on("summary", this.summary.bind(this));
11
+ core.on("promoted", this.full.bind(this));
12
+ core.on("demoted", this.summary.bind(this));
13
13
  core.filter("instructions.toolDocs", async (docsMap) => {
14
14
  docsMap.update = docs;
15
15
  return docsMap;
@@ -2,7 +2,10 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
- ["## <update>[brief status]</update> - Signal continuation"],
5
+ [
6
+ "## <update>[brief status]</update> - Heartbeat for ongoing work (one per turn, at the end)",
7
+ "Header defines position and frequency. Without this, model uses update as inline narration between tools — multiple updates per turn.",
8
+ ],
6
9
  [
7
10
  "Example: <update>Reading config files</update>",
8
11
  "Progress checkpoint. Status signal, not a log entry.",
@@ -12,15 +15,15 @@ const LINES = [
12
15
  "Multi-step progress. Ongoing work.",
13
16
  ],
14
17
  [
15
- "* YOU MUST use <update></update> if still working describes the current state",
16
- "Continuation signal. Triggers the next turn.",
18
+ "* Urgent: ONE <update></update> per turn, AT THE END. Not inline narration between tools.",
19
+ "Single-update-per-turn is the missing rule. Model was emitting 3-6 updates per turn as progress commentary.",
17
20
  ],
18
21
  [
19
- "* YOU MUST NOT use <update> if done use <summarize/> instead",
20
- "Mutual exclusion with summarize.",
22
+ "* If you'd repeat the same <update></update> as last turn, the work is either stuck or done. Take a different action or <summarize></summarize>.",
23
+ "Points at the zombie-loop failure mode directly. Gives the model a trigger (same-text-as-prior-update) and two remedies.",
21
24
  ],
22
25
  [
23
- "* YOU MUST keep <update> to <= 80 characters",
26
+ "* YOU MUST keep <update></update> to <= 80 characters",
24
27
  "Length cap.",
25
28
  ],
26
29
  ];
@@ -1,6 +1,18 @@
1
1
  export const deterministic = true;
2
2
 
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.
3
8
  export default function slugify(text) {
4
9
  if (!text) return "";
5
- return encodeURIComponent(text).slice(0, 80);
10
+ return text
11
+ .slice(0, 80)
12
+ .replace(/,/g, "/")
13
+ .replace(/ /g, "_")
14
+ .split("/")
15
+ .filter(Boolean)
16
+ .map(encodeURIComponent)
17
+ .join("/");
6
18
  }
@@ -17,7 +17,7 @@ visible AS (
17
17
  , COALESCE(s.category, 'logging') AS category
18
18
  , CASE
19
19
  -- Archived entries not in context
20
- WHEN ke.fidelity = 'archive' THEN NULL
20
+ WHEN ke.fidelity = 'archived' THEN NULL
21
21
  -- 202 Accepted (proposed) hidden until resolved
22
22
  WHEN ke.status = 202 THEN NULL
23
23
  -- Audit schemes (model_visible = 0) hidden
@@ -43,7 +43,7 @@ projected AS (
43
43
  , category
44
44
  , tokens
45
45
  , CASE
46
- WHEN visible_fidelity IN ('full', 'summary') THEN body
46
+ WHEN visible_fidelity IN ('promoted', 'demoted') THEN body
47
47
  ELSE ''
48
48
  END AS body
49
49
  FROM visible
@@ -72,7 +72,7 @@ SELECT
72
72
  END
73
73
  , CASE scheme WHEN 'skill' THEN 0 ELSE 1 END
74
74
  , CASE fidelity
75
- WHEN 'summary' THEN 0
75
+ WHEN 'demoted' THEN 0
76
76
  ELSE 1
77
77
  END
78
78
  , turn