@possumtech/rummy 0.2.6 → 0.2.8

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 (60) hide show
  1. package/.env.example +2 -3
  2. package/PLUGINS.md +105 -82
  3. package/bin/rummy.js +9 -2
  4. package/migrations/001_initial_schema.sql +53 -68
  5. package/package.json +4 -6
  6. package/service.js +2 -2
  7. package/src/agent/AgentLoop.js +91 -58
  8. package/src/agent/ContextAssembler.js +2 -2
  9. package/src/agent/KnownStore.js +30 -11
  10. package/src/agent/ProjectAgent.js +1 -3
  11. package/src/agent/TurnExecutor.js +119 -31
  12. package/src/agent/XmlParser.js +20 -0
  13. package/src/agent/known_checks.sql +5 -4
  14. package/src/agent/known_queries.sql +4 -3
  15. package/src/agent/known_store.sql +29 -15
  16. package/src/agent/loops.sql +63 -0
  17. package/src/agent/runs.sql +7 -7
  18. package/src/agent/schemes.sql +2 -2
  19. package/src/agent/turns.sql +3 -3
  20. package/src/hooks/PluginContext.js +1 -10
  21. package/src/hooks/RummyContext.js +16 -8
  22. package/src/plugins/ask_user/ask_user.js +3 -2
  23. package/src/plugins/cp/cp.js +7 -7
  24. package/src/plugins/current/current.js +3 -4
  25. package/src/plugins/engine/engine.sql +5 -3
  26. package/src/plugins/engine/turn_context.sql +9 -4
  27. package/src/plugins/env/docs.md +2 -0
  28. package/src/plugins/env/env.js +3 -2
  29. package/src/plugins/file/file.js +9 -19
  30. package/src/plugins/get/docs.md +7 -3
  31. package/src/plugins/get/get.js +22 -6
  32. package/src/plugins/hedberg/docs.md +0 -9
  33. package/src/plugins/hedberg/hedberg.js +2 -5
  34. package/src/plugins/hedberg/matcher.js +1 -1
  35. package/src/plugins/hedberg/patterns.js +6 -6
  36. package/src/plugins/helpers.js +2 -2
  37. package/src/plugins/index.js +28 -15
  38. package/src/plugins/instructions/instructions.js +1 -1
  39. package/src/plugins/known/known.js +9 -11
  40. package/src/plugins/mv/mv.js +7 -7
  41. package/src/plugins/previous/previous.js +6 -5
  42. package/src/plugins/progress/progress.js +6 -0
  43. package/src/plugins/prompt/prompt.js +9 -10
  44. package/src/plugins/rm/docs.md +3 -1
  45. package/src/plugins/rm/rm.js +24 -7
  46. package/src/plugins/rpc/rpc.js +33 -42
  47. package/src/plugins/set/docs.md +3 -1
  48. package/src/plugins/set/set.js +22 -16
  49. package/src/plugins/sh/sh.js +3 -2
  50. package/src/plugins/skills/skills.js +3 -4
  51. package/src/plugins/store/docs.md +2 -1
  52. package/src/plugins/store/store.js +14 -3
  53. package/src/plugins/summarize/summarize.js +1 -1
  54. package/src/plugins/telemetry/telemetry.js +17 -7
  55. package/src/plugins/unknown/unknown.js +3 -2
  56. package/src/plugins/update/update.js +1 -1
  57. package/src/server/ClientConnection.js +3 -5
  58. package/src/sql/v_model_context.sql +20 -23
  59. package/src/sql/v_run_log.sql +3 -3
  60. package/src/agent/prompt_queue.sql +0 -39
@@ -25,6 +25,7 @@ export default class TurnExecutor {
25
25
  projectId,
26
26
  currentRunId,
27
27
  currentAlias,
28
+ currentLoopId,
28
29
  requestedModel,
29
30
  loopPrompt,
30
31
  noContext,
@@ -36,6 +37,7 @@ export default class TurnExecutor {
36
37
 
37
38
  const turnRow = await this.#db.create_turn.get({
38
39
  run_id: currentRunId,
40
+ loop_id: currentLoopId,
39
41
  sequence: turn,
40
42
  });
