@possumtech/rummy 0.3.1 → 0.4.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 (46) hide show
  1. package/.env.example +11 -0
  2. package/README.md +5 -1
  3. package/SPEC.md +31 -17
  4. package/migrations/001_initial_schema.sql +2 -3
  5. package/package.json +1 -1
  6. package/src/agent/AgentLoop.js +50 -151
  7. package/src/agent/KnownStore.js +15 -7
  8. package/src/agent/TurnExecutor.js +75 -318
  9. package/src/agent/XmlParser.js +25 -4
  10. package/src/agent/known_queries.sql +1 -1
  11. package/src/agent/known_store.sql +11 -61
  12. package/src/agent/runs.sql +2 -2
  13. package/src/hooks/Hooks.js +1 -0
  14. package/src/hooks/ToolRegistry.js +6 -5
  15. package/src/plugins/ask_user/ask_userDoc.js +3 -8
  16. package/src/plugins/budget/README.md +26 -18
  17. package/src/plugins/budget/budget.js +60 -3
  18. package/src/plugins/budget/recovery.js +47 -0
  19. package/src/plugins/cp/cpDoc.js +4 -9
  20. package/src/plugins/env/envDoc.js +3 -8
  21. package/src/plugins/get/get.js +2 -4
  22. package/src/plugins/get/getDoc.js +11 -18
  23. package/src/plugins/helpers.js +2 -2
  24. package/src/plugins/instructions/instructions.js +3 -2
  25. package/src/plugins/instructions/preamble.md +27 -16
  26. package/src/plugins/known/known.js +63 -8
  27. package/src/plugins/known/knownDoc.js +10 -14
  28. package/src/plugins/mv/mvDoc.js +6 -21
  29. package/src/plugins/policy/policy.js +47 -0
  30. package/src/plugins/progress/progress.js +9 -45
  31. package/src/plugins/prompt/prompt.js +10 -1
  32. package/src/plugins/rm/rmDoc.js +5 -10
  33. package/src/plugins/rpc/rpc.js +3 -1
  34. package/src/plugins/set/set.js +82 -85
  35. package/src/plugins/set/setDoc.js +28 -41
  36. package/src/plugins/sh/shDoc.js +2 -7
  37. package/src/plugins/summarize/summarize.js +7 -0
  38. package/src/plugins/summarize/summarizeDoc.js +6 -11
  39. package/src/plugins/think/think.js +12 -0
  40. package/src/plugins/think/thinkDoc.js +18 -0
  41. package/src/plugins/unknown/unknown.js +21 -0
  42. package/src/plugins/unknown/unknownDoc.js +9 -14
  43. package/src/plugins/update/update.js +7 -0
  44. package/src/plugins/update/updateDoc.js +6 -11
  45. package/src/server/ClientConnection.js +11 -1
  46. package/src/sql/v_model_context.sql +4 -4
@@ -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, index: 1, full: 1 };
7
+ const VALID_FIDELITY = { archive: 1, summary: 1, full: 1 };
8
8
 
9
9
  // biome-ignore lint/suspicious/noShadowRestrictedNames: tool name is "set"
