@possumtech/rummy 0.5.0 → 2.0.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 (157) hide show
  1. package/.env.example +42 -5
  2. package/PLUGINS.md +389 -194
  3. package/README.md +25 -8
  4. package/SPEC.md +934 -373
  5. package/bin/demo.js +166 -0
  6. package/bin/rummy.js +9 -3
  7. package/biome/no-fallbacks.grit +50 -0
  8. package/lang/en.json +2 -2
  9. package/migrations/001_initial_schema.sql +88 -37
  10. package/package.json +13 -11
  11. package/scriptify/ask_run.js +77 -0
  12. package/service.js +50 -9
  13. package/src/agent/AgentLoop.js +476 -335
  14. package/src/agent/ContextAssembler.js +4 -4
  15. package/src/agent/Entries.js +676 -0
  16. package/src/agent/ProjectAgent.js +30 -18
  17. package/src/agent/TurnExecutor.js +232 -421
  18. package/src/agent/XmlParser.js +99 -33
  19. package/src/agent/budget.js +56 -0
  20. package/src/agent/errors.js +22 -0
  21. package/src/agent/httpStatus.js +39 -0
  22. package/src/agent/known_checks.sql +8 -4
  23. package/src/agent/known_queries.sql +9 -13
  24. package/src/agent/known_store.sql +280 -125
  25. package/src/agent/materializeContext.js +104 -0
  26. package/src/agent/runs.sql +29 -7
  27. package/src/agent/schemes.sql +14 -3
  28. package/src/agent/tokens.js +6 -0
  29. package/src/agent/turns.sql +9 -9
  30. package/src/hooks/HookRegistry.js +6 -5
  31. package/src/hooks/Hooks.js +44 -3
  32. package/src/hooks/PluginContext.js +29 -21
  33. package/src/{server → hooks}/RpcRegistry.js +2 -1
  34. package/src/hooks/RummyContext.js +139 -35
  35. package/src/hooks/ToolRegistry.js +21 -16
  36. package/src/llm/LlmProvider.js +66 -89
  37. package/src/llm/errors.js +21 -0
  38. package/src/llm/retry.js +63 -0
  39. package/src/plugins/ask_user/README.md +1 -1
  40. package/src/plugins/ask_user/ask_user.js +37 -12
  41. package/src/plugins/ask_user/ask_userDoc.js +2 -25
  42. package/src/plugins/ask_user/ask_userDoc.md +10 -0
  43. package/src/plugins/budget/README.md +27 -25
  44. package/src/plugins/budget/budget.js +306 -88
  45. package/src/plugins/cp/README.md +2 -2
  46. package/src/plugins/cp/cp.js +29 -11
  47. package/src/plugins/cp/cpDoc.js +2 -15
  48. package/src/plugins/cp/cpDoc.md +7 -0
  49. package/src/plugins/engine/README.md +2 -2
  50. package/src/plugins/engine/engine.sql +4 -4
  51. package/src/plugins/engine/turn_context.sql +10 -10
  52. package/src/plugins/env/README.md +20 -5
  53. package/src/plugins/env/env.js +45 -6
  54. package/src/plugins/env/envDoc.js +2 -23
  55. package/src/plugins/env/envDoc.md +13 -0
  56. package/src/plugins/error/README.md +16 -0
  57. package/src/plugins/error/error.js +151 -0
  58. package/src/plugins/file/README.md +6 -6
  59. package/src/plugins/file/file.js +15 -2
  60. package/src/plugins/get/README.md +1 -1
  61. package/src/plugins/get/get.js +103 -48
  62. package/src/plugins/get/getDoc.js +2 -32
  63. package/src/plugins/get/getDoc.md +36 -0
  64. package/src/plugins/hedberg/README.md +1 -2
  65. package/src/plugins/hedberg/hedberg.js +8 -4
  66. package/src/plugins/hedberg/matcher.js +16 -17
  67. package/src/plugins/hedberg/normalize.js +0 -48
  68. package/src/plugins/helpers.js +42 -2
  69. package/src/plugins/index.js +146 -123
  70. package/src/plugins/instructions/README.md +35 -9
  71. package/src/plugins/instructions/instructions.js +244 -9
  72. package/src/plugins/instructions/instructions.md +33 -0
  73. package/src/plugins/instructions/instructions_104.md +7 -0
  74. package/src/plugins/instructions/instructions_105.md +38 -0
  75. package/src/plugins/instructions/instructions_106.md +21 -0
  76. package/src/plugins/instructions/instructions_107.md +10 -0
  77. package/src/plugins/instructions/instructions_108.md +0 -0
  78. package/src/plugins/instructions/protocol.js +12 -0
  79. package/src/plugins/known/README.md +2 -2
  80. package/src/plugins/known/known.js +68 -36
  81. package/src/plugins/known/knownDoc.js +2 -17
  82. package/src/plugins/known/knownDoc.md +8 -0
  83. package/src/plugins/log/README.md +48 -0
  84. package/src/plugins/log/log.js +129 -0
  85. package/src/plugins/mv/README.md +2 -2
  86. package/src/plugins/mv/mv.js +55 -22
  87. package/src/plugins/mv/mvDoc.js +2 -18
  88. package/src/plugins/mv/mvDoc.md +10 -0
  89. package/src/plugins/ollama/README.md +15 -0
  90. package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
  91. package/src/plugins/openai/README.md +17 -0
  92. package/src/plugins/openai/openai.js +120 -0
  93. package/src/plugins/openrouter/README.md +27 -0
  94. package/src/plugins/openrouter/openrouter.js +121 -0
  95. package/src/plugins/persona/README.md +20 -0
  96. package/src/plugins/persona/persona.js +9 -16
  97. package/src/plugins/policy/README.md +21 -0
  98. package/src/plugins/policy/policy.js +29 -14
  99. package/src/plugins/prompt/README.md +1 -1
  100. package/src/plugins/prompt/prompt.js +64 -16
  101. package/src/plugins/rm/README.md +1 -1
  102. package/src/plugins/rm/rm.js +56 -12
  103. package/src/plugins/rm/rmDoc.js +2 -20
  104. package/src/plugins/rm/rmDoc.md +13 -0
  105. package/src/plugins/rpc/README.md +2 -2
  106. package/src/plugins/rpc/rpc.js +525 -296
  107. package/src/plugins/set/README.md +1 -1
  108. package/src/plugins/set/set.js +318 -75
  109. package/src/plugins/set/setDoc.js +2 -35
  110. package/src/plugins/set/setDoc.md +22 -0
  111. package/src/plugins/sh/README.md +28 -5
  112. package/src/plugins/sh/sh.js +50 -6
  113. package/src/plugins/sh/shDoc.js +2 -23
  114. package/src/plugins/sh/shDoc.md +13 -0
  115. package/src/plugins/skill/README.md +23 -0
  116. package/src/plugins/skill/skill.js +14 -18
  117. package/src/plugins/stream/README.md +101 -0
  118. package/src/plugins/stream/stream.js +290 -0
  119. package/src/plugins/telemetry/README.md +1 -1
  120. package/src/plugins/telemetry/telemetry.js +129 -80
  121. package/src/plugins/think/README.md +1 -1
  122. package/src/plugins/think/think.js +12 -0
  123. package/src/plugins/think/thinkDoc.js +2 -15
  124. package/src/plugins/think/thinkDoc.md +7 -0
  125. package/src/plugins/unknown/README.md +3 -3
  126. package/src/plugins/unknown/unknown.js +47 -19
  127. package/src/plugins/unknown/unknownDoc.js +2 -21
  128. package/src/plugins/unknown/unknownDoc.md +11 -0
  129. package/src/plugins/update/README.md +1 -1
  130. package/src/plugins/update/update.js +83 -5
  131. package/src/plugins/update/updateDoc.js +2 -30
  132. package/src/plugins/update/updateDoc.md +8 -0
  133. package/src/plugins/xai/README.md +23 -0
  134. package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
  135. package/src/plugins/yolo/yolo.js +192 -0
  136. package/src/server/ClientConnection.js +64 -37
  137. package/src/server/SocketServer.js +23 -10
  138. package/src/server/protocol.js +11 -0
  139. package/src/sql/v_model_context.sql +27 -31
  140. package/src/sql/v_run_log.sql +9 -14
  141. package/EXCEPTIONS.md +0 -46
  142. package/FIDELITY_CONTRACT.md +0 -172
  143. package/src/agent/KnownStore.js +0 -337
  144. package/src/agent/ResponseHealer.js +0 -241
  145. package/src/llm/OpenAiClient.js +0 -100
  146. package/src/llm/OpenRouterClient.js +0 -100
  147. package/src/plugins/budget/recovery.js +0 -47
  148. package/src/plugins/instructions/preamble.md +0 -45
  149. package/src/plugins/performed/README.md +0 -15
  150. package/src/plugins/performed/performed.js +0 -45
  151. package/src/plugins/previous/README.md +0 -16
  152. package/src/plugins/previous/previous.js +0 -56
  153. package/src/plugins/progress/README.md +0 -16
  154. package/src/plugins/progress/progress.js +0 -43
  155. package/src/plugins/summarize/README.md +0 -19
  156. package/src/plugins/summarize/summarize.js +0 -32
  157. package/src/plugins/summarize/summarizeDoc.js +0 -27
