@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
@@ -121,6 +121,7 @@ export default class AgentLoop {
121
121
  const noRepo = options?.noRepo === true;
122
122
  const noInteraction = options?.noInteraction === true;
123
123
  const noWeb = options?.noWeb === true;
124
+ const noProposals = options?.noProposals === true;
124
125
  const requestedModel = model;
125
126
 
126
127
  const runInfo = await this.#ensureRun(projectId, model, run, options);
@@ -146,6 +147,7 @@ export default class AgentLoop {
146
147
  noRepo,
147
148
  noInteraction,
148
149
  noWeb,
150
+ noProposals,
149
151
  temperature: options?.temperature,
150
152
  }),
151
153
  });
@@ -164,100 +166,76 @@ export default class AgentLoop {
164
166
  }
165
167
 
166
168
  async #drainQueue(currentRunId, currentAlias, projectId, project, options) {
167
- let panicAttempted = false;
168
-
169
- while (true) {
170
- const loop = await this.#db.claim_next_loop.get({
171
- run_id: currentRunId,
172
- });
173
- if (!loop) break;
174
-
175
- const loopConfig = loop.config ? JSON.parse(loop.config) : {};
176
- const hook =
177
- loop.mode === "panic"
178
- ? this.#hooks.panic
179
- : loop.mode === "ask"
180
- ? this.#hooks.ask
181
- : this.#hooks.act;
182
-
183
- const result = await this.#executeLoop({
184
- mode: loop.mode,
185
- project,
186
- projectId,
187
- currentRunId,
188
- currentAlias,
189
- currentLoopId: loop.id,
190
- requestedModel: loop.model,
191
- prompt: loop.prompt,
192
- noRepo: loopConfig.noRepo || false,
193
- noInteraction: loopConfig.noInteraction || false,
194
- noWeb: loopConfig.noWeb || false,
195
- options: { ...options, temperature: loopConfig.temperature },
196
- hook,
197
- });
169
+ const controller = new AbortController();
170
+ this.#activeRuns.set(currentRunId, controller);
198
171
 
199
- if (result.status === 413) {
200
- await this.#db.complete_loop.run({
201
- id: loop.id,
202
- status: 413,
203
- result: JSON.stringify(result),
172
+ try {
173
+ while (true) {
174
+ const loop = await this.#db.claim_next_loop.get({
175
+ run_id: currentRunId,
204
176
  });
177
+ if (!loop) break;
178
+
179
+ const loopConfig = loop.config ? JSON.parse(loop.config) : {};
180
+ const hook = loop.mode === "ask" ? this.#hooks.ask : this.#hooks.act;
181
+
182
+ let result;
183
+ try {
184
+ result = await this.#executeLoop({
185
+ mode: loop.mode,
186
+ project,
187
+ projectId,
188
+ currentRunId,
189
+ currentAlias,
190
+ currentLoopId: loop.id,
191
+ requestedModel: loop.model,
192
+ prompt: loop.prompt,
193
+ noRepo: loopConfig.noRepo || false,
194
+ noInteraction: loopConfig.noInteraction || false,
195
+ noWeb: loopConfig.noWeb || false,
196
+ noProposals: loopConfig.noProposals || false,
197
+ options: { ...options, temperature: loopConfig.temperature },
198
+ hook,
199
+ signal: controller.signal,
200
+ });
201
+ } catch (err) {
202
+ await this.#db.complete_loop.run({
203
+ id: loop.id,
204
+ status: 500,
205
+ result: JSON.stringify({ error: err.message }),
206
+ });
207
+ throw err;
208
+ }
205
209
 
206
- // One panic attempt per drain cycle
207
- if (loop.mode === "panic" || panicAttempted) {
210
+ if (result.status === 413) {
211
+ await this.#db.complete_loop.run({
212
+ id: loop.id,
213
+ status: 413,
214
+ result: JSON.stringify(result),
215
+ });
208
216
  return {
209
217
  run: currentAlias,
210
218
  status: 413,
211
- error:
212
- loop.mode === "panic"
213
- ? `Panic mode failed to free enough space (${result.overflow} tokens over).`
214
- : `Context full (${result.overflow} tokens over).`,
219
+ error: `Context full (${result.overflow} tokens over).`,
215
220
  };
216
221
  }
217
222
 