41
43
 
@@ -67,6 +69,7 @@ export default class TurnExecutor {
67
69
  type: mode,
68
70
  sequence: turn,
69
71
  runId: currentRunId,
72
+ loopId: currentLoopId,
70
73
  turnId: turnRow.id,
71
74
  noContext,
72
75
  contextSize,
@@ -123,11 +126,12 @@ export default class TurnExecutor {
123
126
 
124
127
  await this.#db.insert_turn_context.run({
125
128
  run_id: currentRunId,
129
+ loop_id: currentLoopId,
126
130
  turn,
127
131
  ordinal: row.ordinal,
128
132
  path: row.path,
129
133
  fidelity: row.fidelity,
130
- state: row.state,
134
+ status: row.status,
131
135
  body: projectedBody ?? "",
132
136
  tokens: countTokens(projectedBody ?? ""),
133
137
  attributes: row.attributes,
@@ -136,6 +140,8 @@ export default class TurnExecutor {
136
140
  });
137
141
  }
138
142
 
143
+ const demoted = [];
144
+
139
145
  await this.#hooks.run.progress.emit({
140
146
  projectId,
141
147
  run: currentAlias,
@@ -144,20 +150,96 @@ export default class TurnExecutor {
144
150
  });
145
151
 
146
152
  // Assemble messages from projected system prompt + materialized turn_context
147
- const rows = await this.#db.get_turn_context.all({
153
+ let rows = await this.#db.get_turn_context.all({
148
154
  run_id: currentRunId,
149
155
  turn,
150
156
  });
151
- const messages = await ContextAssembler.assembleFromTurnContext(
157
+ let messages = await ContextAssembler.assembleFromTurnContext(
152
158
  rows,
153
159
  {
154
160
  type: mode,
155
161
  systemPrompt,
156
162
  contextSize,
163
+ demoted,
157
164
  },
158
165
  this.#hooks,
159
166
  );
160
167
 
168
+ // Budget check on assembled messages (includes system prompt)
169
+ if (contextSize && demoted.length === 0) {
170
+ const assembledTokens = messages.reduce(
171
+ (sum, m) => sum + countTokens(m.content || ""),
172
+ 0,
173
+ );
174
+ const ceiling = contextSize * 0.95;
175
+ if (assembledTokens > ceiling) {
176
+ const candidates = rows
177
+ .filter(
178
+ (r) =>
179
+ r.fidelity === "full" &&
180
+ r.tokens > 0 &&
181
+ (r.category === "file" || r.category === "known"),
182
+ )
183
+ .toSorted((a, b) => a.source_turn - b.source_turn);
184
+
185
+ let excess = assembledTokens - ceiling;
186
+ for (const entry of candidates) {
187
+ if (excess <= 0) break;
188
+ await this.#knownStore.setFidelity(
189
+ currentRunId,
190
+ entry.path,
191
+ "summary",
192
+ );
193
+ excess -= entry.tokens;
194
+ demoted.push(entry.path);
195
+ }
196
+
197
+ if (demoted.length > 0) {
198
+ await this.#db.clear_turn_context.run({ run_id: currentRunId, turn });
199
+ const freshViewRows = await this.#db.get_model_context.all({
200
+ run_id: currentRunId,
201
+ });
202
+ for (const row of freshViewRows) {
203
+ const scheme = row.scheme || "file";
204
+ const projectedBody = await this.#hooks.tools.view(scheme, {
205
+ path: row.path,
206
+ scheme,
207
+ body: row.body,
208
+ attributes: row.attributes ? JSON.parse(row.attributes) : null,
209
+ fidelity: row.fidelity,
210
+ category: row.category,
211
+ });
212
+ await this.#db.insert_turn_context.run({
213
+ run_id: currentRunId,
214
+ loop_id: currentLoopId,
215
+ turn,
216
+ ordinal: row.ordinal,
217
+ path: row.path,
218
+ fidelity: row.fidelity,
219
+ status: row.status,
220
+ body: projectedBody ?? "",
221
+ tokens: countTokens(projectedBody ?? ""),
222
+ attributes: row.attributes,
223
+ category: row.category,
224
+ source_turn: row.turn,
225
+ });
226
+ }
227
+ rows = await this.#db.get_turn_context.all({
228
+ run_id: currentRunId,
229
+ turn,
230
+ });
231
+ messages = await ContextAssembler.assembleFromTurnContext(
232
+ rows,
233
+ { type: mode, systemPrompt, contextSize, demoted },
234
+ this.#hooks,
235
+ );
236
+ console.warn(
237
+ `[RUMMY] Budget exceeded: demoted ${demoted.length} entries to fit ${contextSize} token limit`,
238
+ );
239
+ }
240
+ }
241
+ }
242
+
161
243
  const filteredMessages = await this.#hooks.llm.messages.filter(messages, {