@@ -1,10 +1,44 @@
1
+ import { ceiling, computeBudget, measureMessages } from "../../agent/budget.js";
2
+ import materializeContext from "../../agent/materializeContext.js";
1
3
  import { countTokens } from "../../agent/tokens.js";
2
4
 
3
- const CEILING_RATIO = Number(process.env.RUMMY_BUDGET_CEILING);
4
- if (!CEILING_RATIO) throw new Error("RUMMY_BUDGET_CEILING must be set");
5
+ /**
6
+ * Delta-from-actual baseline. The pre-call <prompt tokenUsage> reports
7
+ * the prior turn's actual API prompt_tokens; post-dispatch predicts
8
+ * next turn's packet = this turn's actual tokens + tokens of new rows
9
+ * written this turn. Keeps the 413 body on the same scale as the
10
+ * model's <prompt> arithmetic — a 60% divergence between pre-call
11
+ * (actual) and post-check (conservative estimator) makes the model
12
+ * dismiss the system as janky and stop following rules.
13
+ */
14
+ function predictNextPacket(rows, currentTurn, baseline) {
15
+ let delta = 0;
16
+ for (const r of rows) {
17
+ if (r.source_turn === currentTurn) delta += countTokens(r.body);
18
+ }
19
+ return baseline + delta;
20
+ }
5
21
 
