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