@possumtech/rummy 0.5.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/.env.example +42 -5
  2. package/PLUGINS.md +389 -194
  3. package/README.md +25 -8
  4. package/SPEC.md +934 -373
  5. package/bin/demo.js +166 -0
  6. package/bin/rummy.js +9 -3
  7. package/biome/no-fallbacks.grit +50 -0
  8. package/lang/en.json +2 -2
  9. package/migrations/001_initial_schema.sql +88 -37
  10. package/package.json +13 -11
  11. package/scriptify/ask_run.js +77 -0
  12. package/service.js +50 -9
  13. package/src/agent/AgentLoop.js +476 -335
  14. package/src/agent/ContextAssembler.js +4 -4
  15. package/src/agent/Entries.js +676 -0
  16. package/src/agent/ProjectAgent.js +30 -18
  17. package/src/agent/TurnExecutor.js +232 -421
  18. package/src/agent/XmlParser.js +99 -33
  19. package/src/agent/budget.js +56 -0
  20. package/src/agent/errors.js +22 -0
  21. package/src/agent/httpStatus.js +39 -0
  22. package/src/agent/known_checks.sql +8 -4
  23. package/src/agent/known_queries.sql +9 -13
  24. package/src/agent/known_store.sql +280 -125
  25. package/src/agent/materializeContext.js +104 -0
  26. package/src/agent/runs.sql +29 -7
  27. package/src/agent/schemes.sql +14 -3
  28. package/src/agent/tokens.js +6 -0
  29. package/src/agent/turns.sql +9 -9
  30. package/src/hooks/HookRegistry.js +6 -5
  31. package/src/hooks/Hooks.js +44 -3
  32. package/src/hooks/PluginContext.js +29 -21
  33. package/src/{server → hooks}/RpcRegistry.js +2 -1
  34. package/src/hooks/RummyContext.js +139 -35
  35. package/src/hooks/ToolRegistry.js +21 -16
  36. package/src/llm/LlmProvider.js +66 -89
  37. package/src/llm/errors.js +21 -0
  38. package/src/llm/retry.js +63 -0
  39. package/src/plugins/ask_user/README.md +1 -1
  40. package/src/plugins/ask_user/ask_user.js +37 -12
  41. package/src/plugins/ask_user/ask_userDoc.js +2 -25
  42. package/src/plugins/ask_user/ask_userDoc.md +10 -0
  43. package/src/plugins/budget/README.md +27 -25
  44. package/src/plugins/budget/budget.js +306 -88
  45. package/src/plugins/cp/README.md +2 -2
  46. package/src/plugins/cp/cp.js +29 -11
  47. package/src/plugins/cp/cpDoc.js +2 -15
  48. package/src/plugins/cp/cpDoc.md +7 -0
  49. package/src/plugins/engine/README.md +2 -2
  50. package/src/plugins/engine/engine.sql +4 -4
  51. package/src/plugins/engine/turn_context.sql +10 -10
  52. package/src/plugins/env/README.md +20 -5
  53. package/src/plugins/env/env.js +45 -6
  54. package/src/plugins/env/envDoc.js +2 -23
  55. package/src/plugins/env/envDoc.md +13 -0
  56. package/src/plugins/error/README.md +16 -0
  57. package/src/plugins/error/error.js +151 -0
  58. package/src/plugins/file/README.md +6 -6
  59. package/src/plugins/file/file.js +15 -2
  60. package/src/plugins/get/README.md +1 -1
  61. package/src/plugins/get/get.js +103 -48
  62. package/src/plugins/get/getDoc.js +2 -32
  63. package/src/plugins/get/getDoc.md +36 -0
  64. package/src/plugins/hedberg/README.md +1 -2
  65. package/src/plugins/hedberg/hedberg.js +8 -4
  66. package/src/plugins/hedberg/matcher.js +16 -17
  67. package/src/plugins/hedberg/normalize.js +0 -48
  68. package/src/plugins/helpers.js +42 -2
  69. package/src/plugins/index.js +146 -123
  70. package/src/plugins/instructions/README.md +35 -9
  71. package/src/plugins/instructions/instructions.js +244 -9
  72. package/src/plugins/instructions/instructions.md +33 -0
  73. package/src/plugins/instructions/instructions_104.md +7 -0
  74. package/src/plugins/instructions/instructions_105.md +38 -0
  75. package/src/plugins/instructions/instructions_106.md +21 -0
  76. package/src/plugins/instructions/instructions_107.md +10 -0
  77. package/src/plugins/instructions/instructions_108.md +0 -0
  78. package/src/plugins/instructions/protocol.js +12 -0
  79. package/src/plugins/known/README.md +2 -2
  80. package/src/plugins/known/known.js +68 -36
  81. package/src/plugins/known/knownDoc.js +2 -17
  82. package/src/plugins/known/knownDoc.md +8 -0
  83. package/src/plugins/log/README.md +48 -0
  84. package/src/plugins/log/log.js +129 -0
  85. package/src/plugins/mv/README.md +2 -2
  86. package/src/plugins/mv/mv.js +55 -22
  87. package/src/plugins/mv/mvDoc.js +2 -18
  88. package/src/plugins/mv/mvDoc.md +10 -0
  89. package/src/plugins/ollama/README.md +15 -0
  90. package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
  91. package/src/plugins/openai/README.md +17 -0
  92. package/src/plugins/openai/openai.js +120 -0
  93. package/src/plugins/openrouter/README.md +27 -0
  94. package/src/plugins/openrouter/openrouter.js +121 -0
  95. package/src/plugins/persona/README.md +20 -0
  96. package/src/plugins/persona/persona.js +9 -16
  97. package/src/plugins/policy/README.md +21 -0
  98. package/src/plugins/policy/policy.js +29 -14
  99. package/src/plugins/prompt/README.md +1 -1
  100. package/src/plugins/prompt/prompt.js +64 -16
  101. package/src/plugins/rm/README.md +1 -1
  102. package/src/plugins/rm/rm.js +56 -12
  103. package/src/plugins/rm/rmDoc.js +2 -20
  104. package/src/plugins/rm/rmDoc.md +13 -0
  105. package/src/plugins/rpc/README.md +2 -2
  106. package/src/plugins/rpc/rpc.js +525 -296
  107. package/src/plugins/set/README.md +1 -1
  108. package/src/plugins/set/set.js +318 -75
  109. package/src/plugins/set/setDoc.js +2 -35
  110. package/src/plugins/set/setDoc.md +22 -0
  111. package/src/plugins/sh/README.md +28 -5
  112. package/src/plugins/sh/sh.js +50 -6
  113. package/src/plugins/sh/shDoc.js +2 -23
  114. package/src/plugins/sh/shDoc.md +13 -0
  115. package/src/plugins/skill/README.md +23 -0
  116. package/src/plugins/skill/skill.js +14 -18
  117. package/src/plugins/stream/README.md +101 -0
  118. package/src/plugins/stream/stream.js +290 -0
  119. package/src/plugins/telemetry/README.md +1 -1
  120. package/src/plugins/telemetry/telemetry.js +129 -80
  121. package/src/plugins/think/README.md +1 -1
  122. package/src/plugins/think/think.js +12 -0
  123. package/src/plugins/think/thinkDoc.js +2 -15
  124. package/src/plugins/think/thinkDoc.md +7 -0
  125. package/src/plugins/unknown/README.md +3 -3
  126. package/src/plugins/unknown/unknown.js +47 -19
  127. package/src/plugins/unknown/unknownDoc.js +2 -21
  128. package/src/plugins/unknown/unknownDoc.md +11 -0
  129. package/src/plugins/update/README.md +1 -1
  130. package/src/plugins/update/update.js +83 -5
  131. package/src/plugins/update/updateDoc.js +2 -30
  132. package/src/plugins/update/updateDoc.md +8 -0
  133. package/src/plugins/xai/README.md +23 -0
  134. package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
  135. package/src/plugins/yolo/yolo.js +192 -0
  136. package/src/server/ClientConnection.js +64 -37
  137. package/src/server/SocketServer.js +23 -10
  138. package/src/server/protocol.js +11 -0
  139. package/src/sql/v_model_context.sql +27 -31
  140. package/src/sql/v_run_log.sql +9 -14
  141. package/EXCEPTIONS.md +0 -46
  142. package/FIDELITY_CONTRACT.md +0 -172
  143. package/src/agent/KnownStore.js +0 -337
  144. package/src/agent/ResponseHealer.js +0 -241
  145. package/src/llm/OpenAiClient.js +0 -100
  146. package/src/llm/OpenRouterClient.js +0 -100
  147. package/src/plugins/budget/recovery.js +0 -47
  148. package/src/plugins/instructions/preamble.md +0 -45
  149. package/src/plugins/performed/README.md +0 -15
  150. package/src/plugins/performed/performed.js +0 -45
  151. package/src/plugins/previous/README.md +0 -16
  152. package/src/plugins/previous/previous.js +0 -56
  153. package/src/plugins/progress/README.md +0 -16
  154. package/src/plugins/progress/progress.js +0 -43
  155. package/src/plugins/summarize/README.md +0 -19
  156. package/src/plugins/summarize/summarize.js +0 -32
  157. package/src/plugins/summarize/summarizeDoc.js +0 -27