162
244
  model: requestedModel,
163
245
  projectId,
@@ -217,7 +299,13 @@ export default class TurnExecutor {
217
299
  let updateText = null;
218
300
 
219
301
  for (const cmd of commands) {
220
- const entry = await this.#record(currentRunId, turn, mode, cmd);
302
+ const entry = await this.#record(
303
+ currentRunId,
304
+ currentLoopId,
305
+ turn,
306
+ mode,
307
+ cmd,
308
+ );
221
309
  if (!entry) continue;
222
310
 
223
311
  if (entry.scheme === "summarize") summaryText = entry.body;
@@ -249,7 +337,8 @@ export default class TurnExecutor {
249
337
  turn,
250
338
  summaryPath,
251
339
  summaryText,
252
- "summary",
340
+ 200,
341
+ { loopId: currentLoopId },
253
342
  );
254
343
  } else if (updateText) {
255
344
  const updatePath = await this.#knownStore.slugPath(
@@ -262,14 +351,14 @@ export default class TurnExecutor {
262
351
  turn,
263
352
  updatePath,
264
353
  updateText,
265
- "info",
354
+ 200,
355
+ { loopId: currentLoopId },
266
356
  );
267
357
  }
268
358
 
269
359
  // --- PHASE 2: DISPATCH ---
270
360
  // Handlers perform side effects: promote, demote, patch, propose.
271
361
 
272
- let hasErrors = false;
273
362
  for (const entry of recorded) {
274
363
  await this.#hooks.tools.dispatch(entry.scheme, entry, rummy);
275
364
  await this.#hooks.entry.created.emit(entry);
@@ -278,13 +367,16 @@ export default class TurnExecutor {
278
367
  // Materialize proposals (e.g. file plugin applies accumulated revisions)
279
368
  await this.#hooks.turn.proposing.emit({ rummy, recorded });
280
369
 
281
- // Check if any dispatched entries ended in error state
370
+ // Check if any dispatched entries ended in error or proposed state
371
+ let hasErrors = false;
372
+ let hasProposed = false;
282
373
  for (const entry of recorded) {
283
374
  const row = await this.#db.get_entry_state.get({
284
375
  run_id: currentRunId,
285
376
  path: entry.resultPath || entry.path,
286
377
  });
287
- if (row?.state === "error") hasErrors = true;
378
+ if (row?.status >= 400) hasErrors = true;
379
+ if (row?.status === 202) hasProposed = true;
288
380
  }
289
381
 
290
382
  // Errors override summarize — the model thinks it's done but it's not
@@ -293,6 +385,12 @@ export default class TurnExecutor {
293
385
  updateText = "Tool errors detected — retry or investigate.";
294
386
  }
295
387
 