218
- panicAttempted = true;
219
-
220
- const panicPrompt = this.#hooks.budget.panicPrompt({
221
- assembledTokens: result.assembledTokens,
222
- contextSize: result.contextSize,
223
- });
224
-
225
- // Enqueue panic loop
226
- const panicSeq = await this.#db.next_loop.get({ run_id: currentRunId });
227
- await this.#db.enqueue_loop.get({
228
- run_id: currentRunId,
229
- sequence: panicSeq.sequence,
230
- mode: "panic",
231
- model: loop.model,
232
- prompt: panicPrompt,
233
- config: JSON.stringify({ noRepo: true }),
234
- });
235
-
236
- // Re-enqueue the original loop to retry after panic
237
- const retrySeq = await this.#db.next_loop.get({ run_id: currentRunId });
238
- await this.#db.enqueue_loop.get({
239
- run_id: currentRunId,
240
- sequence: retrySeq.sequence,
241
- mode: loop.mode,
242
- model: loop.model,
243
- prompt: loop.prompt,
244
- config: loop.config,
223
+ await this.#db.complete_loop.run({
224
+ id: loop.id,
225
+ status: result.status === 202 ? 202 : result.status,
226
+ result: JSON.stringify(result),
245
227
  });
246
228
 
247
- continue;
229
+ if (result.status === 202) return result;
248
230
  }
249
231
 
250
- await this.#db.complete_loop.run({
251
- id: loop.id,
252
- status: result.status === 202 ? 202 : result.status,
253
- result: JSON.stringify(result),
232
+ const runRow = await this.#db.get_run_by_alias.get({
233
+ alias: currentAlias,
254
234
  });
255
-
256
- if (result.status === 202) return result;
235
+ return { run: currentAlias, status: runRow?.status ?? 200 };
236
+ } finally {
237
+ this.#activeRuns.delete(currentRunId);
257
238
  }
258
-
259
- const runRow = await this.#db.get_run_by_alias.get({ alias: currentAlias });
260
- return { run: currentAlias, status: runRow?.status ?? 200 };
261
239
  }
262
240
 
263
241
  async #executeLoop({
@@ -272,8 +250,10 @@ export default class AgentLoop {
272
250
  noRepo,
273
251
  noInteraction,
274
252
  noWeb,
253
+ noProposals,
275
254
  options,
276
255
  hook,
256
+ signal,
277
257
  }) {
278
258
  const runRow = await this.#db.get_run_by_id.get({ id: currentRunId });
279
259
  if (runRow.status !== 102) {
@@ -292,18 +272,27 @@ export default class AgentLoop {
292
272
  const toolSet = this.#hooks.tools.resolveForLoop(mode, {
293
273
  noInteraction,
294
274
  noWeb,
275
+ noProposals,
295
276
  });
296
277
 
297
278
  let loopIteration = 0;
298
279
  const MAX_LOOP_ITERATIONS = Number(process.env.RUMMY_MAX_TURNS) || 15;
299
280
  const healer = new ResponseHealer();
300
281
 
301
- const controller = new AbortController();
302
- this.#activeRuns.set(currentRunId, controller);
303
-
304
282
  let _lastAssembledTokens = 0;
305
- let _panicStrikes = 0;
306
- let _lastPanicTokens = null;
283
+ let recovery = null; // { target, promptPath, strikes, lastTokens }
284
+
285
+ // Demote full logging entries from previous loops to summary before
286
+ // they appear in <previous>. General policy: keep <previous> compact.
287
+ await this.#knownStore.demotePreviousLoopLogging(
288
+ currentRunId,
289
+ currentLoopId,
290
+ );
291
+
292
+ // Restore any prompt entries left at summary fidelity by a recovery
293
+ // phase that was interrupted (server crash, restart). If the full
294
+ // prompt would overflow, Prompt Demotion on turn 1 handles it.
295
+ await this.#knownStore.restoreSummarizedPrompts(currentRunId);
307
296
 
308
297
  await this.#hooks.loop.started.emit({
309
298
  runId: currentRunId,