6
- function measureMessages(messages) {
7
- return messages.reduce((sum, m) => sum + countTokens(m.content), 0);
22
+ /**
23
+ * Format the 413 error body. Names each demoted path with its turn
24
+ * and token count so the model can avoid re-promoting them next turn.
25
+ * Exported (not private) so unit tests can assert the exact wire
26
+ * format — the model reads this string, so its shape is part of the
27
+ * contract.
28
+ */
29
+ export function overflowBody(overflow, contextSize, demoted) {
30
+ const cap = ceiling(contextSize);
31
+ const size = cap + overflow;
32
+ const count = demoted.length;
33
+ const totalTokens = demoted.reduce((s, r) => s + r.tokens, 0);
34
+ const head = `Token Budget overflow: packet was ${size} tokens, ceiling is ${cap}. ${count} promotion${count === 1 ? "" : "s"} (${totalTokens} tokens) demoted to fit.`;
35
+ if (count === 0) return head;
36
+ const lines = demoted.map((d) =>
37
+ d.turn
38
+ ? `- ${d.path} (turn ${d.turn}, ${d.tokens} tokens)`
39
+ : `- ${d.path} (${d.tokens} tokens)`,
40
+ );
41
+ return `${head}\nDemoted:\n${lines.join("\n")}`;
8
42
  }
9
43
 
10
44
  export default class Budget {
@@ -12,121 +46,305 @@ export default class Budget {
12
46
 
13
47
  constructor(core) {
14
48
  this.#core = core;
15
- core.registerScheme({
16
- name: "budget",
17
- modelVisible: 1,
18
- category: "logging",
19
- });
20
- core.hooks.tools.onView("budget", (entry) => entry.body);
21
49
  core.hooks.budget = {
22
50
  enforce: this.enforce.bind(this),
23
51
  postDispatch: this.postDispatch.bind(this),
24
52
  };
53
+ core.filter("assembly.user", this.assembleBudget.bind(this), 275);
25
54
  }
26
55
 
27
- async enforce({ contextSize, messages, rows, lastPromptTokens = 0 }) {
28
- if (!contextSize) {
29
- return { messages, rows, demoted: [], assembledTokens: 0, status: 200 };
30
- }
56
+ /**
57
+ * Render the <budget> table between <instructions> and <prompt>.
58
+ * See SPEC @token_accounting for the contract: per-row tokens are
59
+ * aTokens (the promotion premium = vTokens − sTokens), summarized
60
+ * entries collapse into a single aggregate line, system overhead
61
+ * (system prompt + tool defs) gets its own line.
62
+ */
63
+ assembleBudget(content, ctx) {
64
+ const { rows, contextSize, systemPrompt } = ctx;
65
+ if (!contextSize) return content;
31
66
 
32
- const assembledTokens =
33
- lastPromptTokens > 0 ? lastPromptTokens : measureMessages(messages);
67
+ const cap = ceiling(contextSize);
34
68
 
35
- console.warn(
36
- `[RUMMY] Budget enforce: ${assembledTokens} tokens (${lastPromptTokens > 0 ? "actual" : "estimated"}), ceiling ${contextSize}, ${rows.length} rows`,
37
- );
69
+ // Per-scheme aggregation: counts and costs at each visibility tier
70
+ // plus the savings (premium) the model would unlock by demoting
71
+ // visible → summarized. All math derives from per-row vTokens
72
+ // (cost as visible) / sTokens (cost as summarized) / aTokens
73
+ // (= vTokens − sTokens, the promotion premium).
74
+ const byScheme = new Map();
75
+ let visibleCount = 0;
76
+ let premiumTokens = 0;
77
+ let summarizedCount = 0;
78
+ let _summarizedTokens = 0;
79
+ let floorTokens = 0;
80
+ let knownVTokens = 0;
81
+ let sourceVTokens = 0;
82
+
83
+ const schemeEntry = (s) => {
84
+ let e = byScheme.get(s);
85
+ if (!e) {
86
+ e = {
87
+ vis: 0,
88
+ sum: 0,
89
+ visTokens: 0, // current cost of visible entries
90
+ visIfSumTokens: 0, // sTokens of visible (what they'd cost demoted)
91
+ sumTokens: 0, // current cost of summarized entries
92
+ premium: 0, // savings from demoting visible → summarized
93
+ };
94
+ byScheme.set(s, e);
95
+ }
96
+ return e;
97
+ };
38
98
 
39
- const ceiling = Math.floor(contextSize * CEILING_RATIO);
40
- if (assembledTokens > ceiling) {
41
- const overflow = assembledTokens - ceiling;
42
- console.warn(
43
- `[RUMMY] Budget 413: ${assembledTokens} tokens > ${contextSize} ceiling (${overflow} over)`,
44
- );
45
- return {
46
- messages,
47
- rows,
48
- demoted: [],
49
- assembledTokens,
50
- status: 413,
51
- overflow,
52
- };
99
+ for (const r of rows) {
100
+ if (r.aTokens == null) continue;
101
+ const s = r.scheme || "file";
102
+ const entry = schemeEntry(s);
103
+ if (r.visibility === "visible") {
104
+ entry.vis += 1;
105
+ entry.visTokens += r.vTokens || 0;
106
+ entry.visIfSumTokens += r.sTokens || 0;
107
+ entry.premium += r.aTokens || 0;
108
+ visibleCount += 1;
109
+ premiumTokens += r.aTokens;
110
+ floorTokens += r.sTokens;
111
+ const v = r.vTokens || 0;
112
+ if (s === "known") knownVTokens += v;
113
+ else if (s === "prompt") sourceVTokens += v;
114
+ else if (r.category === "data") sourceVTokens += v;
115
+ } else if (r.visibility === "summarized") {
116
+ entry.sum += 1;
117
+ entry.sumTokens += r.sTokens || 0;
118
+ summarizedCount += 1;
119
+ _summarizedTokens += r.sTokens;
120
+ floorTokens += r.sTokens;
121
+ }
53
122
  }
54
123
 
55
- return { messages, rows, demoted: [], assembledTokens, status: 200 };
124
+ const fcrmDenom = knownVTokens + sourceVTokens;
125
+ const fcrmScore =
126
+ fcrmDenom > 0 ? (knownVTokens / fcrmDenom).toFixed(2) : "1.00";
127
+
128
+ const systemTokens = countTokens(systemPrompt || "");
129
+ const tokenUsage = floorTokens + premiumTokens + systemTokens;
130
+ const tokensFree = Math.max(0, cap - tokenUsage);
131
+
132
+ // Sort schemes by current cost descending — biggest-impact rows
133
+ // land at the top, so "what should I demote first?" reads
134
+ // straight off the table.
135
+ const schemeRows = [...byScheme.entries()]
136
+ .toSorted(
137
+ ([, a], [, b]) =>
138
+ b.visTokens + b.sumTokens - (a.visTokens + a.sumTokens),
139
+ )
140
+ .map(([scheme, e]) => {
141
+ const cost = e.visTokens + e.sumTokens;
142
+ const ifAllSum = e.visIfSumTokens + e.sumTokens;
143
+ return `| ${scheme} | ${e.vis} | ${e.sum} | ${cost} | ${ifAllSum} | ${e.premium} |`;
144
+ });
145
+
146
+ const systemPct =
147
+ tokenUsage > 0 ? Math.round((systemTokens / tokenUsage) * 100) : 0;
148
+
149
+ const table = [
150
+ "| scheme | vis | sum | cost | if-all-sum | premium |",
151
+ "|---|---|---|---|---|---|",
152
+ ...schemeRows,
153
+ ].join("\n");
154
+
155
+ const systemLine = `System: ${systemTokens} tokens (${systemPct}% of budget).`;
156
+ const totalLine = `Total: ${visibleCount} visible + ${summarizedCount} summarized entries; tokenUsage ${tokenUsage} / ceiling ${cap}. ${tokensFree} tokens free.`;
157
+ const legend = [
158
+ "Columns:",
159
+ "- cost: current cost of this scheme (vTokens for visible + sTokens for summarized)",
160
+ "- if-all-sum: cost if every entry of this scheme were demoted to summarized",
161
+ "- premium: savings from demoting visible → summarized (cost − if-all-sum)",
162
+ ].join("\n");
163
+
164
+ return `${content}<budget tokenUsage="${tokenUsage}" tokensFree="${tokensFree}" fcrmScore="${fcrmScore}">\n${table}\n\n${legend}\n${systemLine}\n${totalLine}\n</budget>\n`;
165
+ }
166
+
167
+ #check({ contextSize, messages, rows, lastPromptTokens = 0 }) {
168
+ const totalTokens =
169
+ lastPromptTokens > 0 ? lastPromptTokens : measureMessages(messages);
170
+ const b = computeBudget({ rows, contextSize, totalTokens });
171
+ return {
172
+ messages,
173
+ rows,
174
+ assembledTokens: b.totalTokens,
175
+ overflow: b.overflow,
176
+ ok: b.ok,
177
+ };
178
+ }
179
+
180
+ async #emitOverflow({
181
+ message,
182
+ runId,
183
+ turn,
184
+ loopId,
185
+ rummy,
186
+ demotedCount = 0,
187
+ demotedTokens = 0,
188
+ }) {
189
+ await rummy.hooks.error.log.emit({
190
+ store: rummy.entries,
191
+ runId,
192
+ turn,
193
+ loopId,
194
+ message,
195
+ status: 413,
196
+ attributes: { demotedCount, demotedTokens },
197
+ });
56
198
  }
57
199
 
58
- async postDispatch({
200
+ /**
201
+ * Pre-LLM budget enforcement. On first-turn overflow, demotes the
202
+ * incoming prompt and re-materializes; re-checks and returns the
203
+ * post-demotion result. If overflow persists after demotion (or on
204
+ * later iterations), emits a 413 error (strike) and returns !ok so
205
+ * TurnExecutor can skip the LLM call this turn.
206
+ *
207
+ * ctx = { runId, loopId, turn, systemPrompt, mode, toolSet, demoted,
208
+ * loopIteration }
209
+ */
210
+ async enforce({
59
211
  contextSize,
60
212
  messages,
61
213
  rows,
62
- runId,
63
- loopId,
64
- turn,
65
- db,
66
- store,
214
+ lastPromptTokens = 0,
215
+ ctx,
216
+ rummy,
67
217
  }) {
68
- if (!contextSize) return null;
218
+ if (!contextSize) {
219
+ return { messages, rows, assembledTokens: 0, ok: true };
220
+ }
69
221
 
70
- const postBudget = await this.enforce({
222
+ const first = this.#check({
71
223
  contextSize,
72
224
  messages,
73
225
  rows,
74
- lastPromptTokens: 0,
226
+ lastPromptTokens,
75
227
  });
228
+ if (first.ok) return first;
76
229
 
77
- if (postBudget.status !== 413) return null;
78
-
79
- // Demote this turn's entries
80
- const demotedEntries = await db.demote_turn_entries.all({
81
- run_id: runId,
82
- turn,
83
- });
230
+ if (ctx?.loopIteration !== 1) {
231
+ const cap = ceiling(contextSize);
232
+ await this.#emitOverflow({
233
+ message: `Token Budget overflow: packet was ${cap + first.overflow} tokens, ceiling is ${cap}.`,
234
+ runId: ctx.runId,
235
+ turn: ctx.turn,
236
+ loopId: ctx.loopId,
237
+ rummy,
238
+ });
239
+ return first;
240
+ }
84
241
 
85
- // Also demote the prompt
86
- const promptRow = rows.find((r) => r.scheme === "prompt");
242
+ const promptRow = rows.findLast(
243
+ (r) => r.category === "prompt" && r.scheme === "prompt",
244
+ );
87
245
  if (promptRow) {
88
- await store.setFidelity(runId, promptRow.path, "demoted");
246
+ await rummy.entries.set({
247
+ runId: ctx.runId,
248
+ path: promptRow.path,
249
+ visibility: "summarized",
250
+ });
251
+ }
252
+ const reMat = await materializeContext({
253
+ db: rummy.db,
254
+ hooks: rummy.hooks,
255
+ runId: ctx.runId,
256
+ loopId: ctx.loopId,
257
+ turn: ctx.turn,
258
+ systemPrompt: ctx.systemPrompt,
259
+ mode: ctx.mode,
260
+ toolSet: ctx.toolSet,
261
+ contextSize,
262
+ demoted: ctx.demoted,
263
+ });
264
+ const rechecked = this.#check({
265
+ contextSize,
266
+ messages: reMat.messages,
267
+ rows: reMat.rows,
268
+ lastPromptTokens: reMat.lastContextTokens,
269
+ });
270
+ if (!rechecked.ok) {
271
+ const cap = ceiling(contextSize);
272
+ await this.#emitOverflow({
273
+ message: `Token Budget overflow: packet was ${cap + rechecked.overflow} tokens after demoting the prompt, ceiling is ${cap}.`,
274
+ runId: ctx.runId,
275
+ turn: ctx.turn,
276
+ loopId: ctx.loopId,
277
+ rummy,
278
+ });
89
279
  }
