@possumtech/rummy 0.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/.env.example +21 -4
  2. package/PLUGINS.md +389 -194
  3. package/README.md +25 -8
  4. package/SPEC.md +850 -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 +6 -4
  11. package/service.js +50 -9
  12. package/src/agent/AgentLoop.js +460 -331
  13. package/src/agent/ContextAssembler.js +4 -2
  14. package/src/agent/Entries.js +655 -0
  15. package/src/agent/ProjectAgent.js +30 -18
  16. package/src/agent/TurnExecutor.js +232 -379
  17. package/src/agent/XmlParser.js +242 -67
  18. package/src/agent/budget.js +56 -0
  19. package/src/agent/errors.js +22 -0
  20. package/src/agent/httpStatus.js +39 -0
  21. package/src/agent/known_checks.sql +8 -4
  22. package/src/agent/known_queries.sql +9 -13
  23. package/src/agent/known_store.sql +275 -118
  24. package/src/agent/materializeContext.js +102 -0
  25. package/src/agent/runs.sql +10 -7
  26. package/src/agent/schemes.sql +14 -3
  27. package/src/agent/turns.sql +9 -9
  28. package/src/hooks/HookRegistry.js +6 -5
  29. package/src/hooks/Hooks.js +44 -3
  30. package/src/hooks/PluginContext.js +35 -21
  31. package/src/{server → hooks}/RpcRegistry.js +2 -1
  32. package/src/hooks/RummyContext.js +140 -37
  33. package/src/hooks/ToolRegistry.js +36 -35
  34. package/src/llm/LlmProvider.js +64 -90
  35. package/src/llm/errors.js +21 -0
  36. package/src/plugins/ask_user/README.md +1 -1
  37. package/src/plugins/ask_user/ask_user.js +37 -12
  38. package/src/plugins/ask_user/ask_userDoc.js +2 -23
  39. package/src/plugins/ask_user/ask_userDoc.md +10 -0
  40. package/src/plugins/budget/README.md +27 -23
  41. package/src/plugins/budget/budget.js +261 -69
  42. package/src/plugins/cp/README.md +2 -2
  43. package/src/plugins/cp/cp.js +31 -13
  44. package/src/plugins/cp/cpDoc.js +2 -23
  45. package/src/plugins/cp/cpDoc.md +7 -0
  46. package/src/plugins/engine/README.md +2 -2
  47. package/src/plugins/engine/engine.sql +4 -4
  48. package/src/plugins/engine/turn_context.sql +10 -10
  49. package/src/plugins/env/README.md +20 -5
  50. package/src/plugins/env/env.js +47 -8
  51. package/src/plugins/env/envDoc.js +2 -23
  52. package/src/plugins/env/envDoc.md +13 -0
  53. package/src/plugins/error/README.md +16 -0
  54. package/src/plugins/error/error.js +151 -0
  55. package/src/plugins/file/README.md +6 -6
  56. package/src/plugins/file/file.js +15 -7
  57. package/src/plugins/get/README.md +1 -1
  58. package/src/plugins/get/get.js +125 -49
  59. package/src/plugins/get/getDoc.js +2 -43
  60. package/src/plugins/get/getDoc.md +36 -0
  61. package/src/plugins/hedberg/README.md +1 -2
  62. package/src/plugins/hedberg/hedberg.js +8 -4
  63. package/src/plugins/hedberg/matcher.js +16 -17
  64. package/src/plugins/hedberg/normalize.js +0 -48
  65. package/src/plugins/helpers.js +43 -3
  66. package/src/plugins/index.js +146 -123
  67. package/src/plugins/instructions/README.md +35 -9
  68. package/src/plugins/instructions/instructions.js +126 -12
  69. package/src/plugins/instructions/instructions.md +25 -0
  70. package/src/plugins/instructions/instructions_104.md +7 -0
  71. package/src/plugins/instructions/instructions_105.md +46 -0
  72. package/src/plugins/instructions/instructions_106.md +0 -0
  73. package/src/plugins/instructions/instructions_107.md +0 -0
  74. package/src/plugins/instructions/instructions_108.md +8 -0
  75. package/src/plugins/instructions/protocol.js +12 -0
  76. package/src/plugins/known/README.md +2 -2
  77. package/src/plugins/known/known.js +77 -45
  78. package/src/plugins/known/knownDoc.js +2 -29
  79. package/src/plugins/known/knownDoc.md +8 -0
  80. package/src/plugins/log/README.md +48 -0
  81. package/src/plugins/log/log.js +109 -0
  82. package/src/plugins/mv/README.md +2 -2
  83. package/src/plugins/mv/mv.js +57 -24
  84. package/src/plugins/mv/mvDoc.js +2 -29
  85. package/src/plugins/mv/mvDoc.md +10 -0
  86. package/src/plugins/ollama/README.md +15 -0
  87. package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
  88. package/src/plugins/openai/README.md +17 -0
  89. package/src/plugins/openai/openai.js +120 -0
  90. package/src/plugins/openrouter/README.md +27 -0
  91. package/src/plugins/openrouter/openrouter.js +121 -0
  92. package/src/plugins/persona/README.md +20 -0
  93. package/src/plugins/persona/persona.js +9 -16
  94. package/src/plugins/policy/README.md +21 -0
  95. package/src/plugins/policy/policy.js +29 -14
  96. package/src/plugins/prompt/README.md +1 -1
  97. package/src/plugins/prompt/prompt.js +63 -18
  98. package/src/plugins/rm/README.md +1 -1
  99. package/src/plugins/rm/rm.js +58 -14
  100. package/src/plugins/rm/rmDoc.js +2 -24
  101. package/src/plugins/rm/rmDoc.md +13 -0
  102. package/src/plugins/rpc/README.md +2 -2
  103. package/src/plugins/rpc/rpc.js +515 -296
  104. package/src/plugins/set/README.md +1 -1
  105. package/src/plugins/set/set.js +318 -77
  106. package/src/plugins/set/setDoc.js +2 -35
  107. package/src/plugins/set/setDoc.md +22 -0
  108. package/src/plugins/sh/README.md +28 -5
  109. package/src/plugins/sh/sh.js +52 -8
  110. package/src/plugins/sh/shDoc.js +2 -23
  111. package/src/plugins/sh/shDoc.md +13 -0
  112. package/src/plugins/skill/README.md +23 -0
  113. package/src/plugins/skill/skill.js +14 -17
  114. package/src/plugins/stream/README.md +101 -0
  115. package/src/plugins/stream/stream.js +290 -0
  116. package/src/plugins/telemetry/README.md +1 -1
  117. package/src/plugins/telemetry/telemetry.js +148 -74
  118. package/src/plugins/think/README.md +1 -1
  119. package/src/plugins/think/think.js +14 -1
  120. package/src/plugins/think/thinkDoc.js +2 -17
  121. package/src/plugins/think/thinkDoc.md +7 -0
  122. package/src/plugins/unknown/README.md +3 -3
  123. package/src/plugins/unknown/unknown.js +56 -21
  124. package/src/plugins/unknown/unknownDoc.js +2 -25
  125. package/src/plugins/unknown/unknownDoc.md +11 -0
  126. package/src/plugins/update/README.md +1 -1
  127. package/src/plugins/update/update.js +67 -5
  128. package/src/plugins/update/updateDoc.js +2 -27
  129. package/src/plugins/update/updateDoc.md +8 -0
  130. package/src/plugins/xai/README.md +23 -0
  131. package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
  132. package/src/server/ClientConnection.js +64 -37
  133. package/src/server/SocketServer.js +23 -10
  134. package/src/server/protocol.js +11 -0
  135. package/src/sql/functions/slugify.js +13 -1
  136. package/src/sql/v_model_context.sql +27 -31
  137. package/src/sql/v_run_log.sql +9 -14
  138. package/EXCEPTIONS.md +0 -46
  139. package/src/agent/KnownStore.js +0 -338
  140. package/src/agent/ResponseHealer.js +0 -188
  141. package/src/llm/OpenAiClient.js +0 -100
  142. package/src/llm/OpenRouterClient.js +0 -100
  143. package/src/plugins/budget/recovery.js +0 -47
  144. package/src/plugins/instructions/preamble.md +0 -37
  145. package/src/plugins/performed/README.md +0 -15
  146. package/src/plugins/performed/performed.js +0 -45
  147. package/src/plugins/previous/README.md +0 -16
  148. package/src/plugins/previous/previous.js +0 -60
  149. package/src/plugins/progress/README.md +0 -16
  150. package/src/plugins/progress/progress.js +0 -26
  151. package/src/plugins/summarize/README.md +0 -19
  152. package/src/plugins/summarize/summarize.js +0 -32
  153. package/src/plugins/summarize/summarizeDoc.js +0 -28