388
+ // Proposals override summarize — outcome unknown until user resolves
389
+ if (hasProposed && summaryText) {
390
+ summaryText = null;
391
+ updateText = "Awaiting approval for proposed changes.";
392
+ }
393
+
296
394
  // --- Classify for return value ---
297
395
 
298
396
  const actionCalls = recorded.filter((e) =>
@@ -343,7 +441,7 @@ export default class TurnExecutor {
343
441
  * Record a parsed command as a known_entries row.
344
442
  * Returns the recorded entry descriptor, or null if rejected/skipped.
345
443
  */
346
- async #record(runId, turn, mode, cmd) {
444
+ async #record(runId, loopId, turn, mode, cmd) {
347
445
  // Mode enforcement — reject prohibited commands in ask mode
348
446
  if (mode === "ask") {
349
447
  if (cmd.name === "sh") {
@@ -391,7 +489,9 @@ export default class TurnExecutor {
391
489
  "unknown",
392
490
  cmd.body,
393
491
  );
394
- await this.#knownStore.upsert(runId, turn, unknownPath, cmd.body, "full");
492
+ await this.#knownStore.upsert(runId, turn, unknownPath, cmd.body, 200, {
493
+ loopId,
494
+ });
395
495
  return {
396
496
  scheme,
397
497
  path: unknownPath,
@@ -401,14 +501,8 @@ export default class TurnExecutor {
401
501
  };
402
502
  }
403
503
 
404
- // Normalize path — encode spaces in scheme:// paths
405
504
  const rawTarget = cmd.path || cmd.command || cmd.question || "";
406
- const target = rawTarget.includes("://")
407
- ? rawTarget.replace(
408
- /:\/\/(.*)$/,
409
- (_, rest) => `://${encodeURIComponent(decodeURIComponent(rest))}`,
410
- )
411
- : rawTarget;
505
+ const target = rawTarget;
412
506
  const resultPath = await this.#knownStore.dedup(runId, scheme, target);
413
507
 
414
508
  // Pass parsed command fields through as attributes
@@ -420,7 +514,9 @@ export default class TurnExecutor {
420
514
  if (!cmd.body) return null;
421
515
  const knownPath =
422
516
  cmd.path || (await this.#knownStore.slugPath(runId, "known", cmd.body));
423
- await this.#knownStore.upsert(runId, turn, knownPath, cmd.body, "full");
517
+ await this.#knownStore.upsert(runId, turn, knownPath, cmd.body, 200, {
518
+ loopId,
519
+ });
424
520
  return {
425
521
  scheme: "known",
426
522
  path: knownPath,
@@ -430,11 +526,11 @@ export default class TurnExecutor {
430
526
  };
431
527
  }
432
528
 
433
- // Record the entry
529
+ // Record the entry — 200 OK, handlers change status during dispatch
434
530
  const body = cmd.body || cmd.command || cmd.question || "";
435
- const state = this.#initialState(scheme);
436
- await this.#knownStore.upsert(runId, turn, resultPath, body, state, {
531
+ await this.#knownStore.upsert(runId, turn, resultPath, body, 200, {
437
532
  attributes,
533
+ loopId,
438
534
  });
439
535
 
440
536
  return {
@@ -442,16 +538,8 @@ export default class TurnExecutor {
442
538
  path: resultPath,
443
539
  body,
444
540
  attributes,
445
- state,
541
+ status: 200,
446
542
  resultPath,
447
543
  };
448
544
  }
449
-
450
- /**
451
- * Initial state for a recorded command entry.
452
- * All entries start at "full". Handlers change state during dispatch.
453
- */
454
- #initialState(_scheme) {
455
- return "full";
456
- }
457
545
  }
@@ -190,6 +190,16 @@ export default class XmlParser {
190
190
  return;
191
191
  }
192
192
 
193
+ // Known tool opened while another is still open — close the old one.
194
+ if (current) {
195
+ warnings.push(
196
+ `Unclosed <${current.name}> before <${name}> — recovered`,
197
+ );
198
+ commands.push(
199
+ resolveCommand(current.name, current.attrs, current.rawBody),
200
+ );
201
+ }
202
+
193
203
  current = { name, attrs, rawBody: "" };