280
+ return rechecked;
281
+ }
282
+
283
+ /**
284
+ * Post-dispatch Turn Demotion. Re-materializes end-of-turn context and
285
+ * checks against the ceiling. On overflow, demotes this turn's promoted
286
+ * entries and emits a 413 error (strike) with the descriptive body so
287
+ * the model sees it next turn via the unified error channel.
288
+ *
289
+ * ctx = { runId, loopId, turn, systemPrompt, mode, toolSet, demoted }
290
+ */
291
+ async postDispatch({ contextSize, ctx, rummy }) {
292
+ if (!contextSize) return { failed: false };
293
+ const postMat = await materializeContext({
294
+ db: rummy.db,
295
+ hooks: rummy.hooks,
296
+ runId: ctx.runId,
297
+ loopId: ctx.loopId,
298
+ turn: ctx.turn,
299
+ systemPrompt: ctx.systemPrompt,
300
+ mode: ctx.mode,
301
+ toolSet: ctx.toolSet,
302
+ contextSize,
303
+ demoted: ctx.demoted,
304
+ });
305
+ // Baseline from this turn's actual API tokens (telemetry wrote it
306
+ // before post-dispatch runs). Delta from rows added this turn.
307
+ // Predicted next-turn packet stays on the tokenUsage scale the
308
+ // model can verify against its own arithmetic. materializeContext
309
+ // guarantees a number (0 when no prior API call exists).
310
+ const baseline = postMat.lastContextTokens;
311
+ const predicted = predictNextPacket(postMat.rows, ctx.turn, baseline);
312
+ const cap = ceiling(contextSize);
313
+ if (predicted <= cap) return { failed: false };
314
+ const post = { overflow: predicted - cap };
90
315
 
91
- // Rewrite get-result bodies — the get handler claimed "promoted" success
92
- // before this panic ran. Without rewriting, the model reads conflicting
93
- // signals next turn (status=413 but body says "promoted").
94
- for (const entry of demotedEntries) {
95
- if (!entry.path.startsWith("get://")) continue;
96
- await db.resolve_known_entry.run({
97
- run_id: runId,
98
- path: entry.path,
99
- body: `Demoted by budget. See budget://${loopId}/${turn}.`,
100
- status: 413,
316
+ const store = rummy.entries;
317
+ let demotedEntries = await store.demoteTurnEntries(ctx.runId, ctx.turn);
318
+ // Fallback: if this turn had nothing to demote but the packet still
319
+ // overflows, the pressure is coming from prior-turn promotions the
320
+ // model never demoted itself. Widen to all currently-visible
321
+ // entries in the run. Without this fallback, overflow-with-nothing
322
+ // strikes out runs where the base context has drifted over ceiling
323
+ // through no fault of the current turn (observed: runs where 3
324
+ // stale promotions from turns 12–14 saturate every subsequent
325
+ // turn's budget).
326
+ if (demotedEntries.length === 0) {
327
+ demotedEntries = await store.demoteRunVisibleEntries(ctx.runId);
328
+ }
329
+ const promptRow = postMat.rows.find((r) => r.scheme === "prompt");
330
+ if (promptRow) {
331
+ await store.set({
332
+ runId: ctx.runId,
333
+ path: promptRow.path,
334
+ visibility: "summarized",
101
335
  });
102
336
  }
103
337
 
104
- // Write budget entry — terse, actionable. Path list dropped since
105
- // demoted entries already render at fidelity="demoted" in <knowns>/<files>.
106
- // "tokens remaining" dropped too — the number was over-optimistic (it
107
- // treated re-demoted files as freeing their full-body tokens when their
108
- // demoted-view renderings return to baseline). Model reads the truthful
109
- // remaining in next turn's progress line.
110
- //
111
- // The 50% rule is the key directive: it forces the model to sum
112
- // promotion costs (which is the behavior we want), and the threshold
113
- // gives a concrete ceiling for the next try. Twofer — abiding by the
114
- // rule requires budget awareness as a side effect.
115
- const ceiling = Math.floor(contextSize * CEILING_RATIO);
116
338
  const totalDemoted = demotedEntries.reduce((s, r) => s + r.tokens, 0);
117
- const body = [
118
- `413 Token Budget Error: overflowed by ${postBudget.overflow} tokens. Token Budget: ${ceiling}.`,
119
- `Your ${demotedEntries.length} promotions from last turn (${totalDemoted} tokens total) were demoted to fit.`,
120
- `Required: sum the tokens="N" of your promotions and new entries before emitting. A single turn must add no more than 50% of remaining Token Budget.`,
121
- ].join("\n");
122
-
123
- await store.upsert(runId, turn, `budget://${loopId}/${turn}`, body, 413, {
124
- loopId,
339
+ await this.#emitOverflow({
340
+ message: overflowBody(post.overflow, contextSize, demotedEntries),
341
+ demotedCount: demotedEntries.length,
342
+ demotedTokens: totalDemoted,
343
+ runId: ctx.runId,
344
+ turn: ctx.turn,
345
+ loopId: ctx.loopId,
346
+ rummy,
125
347
  });
126
-
127
- return {
128
- target: ceiling,
129
- promptPath: promptRow?.path ?? null,
130
- };
348
+ return { failed: true };
131
349
  }
132
350
  }
@@ -1,4 +1,4 @@
1
- # cp
1
+ # cp {#cp_plugin}
2
2
 
3
3
  Copies an entry from one path to another within the K/V store.
4
4
 
@@ -15,5 +15,5 @@ Shows `cp {from} {to}`.
15
15
  ## Behavior
16
16
 
17
17
  Warns if the destination already exists and will be overwritten. Uses
18
- `KnownStore.scheme()` to determine whether the destination is a scheme
18
+ `Entries.scheme()` to determine whether the destination is a scheme
19
19
  path or a file path.
@@ -1,4 +1,4 @@
1
- import KnownStore from "../../agent/KnownStore.js";
1
+ import Entries from "../../agent/Entries.js";
2
2
  import docs from "./cpDoc.js";
3
3
 
4
4
  export default class Cp {
@@ -8,8 +8,8 @@ export default class Cp {
8
8
  this.#core = core;
9
9
  core.registerScheme();
10
10
  core.on("handler", this.handler.bind(this));
11
- core.on("promoted", this.full.bind(this));
12
- core.on("demoted", this.summary.bind(this));
11
+ core.on("visible", this.full.bind(this));
12
+ core.on("summarized", this.summary.bind(this));
13
13
  core.filter("instructions.toolDocs", async (docsMap) => {
14
14
  docsMap.cp = docs;
15
15
  return docsMap;
@@ -19,15 +19,15 @@ 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 = { promoted: 1, demoted: 1, archived: 1 };
23
- const fidelity = VALID[entry.attributes.fidelity]
24
- ? entry.attributes.fidelity
22
+ const VALID = { visible: 1, summarized: 1, archived: 1 };
23
+ const visibility = VALID[entry.attributes.visibility]
24
+ ? entry.attributes.visibility
25
25
  : undefined;
26
26
 
27
27
  const source = await store.getBody(runId, path);
28
28
  if (source === null) return;
29
29
 
30
- const destScheme = KnownStore.scheme(to);
30
+ const destScheme = Entries.scheme(to);
31
31
  const existing = await store.getBody(runId, to);
32
32
  const warning =
33
33
  existing !== null && destScheme !== null
@@ -36,13 +36,31 @@ export default class Cp {
36
36
 
37
37
  const body = `${path} ${to}`;
38
38
  if (destScheme === null) {
39
- await store.upsert(runId, turn, entry.resultPath, body, 202, {
39
+ await store.set({
40
+ runId,
41
+ turn,
42
+ path: entry.resultPath,
43
+ body,
44
+ state: "proposed",
40
45
  attributes: { from: path, to, isMove: false, warning },
41
46
  loopId,
42
47
  });
43
48
  } else {
44
- await store.upsert(runId, turn, to, source, 200, { fidelity, loopId });
45
- await store.upsert(runId, turn, entry.resultPath, body, 200, {
49
+ await store.set({
50
+ runId,
51
+ turn,
52
+ path: to,
53
+ body: source,
54
+ state: "resolved",
55
+ visibility,
56
+ loopId,
57
+ });
58
+ await store.set({
59
+ runId,
60
+ turn,
61
+ path: entry.resultPath,
62
+ body,
63
+ state: "resolved",
46
64
  attributes: { from: path, to, isMove: false, warning },
47
65
  loopId,
48
66
  });
@@ -50,7 +68,7 @@ export default class Cp {
50
68
  }
51
69
 
52
70
  full(entry) {
53
- return `# cp ${entry.attributes.from || ""} ${entry.attributes.to || ""}`;
71
+ return `# cp ${entry.attributes.from} ${entry.attributes.to}`;
54
72
  }
55
73
 
56
74
  summary() {
@@ -1,16 +1,3 @@
1
- // Tool doc for <cp>. 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
- ['## <cp path="[source]">[destination]</cp> - Copy a file or entry'],
6
- [
7
- 'Example: <cp path="src/config.js">src/config.backup.js</cp>',
8
- "Simple file copy. Path = source, body = destination.",
9
- ],
10
- [
11
- 'Example: <cp path="known://plan_*">known://archive_</cp>',
12
- "Glob batch copy across known entries.",
13
- ],
14
- ];
1
+ import { loadDoc } from "../helpers.js";
15
2
 
16
- export default LINES.map(([text]) => text).join("\n");
3
+ export default loadDoc(import.meta.url, "cpDoc.md");
@@ -0,0 +1,7 @@
1
+ ## <cp path="[source]">[destination]</cp> - Copy a file or entry
2
+
3
+ Example: <cp path="src/config.js">src/config.backup.js</cp>
4
+ <!-- Simple file copy. Path = source, body = destination. -->
5
+
6
+ Example: <cp path="known://plan_*">known://archive_</cp>
7
+ <!-- Glob batch copy across known entries. -->
@@ -1,10 +1,10 @@
1
- # engine
1
+ # engine {#engine_plugin}
2
2
 
3
3
  SQL infrastructure for context assembly and turn management. No JS plugin.
4
4
 
5
5
  ## Files
6
6
 
7
- - **engine.sql** — Queries for retrieving promoted entries by scheme tier, model visibility, and state.
7
+ - **engine.sql** — Queries for retrieving visible entries by scheme tier, model visibility, and state.
8
8
  - **turn_context.sql** — Queries for clearing and reading the `turn_context` / `v_model_context` view, which produces the ordered context sent to the model.
9
9
 
10
10
  ## Behavior
@@ -1,13 +1,13 @@
1
1
  -- PREP: get_promoted_entries
2
2
  SELECT
3
- ke.path, ke.scheme, ke.status, ke.fidelity, ke.turn
4
- , ke.tokens, ke.refs
3
+ ke.path, ke.scheme, ke.state, ke.outcome, ke.visibility, ke.turn
4
+ , countTokens(ke.body) AS tokens, ke.refs
5
5
  FROM known_entries AS ke
6
6
  JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
7
7
  WHERE
8
8
  ke.run_id = :run_id
9
- AND ke.fidelity IN ('promoted', 'demoted')
9
+ AND ke.visibility IN ('visible', 'summarized')
10
10
  AND s.model_visible = 1
11
- ORDER BY ke.turn, ke.refs, ke.tokens DESC;
11
+ ORDER BY ke.turn, ke.refs, countTokens(ke.body) DESC;
12
12
 
13
13
 
@@ -4,33 +4,33 @@ WHERE run_id = :run_id AND turn = :turn;
4
4
 
5
5
  -- PREP: get_model_context
6
6
  SELECT
7
- ordinal, path, scheme, fidelity, status, body
8
- , tokens, attributes, category, turn
7
+ ordinal, path, scheme, visibility, state, outcome, body
8
+ , attributes, category, turn
9
9
  FROM v_model_context
10
10
  WHERE run_id = :run_id
11
11
  ORDER BY ordinal;
12
12
 
13
13
  -- PREP: insert_turn_context
14
14
  INSERT INTO turn_context (
15
- run_id, loop_id, turn, ordinal, path, fidelity, status
16
- , body, tokens, attributes, category, source_turn
15
+ run_id, loop_id, turn, ordinal, path, visibility, state, outcome
16
+ , body, attributes, category, source_turn
17
17
  )
18
18
  VALUES (
19
- :run_id, :loop_id, :turn, :ordinal, :path, :fidelity
20
- , :status, :body, :tokens
19
+ :run_id, :loop_id, :turn, :ordinal, :path, :visibility
20
+ , :state, :outcome, :body
21
21
  , COALESCE(:attributes, '{}'), :category, :source_turn
22
22
  );
23
23
 
24
24
  -- PREP: get_turn_context
25
25
  SELECT
26
- ordinal, path, scheme, fidelity, status, body
27
- , tokens, attributes, category, source_turn
26
+ ordinal, path, scheme, visibility, state, outcome, body
27
+ , attributes, category, source_turn
28
28
  FROM turn_context
29
29
  WHERE run_id = :run_id AND turn = :turn
30
30
  ORDER BY ordinal;
31
31
 
32
32
  -- PREP: get_turn_budget
33
- SELECT COALESCE(SUM(tokens), 0) AS total
33
+ SELECT COALESCE(SUM(countTokens(body)), 0) AS total
34
34
  FROM turn_context
35
35
  WHERE run_id = :run_id AND turn = :turn;
36
36
 
@@ -43,7 +43,7 @@ SELECT
43
43
  WHEN 'prompt' THEN 'prompt'
44
44
  ELSE 'system'
45
45
  END AS bucket,
46
- COALESCE(SUM(tokens), 0) AS tokens,
46
+ COALESCE(SUM(countTokens(body)), 0) AS tokens,
47
47
  COUNT(*) AS entries
48
48
  FROM turn_context
49
49
  WHERE run_id = :run_id AND turn = :turn