@@ -1,196 +1,351 @@
1
- -- PREP: upsert_known_entry
2
- INSERT INTO known_entries (
3
- run_id, loop_id, turn, path, body, status, fidelity, hash
4
- , attributes, tokens, updated_at
1
+ -- PREP: upsert_entry
2
+ -- Content-layer upsert. Returns entry id for the subsequent run_view write.
3
+ -- Null :attributes on UPDATE path means "don't touch existing attributes"
4
+ -- — so UPDATE reads :attributes directly, not excluded.attributes (which
5
+ -- would have been coerced to '{}' by the VALUES clause).
6
+ INSERT INTO entries (
7
+ scope, path, body, attributes, hash, updated_at
5
8
  )
6
9
  VALUES (
7
- :run_id, :loop_id, :turn, :path, :body, :status, :fidelity, :hash
8
- , COALESCE(:attributes, '{}')
9
- , countTokens(:body)
10
- , COALESCE(:updated_at, CURRENT_TIMESTAMP)
10
+ :scope, :path, :body, COALESCE(:attributes, '{}'), :hash
11
+ , CURRENT_TIMESTAMP
11
12
  )
12
- ON CONFLICT (run_id, path) DO UPDATE SET
13
+ ON CONFLICT (scope, path) DO UPDATE SET
13
14
  body = excluded.body
14
- , status = excluded.status
15
- , fidelity = excluded.fidelity
16
- , hash = COALESCE(excluded.hash, known_entries.hash)
17
- , attributes = COALESCE(excluded.attributes, known_entries.attributes)
18
- , loop_id = excluded.loop_id
15
+ , attributes = COALESCE(:attributes, entries.attributes)
16
+ , hash = COALESCE(:hash, entries.hash)
17
+ , updated_at = CURRENT_TIMESTAMP
18
+ RETURNING id;
19
+
20
+ -- PREP: upsert_run_view
21
+ -- View-layer upsert. Called after upsert_entry with the returned entry id.
22
+ INSERT INTO run_views (
23
+ run_id, entry_id, loop_id, turn, state, outcome, visibility, updated_at
24
+ )
25
+ VALUES (
26
+ :run_id, :entry_id, :loop_id, :turn, :state, :outcome, :visibility
27
+ , CURRENT_TIMESTAMP
28
+ )
29
+ ON CONFLICT (run_id, entry_id) DO UPDATE SET
30
+ loop_id = excluded.loop_id
19
31
  , turn = excluded.turn
20
- , tokens = countTokens(excluded.body)
21
- , write_count = known_entries.write_count + 1
22
- , updated_at = COALESCE(excluded.updated_at, CURRENT_TIMESTAMP);
23
-
24
- -- PREP: recount_tokens
25
- UPDATE known_entries
26
- SET tokens = :tokens
27
- WHERE run_id = :run_id AND path = :path;
28
-
29
- -- PREP: get_stale_tokens
30
- SELECT path, body
31
- FROM known_entries
32
- WHERE
33
- run_id = :run_id
34
- AND turn = :turn;
32
+ , state = excluded.state
33
+ , outcome = excluded.outcome
34
+ , visibility = excluded.visibility
35
+ , write_count = run_views.write_count + 1
36
+ , updated_at = CURRENT_TIMESTAMP;
37
+
38
+ -- Helper fragment: "the entry this run's view references at this path".
39
+ -- Every UPDATE/DELETE resolves its target this way so the logic is
40
+ -- correct whether the entry lives in the run's own scope or a shared one.
41
+
42
+ -- PREP: append_entry_body
43
+ -- Streaming entry body growth. Appends a chunk to the existing body.
44
+ UPDATE entries
45
+ SET
46
+ body = body || :chunk
47
+ , updated_at = CURRENT_TIMESTAMP
48
+ WHERE id = (
49
+ SELECT e.id FROM entries AS e
50
+ JOIN run_views AS rv ON rv.entry_id = e.id
51
+ WHERE rv.run_id = :run_id AND e.path = :path
52
+ LIMIT 1
53
+ );
35
54
 
36
55
  -- PREP: delete_known_entry
37
- DELETE FROM known_entries
38
- WHERE run_id = :run_id AND path = :path;
56
+ -- Removes the view only. Entry is left for future GC; may be shared.
57
+ DELETE FROM run_views
58
+ WHERE run_id = :run_id AND entry_id = (
59
+ SELECT e.id FROM entries AS e
60
+ JOIN run_views AS rv ON rv.entry_id = e.id
61
+ WHERE rv.run_id = :run_id AND e.path = :path
62
+ LIMIT 1
63
+ );
39
64
 
40
65
  -- PREP: delete_file_entries_by_pattern
41
- DELETE FROM known_entries
42
- WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL;
66
+ DELETE FROM run_views
67
+ WHERE run_id = :run_id AND entry_id IN (
68
+ SELECT e.id FROM entries AS e
69
+ JOIN run_views AS rv ON rv.entry_id = e.id
70
+ WHERE
71
+ rv.run_id = :run_id
72
+ AND hedmatch(:pattern, e.path)
73
+ AND e.scheme IS NULL
74
+ );
75
+
76
+ -- PREP: resolve_known_entry_view
77
+ UPDATE run_views
78
+ SET
79
+ state = :state
80
+ , outcome = :outcome
81
+ , updated_at = CURRENT_TIMESTAMP
82
+ WHERE run_id = :run_id AND entry_id = (
83
+ SELECT e.id FROM entries AS e
84
+ JOIN run_views AS rv ON rv.entry_id = e.id
85
+ WHERE rv.run_id = :run_id AND e.path = :path
86
+ LIMIT 1
87
+ );
43
88
 
44
- -- PREP: resolve_known_entry
45
- UPDATE known_entries
89
+ -- PREP: resolve_known_entry_body
90
+ UPDATE entries
46
91
  SET
47
- status = :status
48
- , body = :body
92
+ body = :body
49
93
  , updated_at = CURRENT_TIMESTAMP
50
- WHERE run_id = :run_id AND path = :path;
94
+ WHERE id = (
95
+ SELECT e.id FROM entries AS e
96
+ JOIN run_views AS rv ON rv.entry_id = e.id
97
+ WHERE rv.run_id = :run_id AND e.path = :path
98
+ LIMIT 1
99
+ );
51
100
 
52
- -- PREP: set_file_fidelity
53
- UPDATE known_entries
101
+ -- PREP: set_file_visibility
102
+ UPDATE run_views
54
103
  SET
55
- fidelity = :fidelity
104
+ visibility = :visibility
56
105
  , updated_at = CURRENT_TIMESTAMP
57
- WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL;
106
+ WHERE run_id = :run_id AND entry_id IN (
107
+ SELECT e.id FROM entries AS e
108
+ JOIN run_views AS rv ON rv.entry_id = e.id
109
+ WHERE
110
+ rv.run_id = :run_id
111
+ AND hedmatch(:pattern, e.path)
112
+ AND e.scheme IS NULL
113
+ );
58
114
 
59
115
  -- PREP: promote_path
60
- UPDATE known_entries
116
+ UPDATE run_views
61
117
  SET
62
- fidelity = 'promoted'
63
- , status = 200
118
+ visibility = 'visible'
119
+ , state = 'resolved'
120
+ , outcome = NULL
64
121
  , turn = :turn
65
122
  , updated_at = CURRENT_TIMESTAMP
66
- WHERE run_id = :run_id AND path = :path;
123
+ WHERE run_id = :run_id AND entry_id = (
124
+ SELECT e.id FROM entries AS e
125
+ JOIN run_views AS rv ON rv.entry_id = e.id
126
+ WHERE rv.run_id = :run_id AND e.path = :path
127
+ LIMIT 1
128
+ );
67
129
 
68
130
  -- PREP: demote_path
69
- UPDATE known_entries
131
+ UPDATE run_views
70
132
  SET
71
- fidelity = 'archived'
133
+ visibility = 'archived'
72
134
  , updated_at = CURRENT_TIMESTAMP
73
- WHERE run_id = :run_id AND path = :path;
135
+ WHERE run_id = :run_id AND entry_id = (
136
+ SELECT e.id FROM entries AS e
137
+ JOIN run_views AS rv ON rv.entry_id = e.id
138
+ WHERE rv.run_id = :run_id AND e.path = :path
139
+ LIMIT 1
140
+ );
74
141
 
75
- -- PREP: set_fidelity
76
- -- Tokens unchanged — always reflects full body cost.
77
- UPDATE known_entries
142
+ -- PREP: set_visibility
143
+ UPDATE run_views
78
144
  SET
79
- fidelity = :fidelity
145
+ visibility = :visibility
80
146
  , updated_at = CURRENT_TIMESTAMP
81
- WHERE run_id = :run_id AND path = :path;
147
+ WHERE run_id = :run_id AND entry_id = (
148
+ SELECT e.id FROM entries AS e
149
+ JOIN run_views AS rv ON rv.entry_id = e.id
150
+ WHERE rv.run_id = :run_id AND e.path = :path
151
+ LIMIT 1
152
+ );
82
153
 
83
154
  -- PREP: get_entry_body
84
- SELECT body
85
- FROM known_entries
86
- WHERE run_id = :run_id AND path = :path;
155
+ SELECT e.body AS body
156
+ FROM run_views AS rv
157
+ JOIN entries AS e ON e.id = rv.entry_id
158
+ WHERE rv.run_id = :run_id AND e.path = :path;
87
159
 
88
160
  -- PREP: get_entry_state
89
- SELECT status, fidelity, scheme, turn
90
- FROM known_entries
91
- WHERE run_id = :run_id AND path = :path;
161
+ SELECT rv.state, rv.outcome, rv.visibility, e.scheme, rv.turn
162
+ FROM run_views AS rv
163
+ JOIN entries AS e ON e.id = rv.entry_id
164
+ WHERE rv.run_id = :run_id AND e.path = :path;
92
165
 
93
166
  -- PREP: get_file_states_by_pattern
94
- SELECT path, status, fidelity, turn
95
- FROM known_entries
96
- WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL
97
- ORDER BY path;
167
+ SELECT e.path, rv.state, rv.outcome, rv.visibility, rv.turn
168
+ FROM run_views AS rv
169
+ JOIN entries AS e ON e.id = rv.entry_id
170
+ WHERE
171
+ rv.run_id = :run_id
172
+ AND hedmatch(:pattern, e.path)
173
+ AND e.scheme IS NULL
174
+ ORDER BY e.path;
98
175
 
99
176
  -- PREP: update_entry_attributes
100
- UPDATE known_entries
177
+ UPDATE entries
101
178
  SET
102
179
  attributes = json_patch(attributes, :attributes)
103
180
  , updated_at = CURRENT_TIMESTAMP
104
- WHERE run_id = :run_id AND path = :path;
181
+ WHERE id = (
182
+ SELECT e.id FROM entries AS e
183
+ JOIN run_views AS rv ON rv.entry_id = e.id
184
+ WHERE rv.run_id = :run_id AND e.path = :path
185
+ LIMIT 1
186
+ );
105
187
 
106
188
  -- PREP: get_entry_attributes
107
- SELECT attributes
108
- FROM known_entries
109
- WHERE run_id = :run_id AND path = :path;
189
+ SELECT e.attributes AS attributes
190
+ FROM run_views AS rv
191
+ JOIN entries AS e ON e.id = rv.entry_id
192
+ WHERE rv.run_id = :run_id AND e.path = :path;
110
193
 
111
194
  -- PREP: promote_by_pattern
112
- UPDATE known_entries
195
+ UPDATE run_views
113
196
  SET
114
- fidelity = 'promoted'
115
- , status = 200
197
+ visibility = 'visible'
198
+ , state = 'resolved'
199
+ , outcome = NULL
116
200
  , turn = :turn
117
201
  , updated_at = CURRENT_TIMESTAMP
118
- WHERE
119
- run_id = :run_id
120
- AND hedmatch(:path, path)
121
- AND (:body IS NULL OR hedsearch(:body, body));
202
+ WHERE run_id = :run_id AND entry_id IN (
203
+ SELECT e.id FROM entries AS e
204
+ JOIN run_views AS rv ON rv.entry_id = e.id
205
+ WHERE
206
+ rv.run_id = :run_id
207
+ AND hedmatch(:path, e.path)
208
+ AND (:body IS NULL OR hedsearch(:body, e.body))
209
+ );
122
210
 
123
211
  -- PREP: demote_by_pattern
124
- UPDATE known_entries
212
+ UPDATE run_views
125
213
  SET
126
- fidelity = 'archived'
214
+ visibility = 'archived'
127
215
  , updated_at = CURRENT_TIMESTAMP
128
- WHERE
129
- run_id = :run_id
130
- AND hedmatch(:path, path)
131
- AND (:body IS NULL OR hedsearch(:body, body));
216
+ WHERE run_id = :run_id AND entry_id IN (
217
+ SELECT e.id FROM entries AS e
218
+ JOIN run_views AS rv ON rv.entry_id = e.id
219
+ WHERE
220
+ rv.run_id = :run_id
221
+ AND hedmatch(:path, e.path)
222
+ AND (:body IS NULL OR hedsearch(:body, e.body))
223
+ );
132
224
 
133
225
  -- PREP: get_entries_by_pattern
134
- SELECT path, body, scheme, status, fidelity, tokens, attributes
135
- FROM known_entries
226
+ -- Default excludes audit schemes (system://, reasoning://, model://, user://,
227
+ -- assistant://, content://, instructions://) so model-facing tools never leak
228
+ -- internal entries. Internal callers that need them pass include_audit_schemes=1.
229
+ SELECT
230
+ e.path, e.body, e.scheme, rv.state, rv.outcome, rv.visibility
231
+ , countTokens(e.body) AS tokens, e.attributes
232
+ FROM run_views AS rv
233
+ JOIN entries AS e ON e.id = rv.entry_id
234
+ JOIN schemes AS s ON s.name = COALESCE(e.scheme, 'file')
136
235
  WHERE
137
- run_id = :run_id
138
- AND hedmatch(:path, path)
139
- AND (:body IS NULL OR hedsearch(:body, body))
140
- ORDER BY path
236
+ rv.run_id = :run_id
237
+ AND hedmatch(:path, e.path)
238
+ AND (:body IS NULL OR hedsearch(:body, e.body))
239
+ AND (:include_audit_schemes IS NOT NULL OR s.model_visible = 1)
240
+ ORDER BY e.path
141
241
  LIMIT
142
242
  COALESCE(:limit, -1)
143
- OFFSET
144
- COALESCE(:offset, 0);
243
+ OFFSET COALESCE(:offset, 0);
145
244
 
146
245
  -- PREP: delete_entries_by_pattern
147
- DELETE FROM known_entries
148
- WHERE
149
- run_id = :run_id
150
- AND hedmatch(:path, path)
151
- AND (:body IS NULL OR hedsearch(:body, body));
246
+ DELETE FROM run_views
247
+ WHERE run_id = :run_id AND entry_id IN (
248
+ SELECT e.id FROM entries AS e
249
+ JOIN run_views AS rv ON rv.entry_id = e.id
250
+ WHERE
251
+ rv.run_id = :run_id
252
+ AND hedmatch(:path, e.path)
253
+ AND (:body IS NULL OR hedsearch(:body, e.body))
254
+ );
152
255
 
153
256
  -- PREP: update_body_by_pattern
154
- UPDATE known_entries
257
+ UPDATE entries
155
258
  SET
156
259
  body = :new_body
157
- , tokens = countTokens(:new_body)
158
- , write_count = write_count + 1
159
260
  , updated_at = CURRENT_TIMESTAMP
160
- WHERE
161
- run_id = :run_id
162
- AND hedmatch(:path, path)
163
- AND (:body IS NULL OR hedsearch(:body, body));
164
-
165
- -- PREP: restore_summarized_prompts
166
- -- Restore prompt entries demoted by a recovery phase that was
167
- -- interrupted (e.g. server crash). Safe to call unconditionally at loop
168
- -- start: if the full prompt would overflow, Prompt Demotion handles it.
169
- UPDATE known_entries
261
+ WHERE id IN (
262
+ SELECT e.id FROM entries AS e
263
+ JOIN run_views AS rv ON rv.entry_id = e.id
264
+ WHERE
265
+ rv.run_id = :run_id
266
+ AND hedmatch(:path, e.path)
267
+ AND (:body IS NULL OR hedsearch(:body, e.body))
268
+ );
269
+
270
+ -- PREP: bump_write_count_by_pattern
271
+ -- Companion to update_body_by_pattern. write_count lives on run_views.
272
+ UPDATE run_views
170
273
  SET
171
- fidelity = 'promoted'
274
+ write_count = write_count + 1
172
275
  , updated_at = CURRENT_TIMESTAMP
173
- WHERE run_id = :run_id AND scheme = 'prompt' AND fidelity = 'demoted';
276
+ WHERE run_id = :run_id AND entry_id IN (
277
+ SELECT e.id FROM entries AS e
278
+ JOIN run_views AS rv ON rv.entry_id = e.id
279
+ WHERE
280
+ rv.run_id = :run_id
281
+ AND hedmatch(:path, e.path)
282
+ AND (:body IS NULL OR hedsearch(:body, e.body))
283
+ );
284
+
285
+ -- PREP: get_turn_demotion_targets
286
+ -- Rows that demote_turn_entries is about to flip. Return shape
287
+ -- matches the old RETURNING (path, tokens) for caller compatibility.
288
+ -- State filter: skip failed/cancelled entries (they're already not
289
+ -- contributing visible context — demoting them would be misleading).
290
+ -- Scheme filter: skip known/unknown — these are the model's deliverables,
291
+ -- not housekeeping. Auto-demoting just-created knowns punishes the
292
+ -- correct Distill+Demote pattern.
293
+ SELECT e.path, countTokens(e.body) AS tokens
294
+ FROM run_views AS rv
295
+ JOIN entries AS e ON e.id = rv.entry_id
296
+ WHERE
297
+ rv.run_id = :run_id
298
+ AND rv.turn = :turn
299
+ AND rv.visibility = 'visible'
300
+ AND rv.state NOT IN ('failed', 'cancelled')
301
+ AND e.scheme NOT IN ('known', 'unknown');
174
302
 
175
303
  -- PREP: demote_turn_entries
176
- -- Demote all promoted entries from a turn for budget claw-back.
177
- -- Action schemes (set/rm/mv/cp) at status 200 keep their status — those
178
- -- represent committed side effects (files written/removed) that can't be
179
- -- clawed back; only the body in context is demoted, not the truth of what
180
- -- happened. Everything else flips to 413 since promotion was reversed.
181
- -- Tokens unchanged — always reports full cost regardless of fidelity.
182
- UPDATE known_entries
304
+ -- View-layer only visibility lives on run_views. State untouched.
305
+ -- Call get_turn_demotion_targets first if you need the list of what
306
+ -- was demoted (used by budget plugin for the overflow error body).
307
+ -- Scheme filter mirrors get_turn_demotion_targets never demote the
308
+ -- model's deliverables (known/unknown) along with housekeeping.
309
+ UPDATE run_views
183
310
  SET
184
- fidelity = 'demoted'
185
- , status = CASE
186
- WHEN scheme IN ('set', 'rm', 'mv', 'cp') AND status = 200 THEN 200
187
- ELSE 413
188
- END
311
+ visibility = 'summarized'
189
312
  , updated_at = CURRENT_TIMESTAMP
190
313
  WHERE
191
314
  run_id = :run_id
192
315
  AND turn = :turn
193
- AND fidelity = 'promoted'
194
- AND status < 400
195
- RETURNING path, tokens;
316
+ AND visibility = 'visible'
317
+ AND state NOT IN ('failed', 'cancelled')
318
+ AND NOT EXISTS (
319
+ SELECT 1
320
+ FROM entries AS e
321
+ WHERE
322
+ e.id = run_views.entry_id
323
+ AND e.scheme IN ('known', 'unknown')
324
+ );
325
+
326
+ -- PREP: get_run_visible_targets
327
+ -- All visible entries across the run, oldest promotion first. Used by
328
+ -- budget postDispatch as the fallback demotion set when this-turn
329
+ -- demotion yields nothing but the packet still overflows (promotions
330
+ -- from prior turns the model forgot to demote themselves).
331
+ SELECT e.path, countTokens(e.body) AS tokens, rv.turn
332
+ FROM run_views AS rv
333
+ JOIN entries AS e ON e.id = rv.entry_id
334
+ WHERE
335
+ rv.run_id = :run_id
336
+ AND rv.visibility = 'visible'
337
+ AND rv.state NOT IN ('failed', 'cancelled')
338
+ ORDER BY rv.turn, e.id;
196
339
 
340
+ -- PREP: demote_run_visible
341
+ -- Broad cross-turn demotion. Separate prep from demote_turn_entries
342
+ -- so the caller's intent (surgical this-turn vs fallback all-visible)
343
+ -- stays explicit.
344
+ UPDATE run_views
345
+ SET
346
+ visibility = 'summarized'
347
+ , updated_at = CURRENT_TIMESTAMP
348
+ WHERE
349
+ run_id = :run_id
350
+ AND visibility = 'visible'
351
+ AND state NOT IN ('failed', 'cancelled');
@@ -0,0 +1,104 @@
1
+ import ContextAssembler from "./ContextAssembler.js";
2
+ import { countLines, countTokens } from "./tokens.js";
3
+
4
+ /**
5
+ * Rebuild turn_context from v_model_context, then assemble messages.
6
+ * Called at turn start and again by the budget plugin when it needs a
7
+ * fresh measurement after mutating visibility.
8
+ */
9
+ export default async function materializeContext({
10
+ db,
11
+ hooks,
12
+ runId,
13
+ loopId,
14
+ turn,
15
+ systemPrompt,
16
+ mode,
17
+ toolSet,
18
+ contextSize,
19
+ demoted,
20
+ }) {
21
+ await db.clear_turn_context.run({ run_id: runId, turn });
22
+ const viewRows = await db.get_model_context.all({ run_id: runId });
23
+ // Per-entry token accounting (see SPEC @token_accounting): captured
24
+ // here while we still have the raw body, then merged onto rows after
25
+ // the read-back roundtrip through turn_context.
26
+ const tokenAccounting = new Map();
27
+ for (const row of viewRows) {
28
+ // schemeOf() yields NULL (or "") for bare file paths — translate
29
+ // to "file" so the view lookup finds the file scheme handler.
30
+ const scheme = row.scheme ? row.scheme : "file";
31
+ const attrs = row.attributes ? JSON.parse(row.attributes) : null;
32
+ // Log entries live at log://turn_N/action/slug. Dispatch projection
33
+ // to the action plugin's view (set, update, search, etc.) by
34
+ // extracting the action segment from the path.
35
+ let projectionKey = scheme;
36
+ if (scheme === "log") {
37
+ const m = row.path.match(/^log:\/\/turn_\d+\/([^/]+)\//);
38
+ if (m) projectionKey = m[1];
39
+ }
40
+ const baseEntry = {
41
+ path: row.path,
42
+ scheme,
43
+ body: row.body,
44
+ attributes: attrs,
45
+ category: row.category,
46
+ };
47
+ const visibleProjection = await hooks.tools.view(projectionKey, {
48
+ ...baseEntry,
49
+ visibility: "visible",
50
+ });
51
+ const summarizedProjection = await hooks.tools.view(projectionKey, {
52
+ ...baseEntry,
53
+ visibility: "summarized",
54
+ });
55
+ const vTokens = countTokens(visibleProjection);
56
+ const sTokens = countTokens(summarizedProjection);
57
+ const vLines = countLines(visibleProjection);
58
+ tokenAccounting.set(row.path, { vTokens, sTokens, vLines });
59
+ const projectedBody =
60
+ row.visibility === "visible" ? visibleProjection : summarizedProjection;
61
+ await db.insert_turn_context.run({
62
+ run_id: runId,
63
+ loop_id: loopId,
64
+ turn,
65
+ ordinal: row.ordinal,
66
+ path: row.path,
67
+ visibility: row.visibility,
68
+ state: row.state,
69
+ outcome: row.outcome,
70
+ body: projectedBody,
71
+ attributes: row.attributes,
72
+ category: row.category,
73
+ source_turn: row.turn,
74
+ });
75
+ }
76
+ const rows = await db.get_turn_context.all({ run_id: runId, turn });
77
+ for (const row of rows) {
78
+ const t = tokenAccounting.get(row.path);
79
+ if (!t) continue;
80
+ row.vTokens = t.vTokens;
81
+ row.sTokens = t.sTokens;
82
+ row.aTokens = t.vTokens - t.sTokens;
83
+ row.vLines = t.vLines;
84
+ }
85
+ const lastCtx = await db.get_last_context_tokens.get({ run_id: runId });
86
+ // First turn of a new run has no prior context.
87
+ let lastContextTokens = 0;
88
+ if (lastCtx) lastContextTokens = lastCtx.context_tokens;
89
+
90
+ const messages = await ContextAssembler.assembleFromTurnContext(
91
+ rows,
92
+ {
93
+ type: mode,
94
+ systemPrompt,
95
+ contextSize,
96
+ demoted,
97
+ toolSet,
98
+ lastContextTokens,
99
+ turn,
100
+ },
101
+ hooks,
102
+ );
103
+ return { rows, messages, lastContextTokens };
104
+ }
@@ -44,7 +44,7 @@ SELECT
44
44
  FROM known_entries AS ke
45
45
  WHERE
46
46
  ke.run_id = r.id
47
- AND ke.scheme = 'summarize'
47
+ AND ke.path LIKE 'log://turn_%/update/%'
48
48
  ORDER BY ke.id DESC
49
49
  LIMIT 1
50
50
  ) AS summary
@@ -79,16 +79,38 @@ WHERE id = :run_id
79
79
  RETURNING next_turn - 1 AS turn;
80
80
 
81
81
  -- PREP: fork_known_entries
82
- INSERT INTO known_entries (
83
- run_id, loop_id, turn, path, body, status, fidelity
84
- , hash, attributes, tokens, refs, write_count
82
+ -- Cheap fork: copy only view rows. Entries stay shared between parent
83
+ -- and child. Child's subsequent writes diverge via upsert into a new
84
+ -- run-scoped entry.
85
+ INSERT INTO run_views (
86
+ run_id, entry_id, loop_id, turn, state, outcome, visibility
87
+ , write_count, refs
85
88
  )
86
89
  SELECT
87
- :new_run_id, NULL, turn, path, body, status, fidelity
88
- , hash, attributes, tokens, refs, write_count
89
- FROM known_entries
90
+ :new_run_id, entry_id, NULL, turn, state, outcome, visibility
91
+ , write_count, refs
92
+ FROM run_views
90
93
  WHERE run_id = :parent_run_id;
91
94
 
95
+ -- PREP: archive_prior_prompt_artifacts
96
+ -- Multi-prompt sessions accumulate artifacts from prior prompt cycles
97
+ -- (consumed prompts, their per-turn logs). These pollute the validator's
98
+ -- prior-prompts check on subsequent Deployment landings. Archive all
99
+ -- prior prompt:// entries and prior-turn log:// entries when a new
100
+ -- prompt arrives. Knowns/unknowns/file entries are untouched — they
101
+ -- carry persistent knowledge across cycles. The loop_id IS NULL clause
102
+ -- catches forked-in views from a parent run (per fork_known_entries),
103
+ -- which represent prior cycles' artifacts inherited into a clean child.
104
+ UPDATE run_views
105
+ SET visibility = 'archived'
106
+ WHERE run_id = :run_id
107
+ AND visibility != 'archived'
108
+ AND (turn < :current_turn OR loop_id IS NULL)
109
+ AND entry_id IN (
110
+ SELECT id FROM entries
111
+ WHERE scheme IN ('prompt', 'log')
112
+ );
113
+
92
114
  -- PREP: get_active_runs
93
115
  SELECT r.id
94
116
  FROM runs AS r
@@ -1,6 +1,17 @@
1
1
  -- PREP: upsert_scheme
2
- INSERT OR REPLACE INTO schemes (name, model_visible, category)
3
- VALUES (:name, :model_visible, :category);
2
+ INSERT INTO schemes (name, model_visible, category, default_scope, writable_by)
3
+ VALUES (
4
+ :name
5
+ , :model_visible
6
+ , :category
7
+ , COALESCE(:default_scope, 'run')
8
+ , COALESCE(:writable_by, '["model","plugin"]')
9
+ )
10
+ ON CONFLICT (name) DO UPDATE SET
11
+ model_visible = excluded.model_visible
12
+ , category = excluded.category
13
+ , default_scope = excluded.default_scope
14
+ , writable_by = excluded.writable_by;
4
15
 
5
16
  -- PREP: get_all_schemes
6
- SELECT name, model_visible, category FROM schemes;
17
+ SELECT name, model_visible, category, default_scope, writable_by FROM schemes;
@@ -12,3 +12,9 @@ export function countTokens(text) {
12
12
  if (!text) return 0;
13
13
  return Math.ceil(text.length / DIVISOR);
14
14
  }
15
+
16
+ export function countLines(text) {
17
+ if (!text) return 0;
18
+ const newlines = (text.match(/\n/g) || []).length;
19
+ return text.endsWith("\n") ? newlines : newlines + 1;
20
+ }