@@ -17,11 +17,13 @@ export default class ContextAssembler {
17
17
  } = {},
18
18
  hooks,
19
19
  ) {
20
- // Find loop boundary from active prompt
20
+ // Find loop boundary from active prompt. Absent on turn 1 before
21
+ // the prompt plugin's turn.started handler has run.
21
22
  const promptEntry = rows.findLast(
22
23
  (r) => r.category === "prompt" && r.scheme === "prompt",
23
24
  );
24
- const loopStartTurn = promptEntry?.source_turn ?? 0;
25
+ let loopStartTurn = 0;
26
+ if (promptEntry) loopStartTurn = promptEntry.source_turn;
25
27
 
26
28
  const ctx = {
27
29
  rows,
@@ -0,0 +1,655 @@
1
+ import slugify from "../sql/functions/slugify.js";
2
+ import { PermissionError } from "./errors.js";
3
+
4
+ export default class Entries {
5
+ #db;
6
+ #onChanged;
7
+ #schemes = new Map();
8
+ #schemesLoaded = null;
9
+ #seq = 0;
10
+ #pendingResolutions = new Map();
11
+
12
+ constructor(db, { onChanged = null } = {}) {
13
+ this.#db = db;
14
+ this.#onChanged = onChanged;
15
+ }
16
+
17
+ /**
18
+ * Populate the scheme cache. Can be called explicitly (e.g. at boot
19
+ * after initPlugins finishes) or runs lazily on first need. Idempotent.
20
+ */
21
+ async loadSchemes(db) {
22
+ const rows = await (db || this.#db).get_all_schemes.all();
23
+ this.#schemes.clear();
24
+ for (const row of rows) {
25
+ this.#schemes.set(row.name, row);
26
+ }
27
+ }
28
+
29
+ async #ensureSchemes() {
30
+ if (!this.#schemesLoaded) {
31
+ this.#schemesLoaded = this.loadSchemes();
32
+ }
33
+ return this.#schemesLoaded;
34
+ }
35
+
36
+ #emitChanged(runId, path, changeType) {
37
+ if (this.#onChanged) this.#onChanged({ runId, path, changeType });
38
+ }
39
+
40
+ static scheme(path) {
41
+ const idx = path.indexOf("://");
42
+ return idx > 0 ? path.slice(0, idx) : null;
43
+ }
44
+
45
+ static normalizePath(path) {
46
+ if (!path?.includes("://")) return path;
47
+ const sep = path.indexOf("://");
48
+ const scheme = path.slice(0, sep).toLowerCase();
49
+ const rest = path.slice(sep + 3);
50
+ try {
51
+ // Decode first (idempotent), then encode — but preserve slashes
52
+ const decoded = decodeURIComponent(rest);
53
+ return `${scheme}://${decoded.split("/").map(encodeURIComponent).join("/")}`;
54
+ } catch {
55
+ return `${scheme}://${rest.split("/").map(encodeURIComponent).join("/")}`;
56
+ }
57
+ }
58
+
59
+ async nextTurn(runId) {
60
+ const row = await this.#db.next_turn.get({ run_id: runId });
61
+ return row.turn;
62
+ }
63
+
64
+ async dedup(runId, scheme, target, turn) {
65
+ const encodedTarget = encodeURIComponent(target);
66
+ const turnPrefix = turn ? `turn_${turn}/` : "";
67
+ const candidate = `${scheme}://${turnPrefix}${encodedTarget}`;
68
+ const existing = await this.#db.get_entry_body.get({
69
+ run_id: runId,
70
+ path: candidate,
71
+ });
72
+ if (!existing) return candidate;
73
+ return `${candidate}_${++this.#seq}`;
74
+ }
75
+
76
+ // Log entries share a single namespace at log://turn_N/action/slug.
77
+ // The action segment is the tool/plugin name (set, get, search, update,
78
+ // error, etc.). Target is URL-encoded so slashes and scheme separators
79
+ // survive round-trips.
80
+ async logPath(runId, turn, action, target) {
81
+ const encodedTarget = encodeURIComponent(target);
82
+ const candidate = `log://turn_${turn}/${action}/${encodedTarget}`;
83
+ const existing = await this.#db.get_entry_body.get({
84
+ run_id: runId,
85
+ path: candidate,
86
+ });
87
+ if (!existing) return candidate;
88
+ return `${candidate}_${++this.#seq}`;
89
+ }
90
+
91
+ async slugPath(runId, scheme, content, summary) {
92
+ // Prefer summary, fall back to body content, then empty — slugify
93
+ // handles empty explicitly by returning "" and the caller generates
94
+ // a sequence-only path.
95
+ let source = "";
96
+ if (summary) source = summary;
97
+ else if (content) source = content;
98
+ const base = slugify(source);
99
+ const prefix = `${scheme}://`;
100
+
101
+ if (!base) return `${prefix}${++this.#seq}`;
102
+
103
+ const candidate = `${prefix}${base}`;
104
+ const existing = await this.#db.get_entry_body.get({
105
+ run_id: runId,
106
+ path: candidate,
107
+ });
108
+ if (!existing) return candidate;
109
+
110
+ return `${prefix}${base}_${++this.#seq}`;
111
+ }
112
+
113
+ /**
114
+ * Resolve a scheme's declared scope kind + writer list + category.
115
+ * Unregistered or declaration-less schemes default to run-level +
116
+ * model/plugin writers so ad-hoc paths (e.g. bare filenames) still
117
+ * work.
118
+ */
119
+ async #schemeRules(scheme) {
120
+ await this.#ensureSchemes();
121
+ const row = scheme ? this.#schemes.get(scheme) : null;
122
+ const kind = row?.default_scope ? row.default_scope : "run";
123
+ const category = row?.category ? row.category : "logging";
124
+ let writers = ["model", "plugin"];
125
+ if (row?.writable_by) {
126
+ const parsed =
127
+ typeof row.writable_by === "string"
128
+ ? JSON.parse(row.writable_by)
129
+ : row.writable_by;
130
+ if (Array.isArray(parsed)) writers = parsed;
131
+ }
132
+ return { kind, writers, category };
133
+ }
134
+
135
+ #defaultVisibility(scheme, category) {
136
+ if (scheme === "skill") return "visible";
137
+ if (category === "prompt") return "visible";
138
+ if (category === "unknown") return "visible";
139
+ if (category === "logging") return "visible";
140
+ return "summarized";
141
+ }
142
+
143
+ #resolveScope(kind, runId, projectId) {
144
+ if (kind === "global") return "global";
145
+ if (kind === "project") {
146
+ if (!projectId) {
147
+ throw new Error(
148
+ "project-scoped write requires projectId; caller must pass it to set()",
149
+ );
150
+ }
151
+ return `project:${projectId}`;
152
+ }
153
+ return `run:${runId}`;
154
+ }
155
+
156
+ /**
157
+ * set — create or update an entry. The semantically wide primitive.
158
+ *
159
+ * Modes (selected by which options are present):
160
+ * — write content: body given, state ∈ {proposed,streaming,resolved,failed,cancelled}
161
+ * — change visibility only: visibility given, body omitted
162
+ * — change state only: state given, body omitted (resolve a proposal)
163
+ * — merge attributes: attributes given, body omitted
164
+ * — append to body: append:true (streaming)
165
+ * — pattern match: path contains wildcards or bodyFilter set
166
+ */
167
+ async set({
168
+ runId,
169
+ projectId = null,
170
+ turn = 0,
171
+ path,
172
+ body,
173
+ state,
174
+ visibility,
175
+ outcome = null,
176
+ attributes,
177
+ append,
178
+ bodyFilter = null,
179
+ pattern,
180
+ hash = null,
181
+ loopId = null,
182
+ writer = "plugin",
183
+ }) {
184
+ if (!runId) throw new Error("set: runId is required");
185
+ if (!path) throw new Error("set: path is required");
186
+
187
+ // Pattern mode is explicit (pattern: true) or implicit when a
188
+ // body filter is supplied. The literal `*` character can appear
189
+ // inside legitimate exact paths (e.g. rm://foo%2F* as a result
190
+ // path for an rm against a pattern); we don't infer pattern mode
191
+ // from the path alone.
192
+ const isPattern = pattern === true || bodyFilter !== null;
193
+
194
+ // Pattern mode: update matching entries (visibility / body / both).
195
+ if (isPattern) {
196
+ if (body != null && !append) {
197
+ await this.#db.update_body_by_pattern.run({
198
+ run_id: runId,
199
+ path,
200
+ body: bodyFilter,
201
+ new_body: body,
202
+ });
203
+ await this.#db.bump_write_count_by_pattern.run({
204
+ run_id: runId,
205
+ path,
206
+ body: bodyFilter,
207
+ });
208
+ this.#emitChanged(runId, path, "body");
209
+ }
210
+ if (visibility === "visible") {
211
+ await this.#db.promote_by_pattern.run({
212
+ run_id: runId,
213
+ path,
214
+ body: bodyFilter,
215
+ turn,
216
+ });
217
+ this.#emitChanged(runId, path, "promote");
218
+ } else if (visibility === "summarized" || visibility === "archived") {
219
+ await this.#db.demote_by_pattern.run({
220
+ run_id: runId,
221
+ path,
222
+ body: bodyFilter,
223
+ });
224
+ this.#emitChanged(runId, path, "demote");
225
+ }
226
+ return;
227
+ }
228
+
229
+ const normalized = Entries.normalizePath(path);
230
+ const scheme = Entries.scheme(normalized);
231
+
232
+ // Append mode: streaming body growth on an existing entry.
233
+ if (append) {
234
+ if (body == null) throw new Error("set: append requires body");
235
+ await this.#db.append_entry_body.run({
236
+ run_id: runId,
237
+ path: normalized,
238
+ chunk: body,
239
+ });
240
+ this.#emitChanged(runId, normalized, "append");
241
+ return;
242
+ }
243
+
244
+ // Body-less state or visibility change on an existing entry.
245
+ if (body == null) {
246
+ if (state != null) {
247
+ await this.#db.resolve_known_entry_view.run({
248
+ run_id: runId,
249
+ path: normalized,
250
+ state,
251
+ outcome,
252
+ });
253
+ this.#emitChanged(runId, normalized, "resolve");
254
+ this.#drainPendingResolution(runId, normalized);
255
+ }
256
+ if (visibility != null) {
257
+ await this.#db.set_visibility.run({
258
+ run_id: runId,
259
+ path: normalized,
260
+ visibility,
261
+ });
262
+ this.#emitChanged(runId, normalized, "visibility");
263
+ }
264
+ if (attributes != null) {
265
+ await this.#db.update_entry_attributes.run({
266
+ run_id: runId,
267
+ path: normalized,
268
+ attributes: JSON.stringify(attributes),
269
+ });
270
+ this.#emitChanged(runId, normalized, "attributes");
271
+ }
272
+ return;
273
+ }
274
+
275
+ // Full write/upsert: body + state + visibility + attributes.
276
+ const { kind, writers, category } = await this.#schemeRules(scheme);
277
+ if (!writers.includes(writer)) {
278
+ throw new PermissionError(scheme, writer, writers);
279
+ }
280
+ const scope = this.#resolveScope(kind, runId, projectId);
281
+ // Log entries self-describe via `action` so consumers (renderer,
282
+ // client UIs, tests) can read the action without parsing the
283
+ // path. Only inject `action` when the caller passes attributes
284
+ // — a null `attributes` means "don't touch existing" and the
285
+ // SQL's COALESCE handles preservation on UPDATE. If we generated
286
+ // `{action: m[1]}` for every null-attributes log write, every
287
+ // body-only update to a log entry would clobber existing attrs
288
+ // (command, summary, demotedCount, ...).
289
+ const effectiveAttributes = attributes ? { ...attributes } : null;
290
+ if (scheme === "log" && effectiveAttributes) {
291
+ const m = normalized.match(/^log:\/\/turn_\d+\/([^/]+)\//);
292
+ if (m) effectiveAttributes.action = m[1];
293
+ }
294
+ const entry = await this.#db.upsert_entry.get({
295
+ scope,
296
+ path: normalized,
297
+ body,
298
+ attributes: effectiveAttributes
299
+ ? JSON.stringify(effectiveAttributes)
300
+ : null,
301
+ hash,
302
+ });
303
+ const effectiveState = state === undefined ? "resolved" : state;
304
+ const effectiveVisibility =
305
+ visibility === undefined
306
+ ? this.#defaultVisibility(scheme, category)
307
+ : visibility;
308
+ await this.#db.upsert_run_view.run({
309
+ run_id: runId,
310
+ entry_id: entry.id,
311
+ loop_id: loopId,
312
+ turn,
313
+ state: effectiveState,
314
+ outcome,
315
+ visibility: effectiveVisibility,
316
+ });
317
+ this.#emitChanged(runId, normalized, "upsert");
318
+ if (effectiveState !== "proposed") {
319
+ this.#drainPendingResolution(runId, normalized);
320
+ }
321
+ }
322
+
323
+ /**
324
+ * get — promote entry(ies) to visible visibility. Default visibility is
325
+ * "visible"; pass visibility explicitly for a read-with-side-effect at
326
+ * a different visibility (rare).
327
+ */
328
+ async get({
329
+ runId,
330
+ turn = 0,
331
+ path,
332
+ bodyFilter = null,
333
+ visibility = "visible",
334
+ }) {
335
+ if (!runId) throw new Error("get: runId is required");
336
+ if (!path) throw new Error("get: path is required");
337
+ if (visibility === "visible") {
338
+ await this.#db.promote_by_pattern.run({
339
+ run_id: runId,
340
+ path,
341
+ body: bodyFilter,
342
+ turn,
343
+ });
344
+ } else {
345
+ await this.#db.demote_by_pattern.run({
346
+ run_id: runId,
347
+ path,
348
+ body: bodyFilter,
349
+ });
350
+ }
351
+ this.#emitChanged(runId, path, "promote");
352
+ }
353
+
354
+ /**
355
+ * rm — remove entry view(s). Matches single path or pattern; optional
356
+ * bodyFilter narrows pattern matches. `filesOnly` restricts to bare
357
+ * file-scheme entries (scheme IS NULL).
358
+ */
359
+ async rm({ runId, path, bodyFilter = null, filesOnly = false }) {
360
+ if (!runId) throw new Error("rm: runId is required");
361
+ if (!path) throw new Error("rm: path is required");
362
+ if (filesOnly) {
363
+ await this.#db.delete_file_entries_by_pattern.run({
364
+ run_id: runId,
365
+ pattern: path,
366
+ });
367
+ } else if (bodyFilter !== null || /[*?[\]]/.test(path)) {
368
+ await this.#db.delete_entries_by_pattern.run({
369
+ run_id: runId,
370
+ path,
371
+ body: bodyFilter,
372
+ });
373
+ } else {
374
+ const normalized = Entries.normalizePath(path);
375
+ await this.#db.delete_known_entry.run({
376
+ run_id: runId,
377
+ path: normalized,
378
+ });
379
+ }
380
+ this.#emitChanged(runId, path, "remove");
381
+ }
382
+
383
+ /**
384
+ * cp — copy an entry to a new path. Source body becomes new body;
385
+ * source view unchanged.
386
+ */
387
+ async cp({
388
+ runId,
389
+ turn = 0,
390
+ from,
391
+ to,
392
+ visibility,
393
+ attributes,
394
+ loopId,
395
+ writer,
396
+ }) {
397
+ if (!runId) throw new Error("cp: runId is required");
398
+ if (!from || !to) throw new Error("cp: from and to are required");
399
+ const sourceBody = await this.getBody(runId, from);
400
+ if (sourceBody === null) return;
401
+ await this.set({
402
+ runId,
403
+ turn,
404
+ path: to,
405
+ body: sourceBody,
406
+ visibility,
407
+ attributes,
408
+ loopId,
409
+ writer,
410
+ });
411
+ }
412
+
413
+ /**
414
+ * mv — rename an entry. Equivalent to cp + rm on source.
415
+ */
416
+ async mv({
417
+ runId,
418
+ turn = 0,
419
+ from,
420
+ to,
421
+ visibility,
422
+ attributes,
423
+ loopId,
424
+ writer,
425
+ }) {
426
+ if (!runId) throw new Error("mv: runId is required");
427
+ if (!from || !to) throw new Error("mv: from and to are required");
428
+ await this.cp({
429
+ runId,
430
+ turn,
431
+ from,
432
+ to,
433
+ visibility,
434
+ attributes,
435
+ loopId,
436
+ writer,
437
+ });
438
+ await this.rm({ runId, path: from });
439
+ }
440
+
441
+ /**
442
+ * update — once-per-turn lifecycle signal from the model (or plugin
443
+ * speaking on its behalf). Writes to update://<slug> with body as the
444
+ * content and attributes.status carrying the model's continuation code
445
+ * (102 continue, 200/204 terminal, 422 can't-answer). Returns the
446
+ * slug path.
447
+ */
448
+ async update({
449
+ runId,
450
+ turn = 0,
451
+ body,
452
+ status = 102,
453
+ attributes = {},
454
+ loopId = null,
455
+ writer = "plugin",
456
+ }) {
457
+ if (!runId) throw new Error("update: runId is required");
458
+ if (body == null) throw new Error("update: body is required");
459
+ const path = await this.logPath(runId, turn, "update", body);
460
+ await this.set({
461
+ runId,
462
+ turn,
463
+ path,
464
+ body,
465
+ state: "resolved",
466
+ loopId,
467
+ writer,
468
+ attributes: { status, ...attributes },
469
+ });
470
+ return path;
471
+ }
472
+
473
+ async getEntriesByPattern(
474
+ runId,
475
+ path,
476
+ body = null,
477
+ { limit = null, offset = null } = {},
478
+ ) {
479
+ return this.#db.get_entries_by_pattern.all({
480
+ run_id: runId,
481
+ path,
482
+ body: body ? body : null,
483
+ limit,
484
+ offset,
485
+ });
486
+ }
487
+
488
+ #drainPendingResolution(runId, normalized) {
489
+ const key = `${runId}:${normalized}`;
490
+ const resolver = this.#pendingResolutions.get(key);
491
+ if (resolver) {
492
+ this.#pendingResolutions.delete(key);
493
+ resolver();
494
+ }
495
+ }
496
+
497
+ waitForResolution(runId, path) {
498
+ const normalized = Entries.normalizePath(path);
499
+ const key = `${runId}:${normalized}`;
500
+ return new Promise((resolve) => {
501
+ this.#pendingResolutions.set(key, resolve);
502
+ });
503
+ }
504
+
505
+ async getLog(runId) {
506
+ return this.#db.get_results.all({ run_id: runId });
507
+ }
508
+
509
+ async getEntries(runId) {
510
+ return this.#db.get_known_entries.all({ run_id: runId });
511
+ }
512
+
513
+ async getFileEntries(runId) {
514
+ return this.#db.get_file_entries.all({ run_id: runId });
515
+ }
516
+
517
+ async getFileStatesByPattern(runId, pattern) {
518
+ return this.#db.get_file_states_by_pattern.all({ run_id: runId, pattern });
519
+ }
520
+
521
+ async hasRejections(runId, loopId) {
522
+ const row = await this.#db.has_rejections.get({
523
+ run_id: runId,
524
+ loop_id: loopId,
525
+ });
526
+ return row.count > 0;
527
+ }
528
+
529
+ async hasAcceptedActions(runId) {
530
+ const row = await this.#db.has_accepted_actions.get({ run_id: runId });
531
+ return row.count > 0;
532
+ }
533
+
534
+ async getUnresolved(runId) {
535
+ return this.#db.get_unresolved.all({ run_id: runId });
536
+ }
537
+
538
+ async countUnknowns(runId) {
539
+ const row = await this.#db.count_unknowns.get({ run_id: runId });
540
+ return row.count;
541
+ }
542
+
543
+ async getUnknownValues(runId) {
544
+ const rows = await this.#db.get_unknown_values.all({ run_id: runId });
545
+ return new Set(rows.map((r) => r.body));
546
+ }
547
+
548
+ /**
549
+ * Unknown entries for a run, in DB order. Rows include path + body.
550
+ */
551
+ async getUnknowns(runId) {
552
+ return this.#db.get_unknowns.all({ run_id: runId });
553
+ }
554
+
555
+ async forkEntries(parentRunId, childRunId) {
556
+ await this.#db.fork_known_entries.run({
557
+ new_run_id: childRunId,
558
+ parent_run_id: parentRunId,
559
+ });
560
+ }
561
+
562
+ /**
563
+ * Demote all promoted entries for a run on a given turn. Returns the
564
+ * affected rows (path, tokens) so callers can summarize.
565
+ *
566
+ * Implemented as SELECT-then-UPDATE because SQLite's RETURNING doesn't
567
+ * support the cross-table lookup needed to report content paths/tokens
568
+ * from the view-layer update.
569
+ */
570
+ async demoteTurnEntries(runId, turn) {
571
+ const targets = await this.#db.get_turn_demotion_targets.all({
572
+ run_id: runId,
573
+ turn,
574
+ });
575
+ await this.#db.demote_turn_entries.run({ run_id: runId, turn });
576
+ return targets;
577
+ }
578
+
579
+ /**
580
+ * Demote every currently-visible entry in a run. Used by budget
581
+ * postDispatch as the fallback when this-turn demotion finds nothing
582
+ * and the packet still overflows — left-over promotions from prior
583
+ * turns the model didn't demote themselves. Returns the affected
584
+ * rows (path, tokens, turn) ordered oldest promotion first so the
585
+ * error body can name them.
586
+ */
587
+ async demoteRunVisibleEntries(runId) {
588
+ const targets = await this.#db.get_run_visible_targets.all({
589
+ run_id: runId,
590
+ });
591
+ await this.#db.demote_run_visible.run({ run_id: runId });
592
+ return targets;
593
+ }
594
+
595
+ /**
596
+ * Run metadata lookup. Exposed here so plugins don't reach into
597
+ * core.db for run-scoped lookups.
598
+ */
599
+ async getRun(runId) {
600
+ return this.#db.get_run_by_id.get({ id: runId });
601
+ }
602
+
603
+ /**
604
+ * Turn-level usage stats write (telemetry). Same rationale as getRun.
605
+ */
606
+ async updateTurnStats(stats) {
607
+ return this.#db.update_turn_stats.run(stats);
608
+ }
609
+
610
+ async getBody(runId, path) {
611
+ const row = await this.#db.get_entry_body.get({
612
+ run_id: runId,
613
+ path: Entries.normalizePath(path),
614
+ });
615
+ if (!row) return null;
616
+ return row.body;
617
+ }
618
+
619
+ async setAttributes(runId, path, attrs) {
620
+ const normalized = Entries.normalizePath(path);
621
+ await this.#db.update_entry_attributes.run({
622
+ run_id: runId,
623
+ path: normalized,
624
+ attributes: JSON.stringify(attrs),
625
+ });
626
+ this.#emitChanged(runId, normalized, "attributes");
627
+ }
628
+
629
+ async getState(runId, path) {
630
+ return this.#db.get_entry_state.get({
631
+ run_id: runId,
632
+ path: Entries.normalizePath(path),
633
+ });
634
+ }
635
+
636
+ async getAttributes(runId, path) {
637
+ const row = await this.#db.get_entry_attributes.get({
638
+ run_id: runId,
639
+ path: Entries.normalizePath(path),
640
+ });
641
+ return row?.attributes ? JSON.parse(row.attributes) : null;
642
+ }
643
+
644
+ async getTurnAudit(runId, turn) {
645
+ return this.#db.get_turn_audit.all({ run_id: runId, turn });
646
+ }
647
+
648
+ static toolFromPath(path) {
649
+ return Entries.scheme(path);
650
+ }
651
+
652
+ static isSystemPath(path) {
653
+ return path.includes("://");
654
+ }
655
+ }