@possumtech/rummy 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.env.example +2 -1
  2. package/PLUGINS.md +1 -1
  3. package/SPEC.md +181 -38
  4. package/migrations/001_initial_schema.sql +1 -1
  5. package/package.json +7 -3
  6. package/service.js +5 -3
  7. package/src/agent/AgentLoop.js +182 -136
  8. package/src/agent/ContextAssembler.js +2 -0
  9. package/src/agent/KnownStore.js +28 -85
  10. package/src/agent/ResponseHealer.js +65 -31
  11. package/src/agent/TurnExecutor.js +326 -181
  12. package/src/agent/XmlParser.js +5 -2
  13. package/src/agent/known_store.sql +48 -0
  14. package/src/agent/tokens.js +1 -0
  15. package/src/agent/turns.sql +5 -0
  16. package/src/hooks/HookRegistry.js +7 -0
  17. package/src/hooks/Hooks.js +1 -4
  18. package/src/hooks/ToolRegistry.js +2 -8
  19. package/src/plugins/budget/README.md +2 -14
  20. package/src/plugins/budget/budget.js +15 -39
  21. package/src/plugins/cp/cp.js +1 -1
  22. package/src/plugins/cp/cpDoc.js +1 -1
  23. package/src/plugins/get/get.js +71 -1
  24. package/src/plugins/get/getDoc.js +14 -4
  25. package/src/plugins/hedberg/matcher.js +10 -29
  26. package/src/plugins/instructions/preamble.md +16 -6
  27. package/src/plugins/known/known.js +4 -10
  28. package/src/plugins/known/knownDoc.js +15 -14
  29. package/src/plugins/mv/mv.js +18 -1
  30. package/src/plugins/mv/mvDoc.js +15 -1
  31. package/src/plugins/{current → performed}/README.md +4 -3
  32. package/src/plugins/{current/current.js → performed/performed.js} +15 -20
  33. package/src/plugins/previous/README.md +2 -1
  34. package/src/plugins/previous/previous.js +31 -25
  35. package/src/plugins/progress/README.md +1 -2
  36. package/src/plugins/progress/progress.js +15 -29
  37. package/src/plugins/prompt/prompt.js +0 -7
  38. package/src/plugins/rm/rm.js +27 -15
  39. package/src/plugins/rm/rmDoc.js +3 -3
  40. package/src/plugins/set/set.js +55 -19
  41. package/src/plugins/set/setDoc.js +6 -2
  42. package/src/plugins/telemetry/telemetry.js +14 -9
  43. package/src/plugins/unknown/README.md +2 -1
  44. package/src/plugins/unknown/unknown.js +5 -4
  45. package/src/server/ClientConnection.js +59 -45
  46. package/src/sql/v_model_context.sql +3 -13
  47. package/src/plugins/budget/BudgetGuard.js +0 -74
@@ -189,3 +189,51 @@ WHERE
189
189
  run_id = :run_id
190
190
  AND hedmatch(:path, path)
191
191
  AND (:body IS NULL OR hedsearch(:body, body));