@@ -314,7 +303,7 @@ export default class AgentLoop {
314
303
 
315
304
  try {
316
305
  while (loopIteration < MAX_LOOP_ITERATIONS) {
317
- if (controller.signal.aborted) {
306
+ if (signal.aborted) {
318
307
  await this.#db.update_run_status.run({
319
308
  id: currentRunId,
320
309
  status: 499,
@@ -332,8 +321,6 @@ export default class AgentLoop {
332
321
  let turnPrompt;
333
322
  if (loopIteration === 1) {
334
323
  turnPrompt = prompt;
335
- } else if (mode === "panic") {
336
- turnPrompt = "Continue freeing space. Check <knowns> token counts.";
337
324
  } else {
338
325
  turnPrompt = this.#buildContinuationPrompt(
339
326
  loopIteration,
@@ -350,14 +337,15 @@ export default class AgentLoop {
350
337
  currentLoopId,
351
338
  requestedModel,
352
339
  loopPrompt: turnPrompt,
340
+ loopIteration,
353
341
  noRepo,
354
342
  toolSet,
343
+ inRecovery: recovery !== null,
355
344
  contextSize,
356
345
  options: { ...options, isContinuation: loopIteration > 1 },
357
- signal: controller.signal,
346
+ signal,
358
347
  });
359
348
 
360
- // Budget overflow — return 413 to drainQueue for panic mode
361
349
  if (result.status === 413) {
362
350
  return {
363
351
  run: currentAlias,
@@ -371,44 +359,28 @@ export default class AgentLoop {
371
359
 
372
360
  _lastAssembledTokens = result.assembledTokens;
373
361
 
374
- // Panic mode: target check + strike counting
375
- if (mode === "panic") {
376
- const panicTarget = Math.floor(contextSize * 0.75);
377
- if (result.assembledTokens <= panicTarget) {
378
- await this.#db.update_run_status.run({
379
- id: currentRunId,
380
- status: 200,
381
- });
382
- const out = {
383
- run: currentAlias,
384
- status: 200,
385
- turn: result.turn,
386
- };
387
- await hook.completed.emit({ projectId, ...out });
388
- return out;
389
- }
390
- if (_lastPanicTokens !== null) {
391
- if (result.assembledTokens < _lastPanicTokens) {
392
- _panicStrikes = 0;
393
- } else {
394
- _panicStrikes++;
395
- if (_panicStrikes >= 3) {
396
- await this.#db.update_run_status.run({
397
- id: currentRunId,
398
- status: 200,
399
- });
400
- return {
401
- run: currentAlias,
402
- status: 413,
403
- overflow: result.assembledTokens - contextSize,
404
- assembledTokens: result.assembledTokens,
405
- contextSize,
406
- turn: result.turn,
407
- };
408
- }
409
- }
410
- }
411
- _lastPanicTokens = result.assembledTokens;
362
+ // Budget recovery: enforce progress toward context target.
363
+ const ra = advanceRecovery(recovery, result);
364
+ recovery = ra.next;
365
+ if (ra.action === "restore" && ra.promptPath) {
366
+ await this.#knownStore.setFidelity(
367
+ currentRunId,
368
+ ra.promptPath,
369
+ "full",
370
+ );
371
+ }
372
+ if (ra.action === "hard413") {
373
+ await this.#db.update_run_status.run({
374
+ id: currentRunId,
375
+ status: 413,
376
+ });
377
+ const out = {
378
+ run: currentAlias,
379
+ status: 413,
380
+ turn: result.turn,
381
+ };
382
+ await hook.completed.emit({ projectId, ...out });
383
+ return out;
412
384
  }
413
385
 
414
386
  const runUsage = await this.#db.get_run_usage.get({
@@ -442,12 +414,13 @@ export default class AgentLoop {
442
414
  model: result.model,
443
415
  temperature: result.temperature,
444
416
  context_size: result.contextSize,
445
- context_tokens: (
446
- await this.#db.get_turn_budget.get({
447
- run_id: currentRunId,
448
- turn: result.turn,
449
- })
450
- ).total,
417
+ context_tokens:
418
+ (
419
+ await this.#db.get_turn_context_tokens.get({
420
+ run_id: currentRunId,
421
+ sequence: result.turn,
422
+ })
423
+ )?.context_tokens ?? 0,
451
424
  prompt_tokens: runUsage.prompt_tokens,
452
425
  cached_tokens: runUsage.cached_tokens,
453
426
  completion_tokens: runUsage.completion_tokens,
@@ -482,6 +455,9 @@ export default class AgentLoop {
482
455
  flags: result.flags,
483
456
  });
484
457
 
458
+ // Don't exit while budget recovery is still active.
459
+ if (recovery !== null) continue;
460
+
485
461
  const repetition = healer.assessRepetition(result);