194
204
  },
195
205
 
@@ -210,6 +220,16 @@ export default class XmlParser {
210
220
  resolveCommand(current.name, current.attrs, current.rawBody),
211
221
  );
212
222
  current = null;
223
+ } else if (current && ALL_TOOLS.has(name)) {
224
+ // Mismatched close tag for a known tool — close current tag,
225
+ // don't swallow subsequent commands as body text.
226
+ warnings.push(
227
+ `Mismatched </${name}> closing <${current.name}> — recovered`,
228
+ );
229
+ commands.push(
230
+ resolveCommand(current.name, current.attrs, current.rawBody),
231
+ );
232
+ current = null;
213
233
  } else if (current) {
214
234
  current.rawBody += `</${name}>`;
215
235
  } else if (isImplied && ALL_TOOLS.has(name)) {
@@ -17,25 +17,26 @@ SELECT path, body, attributes, turn
17
17
  FROM known_entries
18
18
  WHERE
19
19
  run_id = :run_id
20
- AND state = 'proposed';
20
+ AND status = 202;
21
21
 
22
22
  -- PREP: has_rejections
23
23
  SELECT COUNT(*) AS count
24
24
  FROM known_entries
25
25
  WHERE
26
26
  run_id = :run_id
27
- AND state = 'rejected';
27
+ AND loop_id = :loop_id
28
+ AND status = 403;
28
29
 
29
30
  -- PREP: has_accepted_actions
30
31
  SELECT COUNT(*) AS count
31
32
  FROM known_entries
32
33
  WHERE
33
34
  run_id = :run_id
34
- AND state = 'pass'
35
+ AND status = 200
35
36
  AND scheme IN ('set', 'sh', 'rm', 'mv', 'cp');
36
37
 
37
38
  -- PREP: get_file_entries
38
- SELECT path, state, hash, updated_at
39
+ SELECT path, status, fidelity, hash, updated_at
39
40
  FROM known_entries
40
41
  WHERE
41
42
  run_id = :run_id
@@ -1,5 +1,5 @@
1
1
  -- PREP: get_known_entries
2
- SELECT path, scheme, state, body, turn, hash, attributes
2
+ SELECT path, scheme, status, fidelity, body, turn, hash, attributes
3
3
  FROM known_entries
4
4
  WHERE run_id = :run_id
5
5
  ORDER BY path;
@@ -18,7 +18,7 @@ WHERE
18
18
  ORDER BY id;
19
19
 
20
20
  -- PREP: get_turn_audit
21
- SELECT path, scheme, state, turn, body, attributes
21
+ SELECT path, scheme, status, fidelity, turn, body, attributes
22
22
  FROM known_entries
23
23
  WHERE
24
24
  run_id = :run_id
@@ -57,12 +57,13 @@ SELECT body
57
57
  FROM known_entries
58
58
  WHERE
59
59
  run_id = :run_id
60
+ AND loop_id = :loop_id
60
61
  AND scheme = 'summarize'
61
62
  ORDER BY id DESC
62
63
  LIMIT 1;
63
64
 
64
65
  -- PREP: get_history
65
- SELECT ke.path, ke.state AS status, ke.body, ke.attributes, ke.turn
66
+ SELECT ke.path, ke.status, ke.body, ke.attributes, ke.turn
66
67
  FROM known_entries AS ke
67
68
  JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
68
69
  WHERE
@@ -1,19 +1,22 @@
1
1
  -- PREP: upsert_known_entry
2
2
  INSERT INTO known_entries (
3
- run_id, turn, path, body, state, hash, attributes
4
- , tokens, tokens_full, updated_at
3
+ run_id, loop_id, turn, path, body, status, fidelity, hash
4
+ , attributes, tokens, tokens_full, updated_at
5
5
  )
6
6
  VALUES (
7
- :run_id, :turn, :path, :body, :state, :hash, COALESCE(:attributes, '{}')
7
+ :run_id, :loop_id, :turn, :path, :body, :status, :fidelity, :hash
8
+ , COALESCE(:attributes, '{}')
8
9
  , countTokens(:body)
9
10
  , countTokens(:body)
10
11
  , COALESCE(:updated_at, CURRENT_TIMESTAMP)
11
12
  )
12
13
  ON CONFLICT (run_id, path) DO UPDATE SET
13
14
  body = excluded.body
14
- , state = excluded.state
15
+ , status = excluded.status
16
+ , fidelity = excluded.fidelity
15
17
  , hash = COALESCE(excluded.hash, known_entries.hash)
16
18
  , attributes = COALESCE(excluded.attributes, known_entries.attributes)
19
+ , loop_id = excluded.loop_id
17
20
  , turn = excluded.turn
18
21
  , tokens = countTokens(excluded.body)
19
22
  , tokens_full = countTokens(excluded.body)
@@ -43,17 +46,17 @@ WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL;
43
46
  -- PREP: resolve_known_entry
44
47
  UPDATE known_entries
45
48
  SET
46
- state = :state
49
+ status = :status
47
50
  , body = :body
48
51
  , updated_at = CURRENT_TIMESTAMP
49
52
  WHERE run_id = :run_id AND path = :path;
50
53
 
51
- -- PREP: set_file_state
54
+ -- PREP: set_file_fidelity
52
55
  UPDATE known_entries
53
56
  SET
54
- state = :state
57
+ fidelity = :fidelity
55
58
  , tokens = CASE
56
- WHEN :state = 'summary' THEN countTokens(body)
59
+ WHEN :fidelity = 'summary' THEN countTokens(body)
57
60
  ELSE tokens_full
58
61
  END
59
62
  , updated_at = CURRENT_TIMESTAMP
@@ -62,7 +65,7 @@ WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL;
62
65
  -- PREP: promote_path
63
66
  UPDATE known_entries
64
67
  SET
65
- state = 'full'
68
+ fidelity = 'full'
66
69
  , turn = :turn
67
70
  , tokens = tokens_full
68
71
  , updated_at = CURRENT_TIMESTAMP
@@ -71,23 +74,34 @@ WHERE run_id = :run_id AND path = :path;
71
74
  -- PREP: demote_path
72
75
  UPDATE known_entries
73
76
  SET
74
- state = 'stored'
77
+ fidelity = 'stored'
75
78
  , tokens = 0
76
79
  , updated_at = CURRENT_TIMESTAMP
77
80
  WHERE run_id = :run_id AND path = :path;
78
81
 
82
+ -- PREP: set_fidelity
83
+ UPDATE known_entries
84
+ SET
85
+ fidelity = :fidelity
86
+ , tokens = CASE
87
+ WHEN :fidelity = 'stored' THEN 0
88
+ ELSE countTokens(body)
89
+ END
90
+ , updated_at = CURRENT_TIMESTAMP
91
+ WHERE run_id = :run_id AND path = :path;
92
+
79
93
  -- PREP: get_entry_body
80
94
  SELECT body
81
95
  FROM known_entries
82
96
  WHERE run_id = :run_id AND path = :path;
83
97
 
84
98
  -- PREP: get_entry_state
85
- SELECT state, scheme, turn
99
+ SELECT status, fidelity, scheme, turn
86
100
  FROM known_entries
87
101
  WHERE run_id = :run_id AND path = :path;
88
102
 
89
103
  -- PREP: get_file_states_by_pattern
90
- SELECT path, state, turn
104
+ SELECT path, status, fidelity, turn
91
105
  FROM known_entries
92
106
  WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL
93
107
  ORDER BY path;
@@ -107,7 +121,7 @@ WHERE run_id = :run_id AND path = :path;
107
121
  -- PREP: promote_by_pattern
108
122
  UPDATE known_entries
109
123
  SET
110
- state = 'full'
124
+ fidelity = 'full'
111
125
  , turn = :turn
112
126
  , tokens = tokens_full
113
127
  , updated_at = CURRENT_TIMESTAMP
@@ -119,7 +133,7 @@ WHERE
119
133
  -- PREP: demote_by_pattern
120
134
  UPDATE known_entries
121
135
  SET
122
- state = 'stored'
136
+ fidelity = 'stored'
123
137
  , tokens = 0
124
138
  , updated_at = CURRENT_TIMESTAMP
125
139
  WHERE
@@ -128,7 +142,7 @@ WHERE
128
142
  AND (:body IS NULL OR hedsearch(:body, body));
129
143
 
130
144
  -- PREP: get_entries_by_pattern
131
- SELECT path, body, scheme, state, tokens_full, attributes
145
+ SELECT path, body, scheme, status, fidelity, tokens_full, attributes
132
146
  FROM known_entries
133
147
  WHERE
134
148
  run_id = :run_id
@@ -0,0 +1,63 @@
1
+ -- PREP: enqueue_loop
2
+ INSERT INTO loops (run_id, sequence, mode, model, prompt, config)
3
+ VALUES (:run_id, :sequence, :mode, :model, :prompt, :config)
4
+ RETURNING id, sequence;
5
+
6
+ -- PREP: next_loop
7
+ UPDATE runs
8
+ SET next_loop = next_loop + 1
9
+ WHERE id = :run_id
10
+ RETURNING next_loop - 1 AS sequence;
11
+
12
+ -- PREP: claim_next_loop
13
+ UPDATE loops
14
+ SET status = 102
15
+ WHERE
16
+ id = (
17
+ SELECT
18
+ id
19
+ FROM loops
20
+ WHERE run_id = :run_id AND status = 100
21
+ ORDER BY id
22
+ LIMIT 1
23
+ )
24
+ RETURNING id, run_id, sequence, mode, model, prompt, config;
25
+
26
+ -- PREP: complete_loop
27
+ UPDATE loops
28
+ SET status = :status, result = :result
29
+ WHERE id = :id;
30
+
31
+ -- PREP: abort_active_loop
32
+ UPDATE loops
33
+ SET status = 499
34
+ WHERE run_id = :run_id AND status = 102;
35
+
36
+ -- PREP: get_pending_loops
37
+ SELECT id, sequence, mode, model, prompt, status, created_at
38
+ FROM loops
39
+ WHERE run_id = :run_id AND status IN (100, 102)
40
+ ORDER BY id;
41
+
42
+ -- PREP: reset_active_loops
43
+ UPDATE loops
44
+ SET status = 100
45
+ WHERE status = 102;
46
+
47
+ -- PREP: get_current_loop
48
+ SELECT id, sequence, mode, model, prompt, status
49
+ FROM loops
50
+ WHERE run_id = :run_id AND status = 102
51
+ LIMIT 1;
52
+
53
+ -- PREP: get_loop_by_id
54
+ SELECT id, run_id, sequence, mode, model, prompt, status, config
55
+ FROM loops
56
+ WHERE id = :id;
57
+
58
+ -- PREP: get_latest_completed_loop
59
+ SELECT id, sequence, mode, status
60
+ FROM loops
61
+ WHERE run_id = :run_id AND status IN (200, 500)
62
+ ORDER BY id DESC
63
+ LIMIT 1;
@@ -22,14 +22,14 @@ RETURNING id;
22
22
  -- PREP: get_run_by_alias
23
23
  SELECT
24
24
  id, project_id, parent_run_id, model, status, alias
25
- , temperature, persona, context_limit, next_turn, created_at
25
+ , temperature, persona, context_limit, next_turn, next_loop, created_at
26
26
  FROM runs
27
27
  WHERE alias = :alias;
28
28
 
29
29
  -- PREP: get_run_by_id
30
30
  SELECT
31
31
  id, project_id, parent_run_id, model, status, alias
32
- , temperature, persona, context_limit, next_turn, created_at
32
+ , temperature, persona, context_limit, next_turn, next_loop, created_at
33
33
  FROM runs
34
34
  WHERE id = :id;
35
35
 
@@ -80,11 +80,11 @@ RETURNING next_turn - 1 AS turn;
80
80
 
81
81
  -- PREP: fork_known_entries
82
82
  INSERT INTO known_entries (
83
- run_id, turn, path, body, state
83
+ run_id, loop_id, turn, path, body, status, fidelity
84
84
  , hash, attributes, tokens, tokens_full, refs, write_count
85
85
  )
86
86
  SELECT
87
- :new_run_id, turn, path, body, state
87
+ :new_run_id, NULL, turn, path, body, status, fidelity
88
88
  , hash, attributes, tokens, tokens_full, refs, write_count
89
89
  FROM known_entries
90
90
  WHERE run_id = :parent_run_id;
@@ -94,7 +94,7 @@ SELECT r.id
94
94
  FROM runs AS r
95
95
  WHERE
96
96
  r.project_id = :project_id
97
- AND r.status IN ('queued', 'running', 'proposed');
97
+ AND r.status IN (100, 102, 202);
98
98
 
99
99
  -- PREP: get_latest_run
100
100
  SELECT r.id
@@ -110,5 +110,5 @@ WHERE r.project_id = :project_id;
110
110
 
111
111
  -- PREP: abort_stuck_runs
112
112
  UPDATE runs
113
- SET status = 'aborted'
114
- WHERE status IN ('running', 'queued');
113
+ SET status = 499
114
+ WHERE status IN (100, 102);
@@ -1,3 +1,3 @@
1
1
  -- PREP: upsert_scheme
2
- INSERT OR REPLACE INTO schemes (name, fidelity, model_visible, valid_states, category)
3
- VALUES (:name, :fidelity, :model_visible, :valid_states, :category);
2
+ INSERT OR REPLACE INTO schemes (name, model_visible, category)
3
+ VALUES (:name, :model_visible, :category);
@@ -1,6 +1,6 @@
1
1
  -- PREP: create_turn
2
- INSERT INTO turns (run_id, sequence)
3
- VALUES (:run_id, :sequence)
2
+ INSERT INTO turns (run_id, loop_id, sequence)
3
+ VALUES (:run_id, :loop_id, :sequence)
4
4
  RETURNING id, sequence;
5
5
 
6
6
  -- PREP: update_turn_stats
@@ -26,7 +26,7 @@ FROM turns
26
26
  WHERE run_id = :run_id;
27
27
 
28
28
  -- PREP: get_run_log
29
- SELECT ke.path, ke.state AS status, ke.body, ke.attributes
29
+ SELECT ke.path, ke.status, ke.body, ke.attributes
30
30
  FROM known_entries AS ke
31
31
  JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
32
32
  WHERE
@@ -48,18 +48,10 @@ export default class PluginContext {
48
48
  return this.#schemes;
49
49
  }
50
50
 
51
- registerScheme({
52
- name,
53
- fidelity = "full",
54
- modelVisible = 1,
55
- validStates = ["full", "proposed", "pass", "rejected", "error"],
56
- category = "result",
57
- } = {}) {
51
+ registerScheme({ name, modelVisible = 1, category = "result" } = {}) {
58
52
  this.#schemes.push({
59
53
  name: name || this.#name,
60
- fidelity,
61
54
  model_visible: modelVisible,
62
- valid_states: JSON.stringify(validStates),
63
55
  category,
64
56
  });
65
57
  }
@@ -78,7 +70,6 @@ export default class PluginContext {
78
70
  return;
79
71
  }
80
72
  if (event === "full" || event === "summary") {
81
- this.#hooks.tools.ensureTool(this.#name);
82
73
  this.#hooks.tools.onView(this.#name, callback, event);
83
74
  return;
84
75
  }