10
10
  export default class Set {
@@ -26,47 +26,19 @@ export default class Set {
26
26
  async handler(entry, rummy) {
27
27
  const { entries: store, sequence: turn, runId, loopId } = rummy;
28
28
  const attrs = entry.attributes;
29
-
30
- // Fidelity control: <set path="..." fidelity="archive"/>
31
29
  const fidelityAttr = VALID_FIDELITY[attrs.fidelity] ? attrs.fidelity : null;
32
- if (fidelityAttr && attrs.path) {
30
+ const rawSummary =
31
+ typeof attrs.summary === "string" ? attrs.summary : null;
32
+ const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
33
+
34
+ // Pure fidelity/metadata change — no body content
35
+ if (!entry.body && fidelityAttr && attrs.path) {
33
36
  const target = attrs.path;
34
- const rawSummary =
35
- typeof attrs.summary === "string" ? attrs.summary : null;
36
- const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
37
37
  const matches = await store.getEntriesByPattern(
38
38
  runId,
39
39
  target,
40
40
  attrs.body,
41
41
  );
42
- if (entry.body) {
43
- // Write content directly at specified fidelity
44
- const entryAttrs = summaryText ? { summary: summaryText } : null;
45
- for (const match of matches) {
46
- await store.upsert(runId, turn, match.path, entry.body, 200, {
47
- fidelity: fidelityAttr,
48
- attributes: entryAttrs,
49
- loopId,
50
- });
51
- }
52
- if (matches.length === 0) {
53
- await store.upsert(runId, turn, target, entry.body, 200, {
54
- fidelity: fidelityAttr,
55
- attributes: entryAttrs,
56
- loopId,
57
- });
58
- }
59
- } else {
60
- // No body — change fidelity, attach summary if provided
61
- for (const match of matches) {
62
- await store.setFidelity(runId, match.path, fidelityAttr);
63
- if (summaryText) {
64
- await store.setAttributes(runId, match.path, {
65
- summary: summaryText,
66
- });
67
- }
68
- }
69
- }
70
42
  if (matches.length === 0) {
71
43
  await store.upsert(
72
44
  runId,
@@ -74,13 +46,18 @@ export default class Set {
74
46
  entry.resultPath,
75
47
  `${target} not found`,
76
48
  404,
77
- {
78
- fidelity: "archive",
79
- loopId,
80
- },
49
+ { fidelity: "archive", loopId },
81
50
  );
82
51
  return;
83
52
  }
53
+ for (const match of matches) {
54
+ await store.setFidelity(runId, match.path, fidelityAttr);
55
+ if (summaryText) {
56
+ await store.setAttributes(runId, match.path, {
57
+ summary: summaryText,
58
+ });
59
+ }
60
+ }
84
61
  const label =
85
62
  fidelityAttr === "archive" ? "archived" : `set to ${fidelityAttr}`;
86
63
  await store.upsert(
@@ -89,20 +66,16 @@ export default class Set {
89
66
  entry.resultPath,
90
67
  `${matches.map((m) => m.path).join(", ")} ${label}`,
91
68
  200,
92
- {
93
- fidelity: "archive",
94
- loopId,
95
- },
69
+ { fidelity: "archive", loopId },
96
70
  );
97
71
  return;
98
72
  }
99
73
 
74
+ // Edit: sed patterns or SEARCH/REPLACE blocks
100
75
  if (attrs.blocks || attrs.search != null) {
101
76
  await this.#processEdit(rummy, entry, attrs);
102
- return;
103
- }
104
-
105
- if (attrs.preview && attrs.path) {
77
+ } else if (attrs.preview && attrs.path) {
78
+ // Preview
106
79
  const matches = await store.getEntriesByPattern(
107
80
  runId,
108
81
  attrs.path,
@@ -119,43 +92,68 @@ export default class Set {
119
92
  { preview: true, loopId },
120
93
  );
121
94
  return;
122
- }
95
+ } else {
96
+ // Write content
97
+ const target = attrs.path;
98
+ if (!target) return;
123
99
 
124
- const target = attrs.path;
125
- if (!target) return;
100
+ const scheme = KnownStore.scheme(target);
101
+ if (scheme === null) {
102
+ // File write — diff against existing content
103
+ const existing = await store.getBody(runId, target);
104
+ const oldContent = existing ?? "";
105
+ const newContent = entry.body || "";
106
+ const udiff = generatePatch(target, oldContent, newContent);
107
+ const merge = oldContent
108
+ ? `<<<<<<< SEARCH\n${oldContent}\n=======\n${newContent}\n>>>>>>> REPLACE`
109
+ : `<<<<<<< SEARCH\n=======\n${newContent}\n>>>>>>> REPLACE`;
110
+ await store.upsert(runId, turn, entry.resultPath, oldContent, 202, {
111
+ attributes: { file: target, patch: udiff, merge },
112
+ loopId,
113
+ });
114
+ } else if (attrs.filter || target.includes("*")) {
115
+ // Pattern update
116
+ const matches = await store.getEntriesByPattern(
117
+ runId,
118
+ target,
119
+ attrs.filter,
120
+ );
121
+ await store.updateBodyByPattern(
122
+ runId,
123
+ target,
124
+ attrs.filter || null,
125
+ entry.body,
126
+ );
127
+ await storePatternResult(
128
+ store,
129
+ runId,
130
+ turn,
131
+ "set",
132
+ target,
133
+ attrs.filter,
134
+ matches,
135
+ { loopId },
136
+ );
137
+ } else {
138
+ // Direct scheme write
139
+ await store.upsert(runId, turn, target, entry.body, 200, {
140
+ fidelity: fidelityAttr || "full",
141
+ attributes: summaryText ? { summary: summaryText } : null,
142
+ loopId,
143
+ });
144
+ }
145
+ }
126
146
 
127
- const scheme = KnownStore.scheme(target);
128
- if (scheme === null) {
129
- const udiff = generatePatch(target, "", entry.body || "");
130
- const merge = `<<<<<<< SEARCH\n=======\n${entry.body || ""}\n>>>>>>> REPLACE`;
131
- await store.upsert(runId, turn, entry.resultPath, "", 202, {
132
- attributes: { file: target, patch: udiff, merge },
133
- loopId,
134
- });
135
- } else if (attrs.filter || target.includes("*")) {
136
- const matches = await store.getEntriesByPattern(
137
- runId,
138
- target,
139
- attrs.filter,
140
- );
141
- await store.updateBodyByPattern(
142
- runId,
143
- target,
144
- attrs.filter || null,
145
- entry.body,
146
- );
147
- await storePatternResult(
148
- store,
149
- runId,
150
- turn,
151
- "set",
152
- target,
153
- attrs.filter,
154
- matches,
155
- { loopId },
156
- );
157
- } else {
158
- await store.upsert(runId, turn, target, entry.body, 200, { loopId });
147
+ // Apply fidelity after all write operations
148
+ if (fidelityAttr && attrs.path) {
149
+ const target = attrs.path;
150
+ const scheme = KnownStore.scheme(target);
151
+ if (scheme !== null) {
152
+ await store.setFidelity(runId, target, fidelityAttr);
153
+ }
154
+ if (summaryText) {
155
+ await store.setAttributes(runId, target, { summary: summaryText });
156
+ }
159
157
  }
160
158
  }
161
159
 
@@ -215,7 +213,7 @@ export default class Set {
215
213
  searchText != null
216
214
  ? `<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`
217
215
  : null;
218
- const beforeTokens = match.tokens_full || 0;
216
+ const beforeTokens = match.tokens || 0;
219
217
  const afterTokens = patch ? countTokens(patch) : beforeTokens;
220
218
 
221
219
  await store.upsert(runId, turn, resultPath, match.body, status, {
@@ -282,7 +280,7 @@ export default class Set {
282
280
  ? generatePatch(filePath, original, current)
283
281
  : null;
284
282
  const merge = mergeBlocks.length > 0 ? mergeBlocks.join("\n") : null;
285
- const beforeTokens = fileEntry[0].tokens_full || 0;
283
+ const beforeTokens = fileEntry[0].tokens || 0;
286
284
  const afterTokens = current ? countTokens(current) : beforeTokens;
287
285
 
288
286
  await store.upsert(runId, turn, entry.path, original, state, {
@@ -334,7 +332,6 @@ export default class Set {
334
332
  flags: block.flags,
335
333
  });
336
334
  }
337
- // Multi-block: apply sequentially, no per-hunk merge notation
338
335
  let current = body;
339
336
  let lastWarning = null;
340
337
  for (const block of attrs.blocks) {
@@ -2,48 +2,35 @@
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
- // --- Syntax: path attr + body = edit content
6
- ['## <set path="[path/to/file]">[edit]</set> - Edit a file or entry'],
7
-
8
- // --- Examples: sed, SEARCH/REPLACE, fidelity control
9
- [
10
- 'Example: <set path="src/config.js">s/port = 3000/port = 8080/g</set>',
11
- "Sed syntax: most common edit pattern. Shows s/old/new/ with g flag.",
12
- ],
13
- [
14
- `Example: <set path="src/app.js"><<<<<<< SEARCH
15
- // TODO: add error handling
5
+ [
6
+ '## <set path="[path/to/file]">[content or edit]</set> - Create, edit, or update a file or entry',
7
+ ],
8
+ [
9
+ 'Example: <set path="known://project/milestones" fidelity="summary" summary="milestone,deadline,2026"/>',
10
+ "Fidelity control first most unique capability of set.",
11
+ ],
12
+ [
13
+ `Example: <set path="src/app.js">
14
+ <<<<<<< SEARCH
15
+ old text
16
16
  =======
17
- // error handler configured
18
- >>>>>>> REPLACE</set>`,
19
- "SEARCH/REPLACE block: literal match and replace. Use when sed escaping is complex.",
20
- ],
21
- [
22
- 'Example: <set path="known://project/milestones" fidelity="summary" summary="milestone,deadline,2026"/> ... <set path="prompt://3" fidelity="index"/>',
23
- "Fidelity control: compress a known entry to keywords, demote a previous prompt to index-only. Both free context while keeping paths visible.",
24
- ],
25
-
26
- // --- Constraints
27
- [
28
- '* `fidelity="..."`: `archive`, `summary`, `index`, `full`',
29
- "Fidelity control. Archive removes from context but preserves for retrieval.",
30
- ],
31
- [
32
- '* `fidelity="summary"` HIDES the body — does NOT require reading or compressing content. Write any short keyword label you already know.',
33
- "M-10 fix: model was reading files before compressing to summary, believing it needed semantic content. It does not. The body is preserved on disk; only context visibility changes.",
34
- ],
35
- [
36
- '* `summary="..."` (<= 80 chars) persists across fidelity changes',
37
- "Model-authored descriptions survive demotion. No janitorial pass needed.",
38
- ],
39
- [
40
- "* YOU MUST NOT use <sh/> or <env/> to read, create, or edit files",
41
- "Forces file operations through set/get. Prevents untracked mutations.",
42
- ],
43
- [
44
- "* Editing: s/old/new/ sed patterns and literal SEARCH/REPLACE blocks",
45
- "Both syntaxes supported. Hedberg normalizes either form.",
46
- ],
17
+ new text
18
+ >>>>>>> REPLACE
19
+ </set>`,
20
+ "SEARCH/REPLACE block — primary edit pattern for existing files.",
21
+ ],
22
+ [
23
+ 'Example: <set path="src/config.js">s/port = 3000/port = 8080/g;s/host = 127.0.0.1/host = localhost/g;</set>',
24
+ "Sed syntax: chained s/old/new/ patterns with semicolons.",
25
+ ],
26
+ [
27
+ 'Example: <set path="example.md">Full file content here</set>',
28
+ "Create: body contents are entire file.",
29
+ ],
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.",
33
+ ],
47
34
  ];
48
35
 
49
36
  export default LINES.map(([text]) => text).join("\n");
@@ -2,23 +2,18 @@
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
- // --- Syntax
6
5
  ["## <sh>[command]</sh> - Run a shell command with side effects"],
7
-
8
- // --- Examples: install and test — real mutations
9
6
  [
10
7
  "Example: <sh>npm install express</sh>",
11
- "Package install. Shows a real side-effect command.",
8
+ "Package install. Real side-effect command.",
12
9
  ],
13
10
  [
14
11
  "Example: <sh>npm test</sh>",
15
12
  "Test execution. Another common side-effect action.",
16
13
  ],
17
-
18
- // --- Constraints
19
14
  [
20
15
  "* YOU MUST NOT use <sh/> to read, create, or edit files — use <get/> and <set/>",
21
- "Forces file operations through the entry system. Prevents untracked mutations.",
16
+ "Forces file operations through the entry system.",
22
17
  ],
23
18
  [
24
19
  "* YOU MUST use <env/> for commands without side effects",
@@ -7,6 +7,7 @@ export default class Summarize {
7
7
  this.#core = core;
8
8
  core.ensureTool();
9
9
  core.registerScheme({ category: "logging" });
10
+ core.on("handler", this.handler.bind(this));
10
11
  core.on("full", this.full.bind(this));
11
12
  core.on("summary", this.summary.bind(this));
12
13
  core.filter("instructions.toolDocs", async (docsMap) => {
@@ -15,6 +16,12 @@ export default class Summarize {
15
16
  });
16
17
  }
17
18
 
19
+ async handler(entry, rummy) {
20
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
21
+ const statusPath = await store.slugPath(runId, "summarize", entry.body);
22
+ await store.upsert(runId, turn, statusPath, entry.body, 200, { loopId });
23
+ }
24
+
18
25
  full(entry) {
19
26
  return `# summarize\n${entry.body}`;
20
27
  }
@@ -2,31 +2,26 @@
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
- // --- Syntax
6
5
  ["## <summarize>[answer or summary]</summarize> - Signal completion"],
7
-
8
- // --- Examples: answer and task completion
9
6
  [
10
7
  "Example: <summarize>The port is 8080</summarize>",
11
- "Direct answer. Shows summarize as the vehicle for delivering answers.",
8
+ "Direct answer. Summarize delivers answers.",
12
9
  ],
13
10
  [
14
11
  "Example: <summarize>Installed express, updated config</summarize>",
15
- "Task summary. Shows summarize for action completion.",
12
+ "Task summary. Action completion.",
16
13
  ],
17
-
18
- // --- Constraints: RFC-style MUST/MUST NOT
19
14
  [
20
- "* YOU MUST use <summarize> when done — describes the final state",
21
- "Completion signal. Without this, the loop continues indefinitely.",
15
+ "* YOU MUST use <summarize></summarize> when done — describes the final state",
16
+ "Completion signal.",
22
17
  ],
23
18
  [
24
19
  "* YOU MUST NOT use <summarize> if still working — use <update/> instead",
25
- "Mutual exclusion with update. Prevents premature completion.",
20
+ "Mutual exclusion with update.",
26
21
  ],
27
22
  [
28
23
  "* YOU MUST keep <summarize> to <= 80 characters",
29
- "Length cap. Matches the summary attribute constraint. Prevents verbose output.",
24
+ "Length cap.",
30
25
  ],
31
26
  ];
32
27
 
@@ -1,5 +1,17 @@
1
+ import docs from "./thinkDoc.js";
2
+
3
+ const THINK_ENABLED = process.env.RUMMY_THINK;
4
+ if (THINK_ENABLED === undefined) throw new Error("RUMMY_THINK must be set (1 or 0)");
5
+
1
6
  export default class Think {
2
7
  constructor(core) {
3
8
  core.registerScheme({ modelVisible: 0, category: "logging" });
9
+ if (THINK_ENABLED === "1") {
10
+ core.ensureTool();
11
+ core.filter("instructions.toolDocs", async (docsMap) => {
12
+ docsMap.think = docs;
13
+ return docsMap;
14
+ });
15
+ }
4
16
  }
5
17
  }
@@ -0,0 +1,18 @@
1
+ // Tool doc for <think/>. Each entry: [text, rationale].
2
+ // Text goes to the model. Rationale stays in source.
3
+ // Changing ANY line requires reading ALL rationales first.
4
+ const LINES = [
5
+ [
6
+ "## <think>[reasoning]</think> - Think before acting",
7
+ ],
8
+ [
9
+ "* Use <think> before any other tools to plan your approach",
10
+ "Positioning: think first, then act. Prevents degenerate tool-call storms.",
11
+ ],
12
+ [
13
+ "* Reasoning inside <think> is private — it does not appear in your context",
14
+ "Frees the model to reason without consuming context budget.",
15
+ ],
16
+ ];
17
+
18
+ export default LINES.map(([text]) => text).join("\n");
@@ -9,7 +9,9 @@ export default class Unknown {
9
9
  core.registerScheme({
10
10
  category: "unknown",
11
11
  });
12
+ core.on("handler", this.handler.bind(this));
12
13
  core.on("full", this.full.bind(this));
14
+ core.on("summary", this.summary.bind(this));
13
15
  core.filter("assembly.system", this.assembleUnknowns.bind(this), 300);
14
16
  core.filter("instructions.toolDocs", async (docsMap) => {
15
17
  docsMap.unknown = docs;
@@ -17,10 +19,29 @@ export default class Unknown {
17
19
  });
18
20
  }
19
21
 
22
+ async handler(entry, rummy) {
23
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
24
+
25
+ // Deduplicate — if this exact body already exists, skip
26
+ const existingValues = await store.getUnknownValues(runId);
27
+ if (existingValues.has(entry.body)) {
28
+ console.warn(`[RUMMY] Unknown deduped: "${entry.body.slice(0, 60)}"`);
29
+ return;
30
+ }
31
+
32
+ // Generate slug path and upsert
33
+ const unknownPath = await store.slugPath(runId, "unknown", entry.body);
34
+ await store.upsert(runId, turn, unknownPath, entry.body, 200, { loopId });
35
+ }
36
+
20
37
  full(entry) {
21
38
  return `# unknown\n${entry.body}`;
22
39
  }
23
40
 
41
+ summary(entry) {
42
+ return this.full(entry);
43
+ }
44
+
24
45
  async assembleUnknowns(content, ctx) {
25
46
  const entries = ctx.rows.filter((r) => r.category === "unknown");
26
47
  if (entries.length === 0) return content;
@@ -2,29 +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
- // --- Syntax: body = what you need to learn
6
5
  [
7
- `## <unknown>[specific thing I need to learn]</unknown> - Track open questions`,
6
+ "## <unknown>[specific thing I need to learn]</unknown> - Track open questions",
8
7
  ],
9
-
10
- // --- Examples: concrete unknowns, not abstract
11
8
  [
12
- `Example: <unknown path="unknown://answer">contents of answer.txt</unknown>`,
13
- `Specific and actionable. Shows that unknowns are concrete investigation targets.`,
9
+ 'Example: <unknown path="unknown://answer">contents of answer.txt</unknown>',
10
+ "Path form: explicit unknown path for structured tracking.",
14
11
  ],
15
12
  [
16
- `Example: <unknown>which database adapter is configured</unknown>`,
17
- `Domain question. Shows unknowns for configuration/architecture questions.`,
13
+ "Example: <unknown>which database adapter is configured</unknown>",
14
+ "Body form: question as body, path auto-generated.",
18
15
  ],
19
-
20
- // --- Lifecycle: register → investigate → resolve
21
16
  [
22
- `* Investigate with Tool Commands`,
23
- `Cross-tool lifecycle: unknowns drive get/env/ask_user actions.`,
17
+ "* Investigate with Tool Commands",
18
+ "Unknowns drive action get, env, search, ask_user.",
24
19
  ],
25
20
  [
26
- `* When resolved or irrelevant, remove with <rm path="unknown://..."/>`,
27
- `Cross-tool lifecycle: rm cleans resolved unknowns from context.`,
21
+ '* When resolved or irrelevant, remove with <set path="unknown://..." fidelity="archive"/>',
22
+ "Archive instead of delete preserves the question for context history.",
28
23
  ],
29
24
  ];
30
25
 
@@ -7,6 +7,7 @@ export default class Update {
7
7
  this.#core = core;
8
8
  core.ensureTool();
9
9
  core.registerScheme({ category: "logging" });
10
+ core.on("handler", this.handler.bind(this));
10
11
  core.on("full", this.full.bind(this));
11
12
  core.on("summary", this.summary.bind(this));
12
13
  core.filter("instructions.toolDocs", async (docsMap) => {
@@ -15,6 +16,12 @@ export default class Update {
15
16
  });
16
17
  }
17
18
 
19
+ async handler(entry, rummy) {
20
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
21
+ const statusPath = await store.slugPath(runId, "update", entry.body);
22
+ await store.upsert(runId, turn, statusPath, entry.body, 200, { loopId });
23
+ }
24
+
18
25
  full(entry) {
19
26
  return `# update\n${entry.body}`;
20
27
  }
@@ -2,31 +2,26 @@
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
- // --- Syntax
6
5
  ["## <update>[brief status]</update> - Signal continuation"],
7
-
8
- // --- Examples: research progress and multi-step work
9
6
  [
10
7
  "Example: <update>Reading config files</update>",
11
- "Progress checkpoint. Shows update as a status signal, not a log entry.",
8
+ "Progress checkpoint. Status signal, not a log entry.",
12
9
  ],
13
10
  [
14
11
  "Example: <update>Found 3 issues, fixing first</update>",
15
- "Multi-step progress. Shows update for ongoing work.",
12
+ "Multi-step progress. Ongoing work.",
16
13
  ],
17
-
18
- // --- Constraints: RFC-style MUST/MUST NOT
19
14
  [
20
- "* YOU MUST use <update> if still working — describes the current state",
21
- "Continuation signal. Triggers the next turn in the loop.",
15
+ "* YOU MUST use <update></update> if still working — describes the current state",
16
+ "Continuation signal. Triggers the next turn.",
22
17
  ],
23
18
  [
24
19
  "* YOU MUST NOT use <update> if done — use <summarize/> instead",
25
- "Mutual exclusion with summarize. Prevents infinite loops.",
20
+ "Mutual exclusion with summarize.",
26
21
  ],
27
22
  [
28
23
  "* YOU MUST keep <update> to <= 80 characters",
29
- "Length cap. Prevents models from writing essays in status updates.",
24
+ "Length cap.",
30
25
  ],
31
26
  ];
32
27
 
@@ -36,6 +36,15 @@ export default class ClientConnection {
36
36
  }
37
37
  };
38
38
 
39
+ #onProposal = (payload) => {
40
+ if (payload.projectId === this.#context.projectId) {
41
+ this.#sendNotification("run/proposal", {
42
+ run: payload.run,
43
+ proposed: payload.proposed,
44
+ });
45
+ }
46
+ };
47
+
39
48
  #onRender = (payload) => {
40
49
  if (payload.projectId === this.#context.projectId) {
41
50
  this.#sendNotification("ui/render", {
@@ -63,7 +72,6 @@ export default class ClientConnection {
63
72
  summary: payload.summary,
64
73
  history: payload.history,
65
74
  unknowns: payload.unknowns,
66
- proposed: payload.proposed,
67
75
  telemetry: payload.telemetry,
68
76
  });
69
77
  }
@@ -71,6 +79,7 @@ export default class ClientConnection {
71
79
 
72
80
  #setupNotifications() {
73
81
  this.#hooks.run.progress.on(this.#onProgress);
82
+ this.#hooks.turn.proposal.on(this.#onProposal);
74
83
  this.#hooks.ui.render.on(this.#onRender);
75
84
  this.#hooks.ui.notify.on(this.#onNotify);
76
85
  this.#hooks.run.state.on(this.#onState);
@@ -78,6 +87,7 @@ export default class ClientConnection {
78
87
 
79
88
  #teardown() {
80
89
  this.#hooks.run.progress.off(this.#onProgress);
90
+ this.#hooks.turn.proposal.off(this.#onProposal);
81
91
  this.#hooks.ui.render.off(this.#onRender);
82
92
  this.#hooks.ui.notify.off(this.#onNotify);
83
93
  this.#hooks.run.state.off(this.#onState);
@@ -13,7 +13,7 @@ visible AS (
13
13
  , ke.turn
14
14
  , ke.updated_at
15
15
  , ke.attributes
16
- , ke.tokens AS tokens_full
16
+ , ke.tokens
17
17
  , COALESCE(s.category, 'logging') AS category
18
18
  , CASE
19
19
  -- Archived entries not in context
@@ -41,6 +41,7 @@ projected AS (
41
41
  , attributes
42
42
  -- Category comes from schemes table — plugins declare it via registerScheme().
43
43
  , category
44
+ , tokens
44
45
  , CASE
45
46
  WHEN visible_fidelity IN ('full', 'summary') THEN body
46
47
  ELSE ''
@@ -71,9 +72,8 @@ SELECT
71
72
  END
72
73
  , CASE scheme WHEN 'skill' THEN 0 ELSE 1 END
73
74
  , CASE fidelity
74
- WHEN 'index' THEN 0
75
- WHEN 'summary' THEN 1
76
- ELSE 2
75
+ WHEN 'summary' THEN 0
76
+ ELSE 1
77
77
  END
78
78
  , turn
79
79
  , updated_at