192
+
193
+ -- PREP: restore_summarized_prompts
194
+ -- Restore prompt entries demoted to summary by a recovery phase that was
195
+ -- interrupted (e.g. server crash). Safe to call unconditionally at loop
196
+ -- start: if the full prompt would overflow, Prompt Demotion handles it.
197
+ UPDATE known_entries
198
+ SET
199
+ fidelity = 'full'
200
+ , tokens = tokens_full
201
+ , updated_at = CURRENT_TIMESTAMP
202
+ WHERE run_id = :run_id AND scheme = 'prompt' AND fidelity = 'summary';
203
+
204
+ -- PREP: demote_previous_loop_logging
205
+ -- Demote full logging entries from all other loops to summary.
206
+ -- Fires at loop start so <previous> entries are already compact.
207
+ UPDATE known_entries
208
+ SET
209
+ fidelity = 'summary'
210
+ , tokens = COALESCE(
211
+ countTokens(json_extract(attributes, '$.summary'))
212
+ , countTokens(substr(body, 1, 80))
213
+ )
214
+ , updated_at = CURRENT_TIMESTAMP
215
+ WHERE
216
+ run_id = :run_id
217
+ AND (loop_id IS NULL OR loop_id != :loop_id)
218
+ AND fidelity = 'full'
219
+ AND scheme IN (SELECT name FROM schemes WHERE category = 'logging');
220
+
221
+ -- PREP: demote_turn_data_entries
222
+ -- Demote full data entries from a turn to summary with 413 status.
223
+ -- Fires when end-of-turn materialization exceeds the context ceiling.
224
+ UPDATE known_entries
225
+ SET
226
+ fidelity = 'summary'
227
+ , status = 413
228
+ , tokens = COALESCE(
229
+ countTokens(json_extract(attributes, '$.summary'))
230
+ , countTokens(substr(body, 1, 80))
231
+ )
232
+ , updated_at = CURRENT_TIMESTAMP
233
+ WHERE
234
+ run_id = :run_id
235
+ AND turn = :turn
236
+ AND fidelity = 'full'
237
+ AND status < 400
238
+ AND scheme IN (SELECT name FROM schemes WHERE category = 'data')
239
+ RETURNING path;
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const DIVISOR = Number(process.env.RUMMY_TOKEN_DIVISOR);
9
+ if (!DIVISOR) throw new Error("RUMMY_TOKEN_DIVISOR must be a non-zero number");
9
10
 