486
462
  if (!repetition.continue) {
487
463
  await this.#db.update_run_status.run({
@@ -526,7 +502,7 @@ export default class AgentLoop {
526
502
  await hook.completed.emit({ projectId, ...out });
527
503
  return out;
528
504
  } catch (err) {
529
- if (controller.signal.aborted) {
505
+ if (signal.aborted) {
530
506
  await this.#db.update_run_status.run({
531
507
  id: currentRunId,
532
508
  status: 499,
@@ -558,7 +534,6 @@ export default class AgentLoop {
558
534
  await hook.completed.emit({ projectId, ...out });
559
535
  return out;
560
536
  } finally {
561
- this.#activeRuns.delete(currentRunId);
562
537
  await this.#hooks.loop.completed
563
538
  .emit({
564
539
  runId: currentRunId,
@@ -599,6 +574,29 @@ export default class AgentLoop {
599
574
  }
600
575
 
601
576
  if (action === "accept") {
577
+ if (path.startsWith("set://") && attrs?.file && attrs?.merge) {
578
+ const fileBody = await this.#knownStore.getBody(runId, attrs.file);
579
+ if (fileBody != null) {
580
+ const blocks = attrs.merge.split(/(?=<<<<<<< SEARCH)/);
581
+ let patched = fileBody;
582
+ for (const block of blocks) {
583
+ const m = block.match(
584
+ /<<<<<<< SEARCH\n?([\s\S]*?)\n?=======\n?([\s\S]*?)\n?>>>>>>> REPLACE/,
585
+ );
586
+ if (m) patched = patched.replace(m[1], m[2]);
587
+ }
588
+ const turn = (await this.#db.get_run_by_id.get({ id: runId }))
589
+ .next_turn;
590
+ await this.#knownStore.upsert(
591
+ runId,
592
+ turn,
593
+ attrs.file,
594
+ patched,
595
+ 200,
596
+ );
597
+ }
598
+ }
599
+
602
600
  if (path.startsWith("rm://")) {
603
601
  if (attrs?.path) {
604
602
  await this.#knownStore.remove(runId, attrs.path);
@@ -676,7 +674,7 @@ export default class AgentLoop {
676
674
  mode: resumeMode,
677
675
  model: runRow.model,
678
676
  prompt: "",
679
- config: "{}",
677
+ config: currentLoop?.config || "{}",
680
678
  });
681
679
  return this.#drainQueue(runId, runAlias, projectId, project, {});
682
680
  }
@@ -735,3 +733,51 @@ export default class AgentLoop {
735
733
  return this.#knownStore.getLog(runRow.id);
736
734
  }
737
735
  }
736
+
737
+ /**
738
+ * Pure recovery state transition — exported for testing.
739
+ *
740
+ * @param {object|null} recovery Current recovery state (mutated copy returned).
741
+ * @param {{ assembledTokens: number, budgetRecovery?: { target: number, promptPath: string|null } }} result
742
+ * @returns {{ next: object|null, action: null|'restore'|'hard413', promptPath: string|null }}
743
+ */
744
+ export function advanceRecovery(recovery, result) {
745
+ // Initialise or update recovery state from a new Turn Demotion event.
746
+ if (result.budgetRecovery) {
747
+ if (!recovery) {
748
+ recovery = {
749
+ target: result.budgetRecovery.target,
750
+ promptPath: result.budgetRecovery.promptPath,
751
+ strikes: 0,
752
+ lastTokens: result.assembledTokens,
753
+ };
754
+ } else {
755
+ // Re-overflow during recovery: tighten target, don't count as strike.
756
+ recovery = {
757
+ ...recovery,
758
+ target: Math.min(recovery.target, result.budgetRecovery.target),
759
+ };
760
+ }
761
+ }
762
+
763
+ if (recovery === null) return { next: null, action: null, promptPath: null };
764
+
765
+ const current = result.assembledTokens;
766
+
767
+ if (current <= recovery.target) {
768
+ return { next: null, action: "restore", promptPath: recovery.promptPath };
769
+ }
770
+
771
+ const noProgress = current >= recovery.lastTokens && !result.budgetRecovery;
772
+ const strikes = noProgress ? recovery.strikes + 1 : 0;
773
+
774
+ if (strikes >= 3) {
775
+ return { next: null, action: "hard413", promptPath: null };
776
+ }
777
+
778
+ return {
779
+ next: { ...recovery, strikes, lastTokens: current },
780
+ action: null,
781
+ promptPath: null,
782
+ };
783
+ }
@@ -13,6 +13,7 @@ export default class ContextAssembler {
13
13
  demoted = [],
14
14
  toolSet = null,
15
15
  lastContextTokens = 0,
16
+ turn = 1,
16
17
  } = {},
17
18
  hooks,
18
19
  ) {
@@ -30,6 +31,7 @@ export default class ContextAssembler {
30
31
  lastContextTokens,
31
32
  demoted,
32
33
  toolSet,
34
+ turn,
33
35
  };
34
36
 
35
37
  const system = await hooks.assembly.system.filter(systemPrompt, ctx);
@@ -1,25 +1,16 @@
1
1
  import slugify from "../sql/functions/slugify.js";
2
- import { countTokens } from "./tokens.js";
3
2
 
4
3
  export default class KnownStore {
5
4
  #db;
6
5
  #onChanged;
7
- #budgetGuard = null;
8
6
  #schemes = new Map();
7
+ #seq = 0;
9
8
 
10
9
  constructor(db, { onChanged } = {}) {
11
10
  this.#db = db;
12
11
  this.#onChanged = onChanged || null;
13
12
  }
14
13
 
15
- get budgetGuard() {
16
- return this.#budgetGuard;
17
- }
18
-
19
- set budgetGuard(guard) {
20
- this.#budgetGuard = guard;
21
- }
22
-
23
14
  async loadSchemes(db) {
24
15
  const rows = await (db || this.#db).get_all_schemes.all();
25
16
  this.#schemes.clear();
@@ -28,13 +19,6 @@ export default class KnownStore {
28
19
  }
29
20
  }
30
21
 
31
- #isVisible(path, fidelity) {
32
- if (fidelity === "archive") return false;
33
- const scheme = KnownStore.scheme(path) ?? "file";
34
- const meta = this.#schemes.get(scheme);
35
- return meta ? meta.model_visible !== 0 : true;
36
- }
37
-
38
22
  #emitChanged(runId, path, changeType) {
39
23
  if (this.#onChanged) this.#onChanged({ runId, path, changeType });
40
24
  }
@@ -46,15 +30,16 @@ export default class KnownStore {
46
30
 
47
31
  static normalizePath(path) {
48
32
  if (!path?.includes("://")) return path;
49
- return path.replace(/:\/\/(.*)$/, (_, rest) => {
50
- try {
51
- // Decode first (idempotent), then encode — but preserve slashes
52
- const decoded = decodeURIComponent(rest);
53
- return `://${decoded.split("/").map(encodeURIComponent).join("/")}`;
54
- } catch {
55
- return `://${rest.split("/").map(encodeURIComponent).join("/")}`;
56
- }
57
- });
33
+ const sep = path.indexOf("://");
34
+ const scheme = path.slice(0, sep).toLowerCase();
35
+ const rest = path.slice(sep + 3);
36
+ try {
37
+ // Decode first (idempotent), then encode — but preserve slashes
38
+ const decoded = decodeURIComponent(rest);
39
+ return `${scheme}://${decoded.split("/").map(encodeURIComponent).join("/")}`;
40
+ } catch {
41
+ return `${scheme}://${rest.split("/").map(encodeURIComponent).join("/")}`;
42
+ }
58
43
  }
59
44
 
60
45
  async nextTurn(runId) {
@@ -71,15 +56,15 @@ export default class KnownStore {
71
56
  path: candidate,
72
57
  });
73
58
  if (!existing) return candidate;
74
- return `${candidate}_${Date.now()}`;
59
+ return `${candidate}_${++this.#seq}`;
75
60
  }
76
61
 
77
62
  async slugPath(runId, scheme, content, summary) {
78
- const source = summary ? summary.replace(/,\s*/g, "/") : content || "";
63
+ const source = summary || content || "";
79
64
  const base = slugify(source);
80
65
  const prefix = `${scheme}://`;
81
66
 
82
- if (!base) return `${prefix}${Date.now()}`;
67
+ if (!base) return `${prefix}${++this.#seq}`;
83
68
 
84
69
  const candidate = `${prefix}${base}`;
85
70
  const existing = await this.#db.get_entry_body.get({
@@ -88,7 +73,7 @@ export default class KnownStore {
88
73
  });
89
74
  if (!existing) return candidate;
90
75
 
91
- return `${prefix}${base}_${Date.now()}`;
76
+ return `${prefix}${base}_${++this.#seq}`;
92
77
  }
93
78
 
94
79
  async upsert(
@@ -106,22 +91,6 @@ export default class KnownStore {
106
91
  } = {},
107
92
  ) {
108
93
  const normalized = KnownStore.normalizePath(path);
109
- let delta = 0;
110
-
111
- if (
112
- this.#budgetGuard &&
113
- status < 400 &&
114
- this.#isVisible(normalized, fidelity)
115
- ) {
116
- const existing = await this.#db.get_entry_body.get({
117
- run_id: runId,
118
- path: normalized,
119
- });
120
- delta =
121
- countTokens(body) - (existing?.body ? countTokens(existing.body) : 0);
122
- this.#budgetGuard.check(delta, normalized);
123
- }
124
-
125
94
  await this.#db.upsert_known_entry.run({
126
95
  run_id: runId,
127
96
  loop_id: loopId,
@@ -135,8 +104,6 @@ export default class KnownStore {
135
104
  updated_at: updatedAt,
136
105
  });
137
106
  this.#emitChanged(runId, normalized, "upsert");
138
-
139
- if (delta > 0) this.#budgetGuard?.charge(delta);
140
107
  }
141
108
 
142
109
  async promote(runId, path, turn) {
@@ -202,21 +169,6 @@ export default class KnownStore {
202
169
  }
203
170
 
204
171
  async promoteByPattern(runId, path, body, turn) {
205
- let cost = 0;
206
- if (this.#budgetGuard) {
207
- const entries = await this.#db.get_entries_by_pattern.all({
208
- run_id: runId,
209
- path,
210
- body: KnownStore.#bodyPattern(body),
211
- limit: null,
212
- offset: null,
213
- });
214
- cost = entries
215
- .filter((e) => e.fidelity === "archive" || e.fidelity === "index")
216
- .reduce((sum, e) => sum + (e.tokens_full || 0), 0);
217
- if (cost > 0) this.#budgetGuard.check(cost, path);
218
- }
219
-
220
172
  await this.#db.promote_by_pattern.run({
221
173
  run_id: runId,
222
174
  path,
@@ -224,8 +176,6 @@ export default class KnownStore {
224
176
  turn,
225
177
  });
226
178
  this.#emitChanged(runId, path, "promote");
227
-
228
- if (cost > 0) this.#budgetGuard?.charge(cost);
229
179
  }
230
180
 
231
181
  async demoteByPattern(runId, path, body) {
@@ -257,24 +207,6 @@ export default class KnownStore {
257
207
  }
258
208
 
259
209
  async updateBodyByPattern(runId, path, body, newBody) {
260
- let delta = 0;
261
- if (this.#budgetGuard) {
262
- const entries = await this.#db.get_entries_by_pattern.all({
263
- run_id: runId,
264
- path,
265
- body: KnownStore.#bodyPattern(body),
266
- limit: null,
267
- offset: null,
268
- });
269
- const visible = entries.filter((e) =>
270
- this.#isVisible(e.path, e.fidelity),
271
- );
272
- const oldTotal = visible.reduce((sum, e) => sum + (e.tokens || 0), 0);
273
- const newTokensPer = countTokens(newBody);
274
- delta = newTokensPer * visible.length - oldTotal;
275
- if (delta > 0) this.#budgetGuard.check(delta, path);
276
- }
277
-
278
210
  await this.#db.update_body_by_pattern.run({
279
211
  run_id: runId,
280
212
  path,
@@ -282,8 +214,6 @@ export default class KnownStore {
282
214
  new_body: newBody,
283
215
  });
284
216
  this.#emitChanged(runId, path, "body");
285
-
286
- if (delta > 0) this.#budgetGuard?.charge(delta);
287
217
  }
288
218
 
289
219
  async resolve(runId, path, status, body) {
@@ -297,6 +227,19 @@ export default class KnownStore {
297
227
  this.#emitChanged(runId, normalized, "resolve");
298
228
  }
299
229
 
230
+ async restoreSummarizedPrompts(runId) {
231
+ await this.#db.restore_summarized_prompts.run({ run_id: runId });
232
+ this.#emitChanged(runId, "prompt://batch", "fidelity");
233
+ }
234
+
235
+ async demotePreviousLoopLogging(runId, loopId) {
236
+ await this.#db.demote_previous_loop_logging.run({
237
+ run_id: runId,
238
+ loop_id: loopId,
239
+ });
240
+ this.#emitChanged(runId, "logging://batch", "fidelity");
241
+ }
242
+
300
243
  async getLog(runId) {
301
244
  return this.#db.get_results.all({ run_id: runId });
302
245
  }