10
11
  export function countTokens(text) {
11
12
  if (!text) return 0;
@@ -27,6 +27,11 @@ SELECT
27
27
  FROM turns
28
28
  WHERE run_id = :run_id;
29
29
 
30
+ -- PREP: get_turn_context_tokens
31
+ SELECT context_tokens
32
+ FROM turns
33
+ WHERE run_id = :run_id AND sequence = :sequence;
34
+
30
35
  -- PREP: get_last_context_tokens
31
36
  SELECT context_tokens
32
37
  FROM turns
@@ -63,6 +63,13 @@ export default class HookRegistry {
63
63
  this.#events.get(tag).sort((a, b) => a.priority - b.priority);
64
64
  }
65
65
 
66
+ removeEvent(tag, callback) {
67
+ const hooks = this.#events.get(tag);
68
+ if (!hooks) return;
69
+ const idx = hooks.findIndex((h) => h.callback === callback);
70
+ if (idx !== -1) hooks.splice(idx, 1);
71
+ }
72
+
66
73
  async emitEvent(tag, ...args) {
67
74
  const hooks = this.#events.get(tag) || [];
68
75
  for (const h of hooks) {
@@ -11,6 +11,7 @@ export default function createHooks(debug = false) {
11
11
 
12
12
  const createEvent = (tag) => ({
13
13
  on: (callback, priority) => registry.addEvent(tag, callback, priority),
14
+ off: (callback) => registry.removeEvent(tag, callback),
14
15
  emit: (...args) => registry.emitEvent(tag, ...args),
15
16
  });
16
17
 
@@ -73,10 +74,6 @@ export default function createHooks(debug = false) {
73
74
  started: createEvent("act.started"),
74
75
  completed: createEvent("act.completed"),
75
76
  },
76
- panic: {
77
- started: createEvent("panic.started"),
78
- completed: createEvent("panic.completed"),
79
- },
80
77
  llm: {
81
78
  request: {
82
79
  started: createEvent("llm.request.started"),
@@ -118,19 +118,13 @@ export default class ToolRegistry {
118
118
  */
119
119
  resolveForLoop(
120
120
  mode,
121
- { noInteraction = false, noWeb = false, noBench = false } = {},
121
+ { noInteraction = false, noWeb = false, noProposals = false } = {},
122
122
  ) {
123
123
  const excluded = new Set();
124
124
  if (mode === "ask") excluded.add("sh");
125
- if (mode === "panic") {
126
- excluded.add("sh");
127
- excluded.add("env");
128
- excluded.add("search");
129
- excluded.add("ask_user");
130
- }
131
125
  if (noInteraction) excluded.add("ask_user");
132
126
  if (noWeb) excluded.add("search");
133
- if (noBench) {
127
+ if (noProposals) {
134
128
  excluded.add("ask_user");
135
129
  excluded.add("env");
136
130
  excluded.add("sh");
@@ -1,11 +1,10 @@
1
1
  # budget
2
2
 
3
- Context ceiling enforcement and panic mode recovery.
3
+ Context ceiling enforcement.
4
4
 
5
5
  ## Files
6
6
 
7
- - **budget.js** — Plugin. Pre-LLM enforce, BudgetGuard activation,
8
- panic prompt generation.
7
+ - **budget.js** — Plugin. Pre-LLM enforce, BudgetGuard activation.
9
8
  - **BudgetGuard.js** — Write-layer gate. Installed on KnownStore during
10
9
  dispatch. Checks token delta on every upsert, promote, and body update.
11
10
 
@@ -14,7 +13,6 @@ Context ceiling enforcement and panic mode recovery.
14
13
  - **Hook**: `hooks.budget.enforce` — pre-LLM ceiling check.
15
14
  - **Hook**: `hooks.budget.activate(store, contextSize, assembledTokens)` — install guard.
16
15
  - **Hook**: `hooks.budget.deactivate(store)` — remove guard.
17
- - **Hook**: `hooks.budget.panicPrompt({ shortfall, assembledTokens, contextSize })` — generate panic prompt.
18
16
 
19
17
  ## Budget Contract
20
18
 
@@ -31,13 +29,3 @@ Exemptions: `status >= 400` (error entries), `model_visible = 0` (audit),
31
29
 
32
30
  On first violation: `BudgetExceeded` thrown, guard trips, all subsequent
33
31
  writes fail. TurnExecutor catches per-tool, writes 413 result entry.
34
-
35
- ## Panic Mode
36
-
37
- When a new prompt exceeds the ceiling, AgentLoop enqueues a panic loop.
38
- The model receives the exact shortfall and must free space using core
39
- tools (get, set, known, unknown, rm, mv, cp, summarize, update).
40
- Excluded: sh, env, search, ask_user.
41
-
42
- Strike system: 3 consecutive turns without context reduction = hard 413.
43
- Any reduction resets the counter. One panic attempt per drain cycle.
@@ -1,56 +1,42 @@
1
1
  import { countTokens } from "../../agent/tokens.js";
2
- import BudgetGuard, { BudgetExceeded } from "./BudgetGuard.js";
3
2
 
4
3
  function measureMessages(messages) {
5
4
  return messages.reduce((sum, m) => sum + countTokens(m.content), 0);
6
5
  }
7
6
 
8
- export { BudgetExceeded };
9
-
10
7
  export default class Budget {
11
8
  #core;
12
9
 
13
10
  constructor(core) {
14
11
  this.#core = core;
12
+ core.registerScheme({
13
+ name: "budget",
14
+ modelVisible: 1,
15
+ category: "logging",
16
+ });
17
+ core.hooks.tools.onView("budget", (entry) => entry.body);
15
18
  core.hooks.budget = {
16
19
  enforce: this.enforce.bind(this),
17
- activate: this.activate.bind(this),
18
- deactivate: this.deactivate.bind(this),
19
- panicPrompt: Budget.panicPrompt,
20
- BudgetExceeded,
21
20
  };
22
21
  }
23
22
 
24
- static panicPrompt({ assembledTokens, contextSize }) {
25
- const target = Math.floor(contextSize * 0.75);
26
- const mustFree = assembledTokens - target;
27
- return [
28
- `CONTEXT OVERFLOW: ${assembledTokens} tokens, ceiling ${contextSize}.`,
29
- `YOU MUST free ${mustFree} tokens to get below ${target} (75%).`,
30
- "YOU MUST NOT load or create new content. Only reduce.",
31
- "",
32
- "<knowns> above shows each entry with its token count.",
33
- "Target the largest entries first.",
34
- '<rm path="..."/> to delete entries you no longer need.',
35
- '<set path="..." fidelity="summary" summary="keywords"/> to compress.',
36
- '<set path="..." fidelity="archive"/> to archive out of context.',
37
- "<summarize/> when done. <update/> if still working.",
38
- ].join("\n");
39
- }
40
-
41
- async enforce({ contextSize, messages, rows }) {
23
+ async enforce({ contextSize, messages, rows, lastPromptTokens = 0 }) {
42
24
  if (!contextSize) {
43
25
  return { messages, rows, demoted: [], assembledTokens: 0, status: 200 };
44
26
  }
45
27
 
46
- const assembledTokens = measureMessages(messages);
28
+ // Prefer actual prompt_tokens from the last API response — the estimate
29
+ // from measureMessages can be wildly off for structured/XML-heavy content.
30
+ const assembledTokens =
31
+ lastPromptTokens > 0 ? lastPromptTokens : measureMessages(messages);
47
32
 
48
33
  console.warn(
49
- `[RUMMY] Budget enforce: ${assembledTokens} tokens, ceiling ${contextSize}, ${rows.length} rows`,
34
+ `[RUMMY] Budget enforce: ${assembledTokens} tokens (${lastPromptTokens > 0 ? "actual" : "estimated"}), ceiling ${contextSize}, ${rows.length} rows`,
50
35
  );
51
36
 
52
- if (assembledTokens > contextSize) {
53
- const overflow = assembledTokens - contextSize;
37
+ const ceiling = Math.floor(contextSize * 0.9);
38
+ if (assembledTokens > ceiling) {
39
+ const overflow = assembledTokens - ceiling;
54
40
  console.warn(
55
41
  `[RUMMY] Budget 413: ${assembledTokens} tokens > ${contextSize} ceiling (${overflow} over)`,
56
42
  );
@@ -66,14 +52,4 @@ export default class Budget {
66
52
 
67
53
  return { messages, rows, demoted: [], assembledTokens, status: 200 };
68
54
  }
69
-
70
- activate(store, contextSize, assembledTokens) {
71
- const guard = new BudgetGuard(contextSize, assembledTokens);
72
- store.budgetGuard = guard;
73
- return guard;
74
- }
75
-
76
- deactivate(store) {
77
- store.budgetGuard = null;
78
- }
79
55
  }
@@ -19,7 +19,7 @@ export default class Cp {
19
19
  async handler(entry, rummy) {
20
20
  const { entries: store, sequence: turn, runId, loopId } = rummy;
21
21
  const { path, to } = entry.attributes;
22
- const VALID = { stored: 1, summary: 1, index: 1, full: 1 };
22
+ const VALID = { stored: 1, summary: 1, index: 1, full: 1, archive: 1 };
23
23
  const fidelity = VALID[entry.attributes.fidelity]
24
24
  ? entry.attributes.fidelity
25
25
  : undefined;
@@ -17,7 +17,7 @@ const LINES = [
17
17
 
18
18
  // --- Constraints
19
19
  [
20
- "* Source path accepts globs: `src/*.js`, `known://draft_*`",
20
+ "* Source path accepts patterns: `src/*.js`, `known://draft_*`",
21
21
  "Pattern support. Distributes glob teaching beyond get.",
22
22
  ],
23
23
  [
@@ -30,13 +30,81 @@ export default class Get {
30
30
  const normalized = KnownStore.normalizePath(target);
31
31
  const bodyFilter = entry.attributes.body || null;
32
32
  const isPattern = bodyFilter || normalized.includes("*");
33
+
34
+ const line =
35
+ entry.attributes.line != null
36
+ ? Math.max(1, parseInt(entry.attributes.line, 10))
37
+ : null;
38
+ const limit =
39
+ entry.attributes.limit != null
40
+ ? Math.max(1, parseInt(entry.attributes.limit, 10))
41
+ : null;
42
+
33
43
  const matches = await store.getEntriesByPattern(
34
44
  runId,
35
45
  normalized,
36
46
  bodyFilter,
37
47
  );
38
48
 
49
+ // Partial read — no fidelity promotion, returns a line slice as the log item.
50
+ if (line !== null || limit !== null) {
51
+ if (isPattern) {
52
+ await store.upsert(
53
+ runId,
54
+ turn,
55
+ entry.resultPath,
56
+ "line/limit requires a single path, not a glob or body filter",
57
+ 400,
58
+ { loopId },
59
+ );
60
+ return;
61
+ }
62
+ if (matches.length === 0) {
63
+ await store.upsert(
64
+ runId,
65
+ turn,
66
+ entry.resultPath,
67
+ `${target} not found`,
68
+ 200,
69
+ { loopId },
70
+ );
71
+ return;
72
+ }
73
+ const allLines = matches[0].body.split("\n");
74
+ const total = allLines.length;
75
+ const startLine = line ?? 1;
76
+ const startIdx = startLine - 1;
77
+ const endIdx = limit !== null ? Math.min(startIdx + limit, total) : total;
78
+ const slice = allLines.slice(startIdx, endIdx).join("\n");
79
+ const endLine = endIdx;
80
+ const header = `[lines ${startLine}–${endLine} / ${total} total]`;
81
+ await store.upsert(
82
+ runId,
83
+ turn,
84
+ entry.resultPath,
85
+ `${header}\n${slice}`,
86
+ 200,
87
+ { loopId },
88
+ );
89
+ return;
90
+ }
91
+
92
+ const VALID_FIDELITY = {
93
+ stored: 1,
94
+ summary: 1,
95
+ index: 1,
96
+ full: 1,
97
+ archive: 1,
98
+ };
99
+ const fidelityAttr = VALID_FIDELITY[entry.attributes.fidelity]
100
+ ? entry.attributes.fidelity
101
+ : null;
102
+
39
103
  await store.promoteByPattern(runId, normalized, bodyFilter, turn);
104
+ if (fidelityAttr) {
105
+ for (const match of matches)
106
+ await store.setFidelity(runId, match.path, fidelityAttr);
107
+ }
40
108
 
41
109
  if (isPattern) {
42
110
  await storePatternResult(
@@ -53,7 +121,9 @@ export default class Get {
53
121
  const total = matches.reduce((s, m) => s + m.tokens_full, 0);
54
122
  const paths = matches.map((m) => m.path).join(", ");
55
123
  const body =
56
- matches.length > 0 ? `${paths} ${total} tokens` : `${target} not found`;
124
+ matches.length > 0
125
+ ? `${paths} loaded into <knowns> (${total} tokens)`
126
+ : `${target} not found`;
57
127
  await store.upsert(runId, turn, entry.resultPath, body, 200, {
58
128
  loopId,
59
129
  });
@@ -15,13 +15,19 @@ const LINES = [
15
15
  "Keyword recall: glob in path, search term in body. Cross-scheme hedberg pattern.",
16
16
  ],
17
17
  [
18
- 'Example: <get path="src/**/*.js" preview>TODO</get>',
19
- "Full pattern: recursive glob + preview + content filter. Shows all 3 features at once.",
18
+ 'Example: <get path="src/**/*.js" preview>authentication</get>',
19
+ "Full pattern: recursive glob + preview + content filter. Shows all 3 features at once. Body is a filter keyword, never file content.",
20
+ ],
21
+
22
+ // --- Partial read: line/limit — show before constraints so model sees it as a first-class pattern
23
+ [
24
+ 'Example: <get path="src/agent/AgentLoop.js" line="644" limit="80"/>',
25
+ "Partial read. Returns lines 644–723 as the log item without promoting the entry to full. Use summary fidelity to find line numbers, then target the symbol directly.",
20
26
  ],
21
27
 
22
28
  // --- Constraints: RFC-style. Each prevents a specific failure mode.
23
29
  [
24
- "* Paths accept globs: `src/**/*.js`, `known://api_*`",
30
+ "* Paths accept patterns: `src/**/*.js`, `known://api_*`",
25
31
  "Reinforces picomatch patterns work everywhere, not just in examples.",
26
32
  ],
27
33
  [
@@ -33,7 +39,11 @@ const LINES = [
33
39
  "Generalizes examples 2-3. Body = filter, not just path.",
34
40
  ],
35
41
  [
36
- '* Use <set path="..." fidelity="index"/> to archive loaded content',
42
+ "* `line` and `limit` read a slice without promoting — patterns not allowed",
43
+ "The no-promotion constraint is what makes partial read safe: context budget is unaffected.",
44
+ ],
45
+ [
46
+ '* Use <set path="..." fidelity="archive"/> to remove loaded content from context',
37
47
  "Lifecycle: get→set. Load, read, archive. Prevents context hoarding.",
38
48
  ],
39
49
  ];
@@ -1,34 +1,15 @@
1
- import { execSync } from "node:child_process";
2
- import { unlinkSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
1
+ import { createTwoFilesPatch } from "diff";
5
2
 
6
3
  export function generatePatch(filePath, oldContent, newContent) {
7
- const id = `${Date.now()}_${Math.random().toString(36).slice(2)}`;
8
- const oldPath = join(tmpdir(), `rummy_diff_old_${id}`);
9
- const newPath = join(tmpdir(), `rummy_diff_new_${id}`);
10
-
11
- try {
12
- writeFileSync(oldPath, oldContent);
13
- writeFileSync(newPath, newContent);
14
-
15
- const result = execSync(
16
- `diff -u --label "${filePath}\told" --label "${filePath}\tnew" "${oldPath}" "${newPath}"`,
17
- { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] },
18
- );
19
- return result;
20
- } catch (err) {
21
- // diff exits 1 when files differ — that's the success case
22
- if (err.stdout) return err.stdout;
23
- return "";
24
- } finally {
25
- try {
26
- unlinkSync(oldPath);
27
- } catch {}
28
- try {
29
- unlinkSync(newPath);
30
- } catch {}
31
- }
4
+ return createTwoFilesPatch(
5
+ `${filePath}\told`,
6
+ `${filePath}\tnew`,
7
+ oldContent,
8
+ newContent,
9
+ "",
10
+ "",
11
+ { context: 3 },
12
+ );
32
13
  }
33
14
 
34
15
  export default class HeuristicMatcher {
@@ -1,15 +1,25 @@
1
- You are an assistant. YOU MUST gather information, then YOU MAY either answer questions or take action.
1
+ You are a folksonomic memory agent. YOU MUST organize all information into searchable taxonomies with navigable path hierarchies and searchable summary tags, then YOU MAY answer questions and/or take action.
2
2
 
3
3
  # Response Rules
4
4
 
5
5
  Required: YOU MUST respond with Tool Commands in the XML format. YOU MAY use multiple tools in your response.
6
+
6
7
  Optional: YOU MAY think in an optional <think></think> tag before using any other Tool Commands.
7
- Required: YOU MUST register all unknowns with <unknown>(specific thing I need to learn)</unknown>.
8
- Required: YOU MUST register all new information, decisions, and plans with <known>(specific information, ideas, or plans)</known>.
9
- Required: YOU MUST conclude every turn with EITHER <update/> if still working OR <summarize/> if done. Never both.
10
- Required: Path and summary information is approximate. YOU MUST use <get> to verify before acting on summarized content.
8
+
9
+ Required: YOU MUST register all unknowns with <unknown>[specific thing I need to learn]</unknown>.
10
+
11
+ Required: YOU MUST register all new facts, decisions, and plans with <known path="topic/subtopic" summary="keyword,keyword,keyword">[specific facts, decisions, or plans]</known>.
12
+ Required: Every <known> MUST include summary="keyword,keyword" tags.
13
+ Info: Paths are addresses for tools. Summary tags tell you what's inside.
14
+ Info: Path and summary information is approximate. YOU MUST use <get/> to verify before acting on summarized content.
11
15
  Info: When information conflicts, later turns are more likely to be relevant and correct than earlier turns.
12
- Info: Your context is limited but your storage is not. Organize and categorize your information, ideas, plans, and history to optimize your context.
16
+ Info: Your context is limited but your archive is not. Organize and categorize your facts, decisions, plans, and history to optimize your context.
17
+
18
+ Required: YOU MUST promote all relevant "summary" entries to "full".
19
+ Required: YOU MUST demote all irrelevant "full" entries to "summary".
20
+
21
+ Required: YOU MUST conclude every turn with EITHER <update></update> if still working OR <summarize></summarize> if done. Never both.
22
+ Required: YOU MUST use one and only one <update></update> or <summarize></summarize> tag, and only at the end.
13
23
 
14
24
  # Tool Commands
15
25
 
@@ -16,9 +16,9 @@ export default class Known {
16
16
  }
17
17
 
18
18
  async handler(entry, rummy) {
19
- const { entries: store, sequence: turn, runId } = rummy;
19
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
20
20
  const target = entry.attributes.path || entry.resultPath;
21
- await store.upsert(runId, turn, target, entry.body, 200);
21
+ await store.upsert(runId, turn, target, entry.body, 200, { loopId });
22
22
  }
23
23
 
24
24
  full(entry) {
@@ -31,13 +31,12 @@ export default class Known {
31
31
 
32
32
  // Rows arrive pre-sorted by SQL: skill → index → summary → full, then by recency
33
33
  const demotedSet = new Set(ctx.demoted || []);
34
- const panic = ctx.type === "panic";
35
- const lines = entries.map((e) => renderKnownTag(e, demotedSet, panic));
34
+ const lines = entries.map((e) => renderKnownTag(e, demotedSet));
36
35
  return `${content}\n\n<knowns>\n${lines.join("\n")}\n</knowns>`;
37
36
  }
38
37
  }
39
38
 
40
- function renderKnownTag(entry, demotedSet, panic = false) {
39
+ function renderKnownTag(entry, demotedSet) {
41
40
  const tag = entry.scheme || "file";
42
41
  const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
43
42
  const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
@@ -45,11 +44,6 @@ function renderKnownTag(entry, demotedSet, panic = false) {
45
44
  const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
46
45
  const flag = demotedSet?.has(entry.path) ? " demoted" : "";
47
46
 
48
- // Panic mode: index-only view so context fits in LLM window
49
- if (panic) {
50
- return `<${tag} path="${entry.path}"${turn}${fidelity}${tokens}/>`;
51
- }
52
-
53
47
  const attrs =
54
48
  typeof entry.attributes === "string"
55
49
  ? JSON.parse(entry.attributes)
@@ -2,31 +2,32 @@
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 = the information to save
5
+ // --- Syntax: path = slash-separated topic hierarchy, body = the information to save
6
6
  [
7
- "## <known>[specific information, ideas, or plans]</known> - Sort and save what you learn for later recall",
7
+ '## <known path="known://topic/subtopic" summary="keyword,keyword,keyword">[specific facts, decisions, or plans]</known> - Sort and save what you learn for later recall',
8
8
  ],
9
- // --- Examples: summary-with-keywords first (teaches the right pattern)
9
+ // --- Examples: category-level entries multiple related facts per entry, not one per item
10
10
  [
11
- 'Example: <known summary="hedberg,comedian,death,2005">Mitch Hedberg died on March 30, 2005</known>',
12
- "Primary pattern: comma-separated keywords in summary. Path auto-generated from summary as known://hedberg/comedian/death/2005. Keywords become searchable path segments.",
11
+ 'Example: <known path="known://config/database" summary="database,host,port,pool,replica">Host: db.internal. Port: 5432. Pool: 10 connections. Replica: db-replica.internal:5432.</known>',
12
+ "Category entry: all database config facts in one entry. Path is an address (topic/subtopic), body collects every related fact, summary is comma-separated search keywords not a description.",
13
13
  ],
14
14
  [
15
- 'Example: <known path="known://people/rumsfeld" summary="defense,secretary,born,1932">Donald Rumsfeld was born in 1932 and served as Secretary of Defense</known>',
16
- "Explicit path form: slashed path=category/key, summary=keywords. For when the model wants direct control over taxonomy.",
15
+ 'Example: <known path="known://project/milestones" summary="milestone,deadline,alpha,launch,2026">Alpha: 2026-03-01. Beta cutoff: 2026-04-15. GA launch: 2026-06-01.</known>',
16
+ "Timeline entry: all milestone dates under one path. Multiple facts per entry reduces fragmentation. Recall by glob or keyword.",
17
17
  ],
18
- // --- Lifecycle
18
+ // --- Constraints: summary and grouping first (model forms generation pattern from header + examples)
19
19
  [
20
- '* Recall with <get path="known://people/*">keyword</get>',
21
- "Cross-tool lifecycle: glob by category, filter by keyword. Matches the slashed path convention.",
20
+ "* `summary` REQUIRED at summary fidelity the body is hidden; these keywords are your only description",
21
+ "Self-interest framing: without summary, the model has a path but no idea what's inside.",
22
22
  ],
23
23
  [
24
- "* `summary` keywords survive compressionwrite keywords you'll search for later",
25
- "Teaches WHY summaries matter. Keywords become the path AND the compressed view.",
24
+ "* Group related facts by topic one entry per topic category, not one per input chunk",
25
+ "Critical behavioral constraint. Topic grouping enables semantic recall; chunk-based filing creates positional, irretrievable entries.",
26
26
  ],
27
+ // --- Lifecycle
27
28
  [
28
- "* YOU MUST sort and save all new information, ideas, and plans in their own <known> entries",
29
- "Critical behavioral constraint. 'new' prevents re-saving known facts.",
29
+ '* Recall with <get path="known://config/*">replica</get>',
30
+ "Cross-tool lifecycle: glob by category, filter by keyword. Matches the slashed path convention.",
30
31
  ],
31
32
  ];
32
33
 
@@ -19,11 +19,28 @@ export default class Mv {
19
19
  async handler(entry, rummy) {
20
20
  const { entries: store, sequence: turn, runId, loopId } = rummy;
21
21
  const { path, to } = entry.attributes;
22
- const VALID = { stored: 1, summary: 1, index: 1, full: 1 };
22
+ const VALID = { stored: 1, summary: 1, index: 1, full: 1, archive: 1 };
23
23
  const fidelity = VALID[entry.attributes.fidelity]
24
24
  ? entry.attributes.fidelity
25
25
  : undefined;
26
26
 
27
+ // Fidelity-in-place: no destination, change visibility of matched entries
28
+ if (fidelity && !to) {
29
+ const matches = await store.getEntriesByPattern(runId, path);
30
+ for (const match of matches)
31
+ await store.setFidelity(runId, match.path, fidelity);
32
+ const label = fidelity === "archive" ? "archived" : `set to ${fidelity}`;
33
+ await store.upsert(
34
+ runId,
35
+ turn,
36
+ entry.resultPath,
37
+ `${matches.map((m) => m.path).join(", ")} ${label}`,
38
+ 200,
39
+ { fidelity: "archive", loopId },
40
+ );
41
+ return;
42
+ }
43
+
27
44
  const source = await store.getBody(runId, path);
28
45
  if (source === null) return;
29
46
 
@@ -17,9 +17,23 @@ const LINES = [
17
17
  "File rename. Shows that mv works on files too, not just known entries.",
18
18
  ],
19
19
 
20
+ // --- Archive lifecycle
21
+ [
22
+ "* You may move entries or pattern-matching batches of entries to and from the archive to manage your context budget.",
23
+ "Teaches archival as a reversible budget operation, not permanent deletion.",
24
+ ],
25
+ [
26
+ 'Example: <mv path="known://project/*" fidelity="index"/> ... <mv path="known://project/active_sprint" fidelity="full"/>',
27
+ "Index a whole category to free context while keeping paths visible, restore one entry when needed. No destination = fidelity change in place.",
28
+ ],
29
+ [
30
+ "* YOU SHOULD demote irrelevant entries to `index` or `archive` — clean context improves reasoning.",
31
+ "Core curation principle: clean context is a quality signal, not just a budget concern. Teach the model to curate eagerly.",
32
+ ],
33
+
20
34
  // --- Constraints
21
35
  [
22
- "* Source path accepts globs for batch moves",
36
+ "* Source path accepts patterns for batch moves",
23
37
  "Pattern support consistent with get/cp/rm.",
24
38
  ],
25
39
  [
@@ -1,6 +1,6 @@
1
- # current
1
+ # performed
2
2
 
3
- Renders the `<current>` section of the user message — the active loop's
3
+ Renders the `<performed>` section of the user message — the active loop's
4
4
  tool results and lifecycle signals.
5
5
 
6
6
  ## Registration
@@ -11,4 +11,5 @@ tool results and lifecycle signals.
11
11
 
12
12
  Filters turn_context rows where `category === "logging"` and
13
13
  `source_turn >= loopStartTurn`. Renders each entry chronologically
14
- with turn number and status. Empty on the first turn of a loop.
14
+ with turn, status, summary, fidelity, and tokens. Empty on the first
15
+ turn of a loop.