@plurnk/plurnk-service 0.7.0 → 0.9.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 (270) hide show
  1. package/SPEC.md +116 -83
  2. package/bin/plurnk-service.ts +132 -0
  3. package/dist/Paths.d.ts +8 -0
  4. package/dist/Paths.d.ts.map +1 -0
  5. package/dist/Paths.js +47 -0
  6. package/dist/Paths.js.map +1 -0
  7. package/dist/content/index.d.ts +9 -0
  8. package/dist/content/index.d.ts.map +1 -0
  9. package/dist/content/index.js +10 -0
  10. package/dist/content/index.js.map +1 -0
  11. package/dist/content/line-marker.d.ts +26 -0
  12. package/dist/content/line-marker.d.ts.map +1 -0
  13. package/dist/content/line-marker.js +323 -0
  14. package/dist/content/line-marker.js.map +1 -0
  15. package/dist/content/matcher.d.ts +15 -0
  16. package/dist/content/matcher.d.ts.map +1 -0
  17. package/dist/content/matcher.js +112 -0
  18. package/dist/content/matcher.js.map +1 -0
  19. package/dist/content/mimetype-binary.d.ts +9 -0
  20. package/dist/content/mimetype-binary.d.ts.map +1 -0
  21. package/dist/content/mimetype-binary.js +86 -0
  22. package/dist/content/mimetype-binary.js.map +1 -0
  23. package/dist/content/path-mimetype.d.ts +6 -0
  24. package/dist/content/path-mimetype.d.ts.map +1 -0
  25. package/dist/content/path-mimetype.js +49 -0
  26. package/dist/content/path-mimetype.js.map +1 -0
  27. package/dist/content/read-resolve.d.ts +20 -0
  28. package/dist/content/read-resolve.d.ts.map +1 -0
  29. package/dist/content/read-resolve.js +60 -0
  30. package/dist/content/read-resolve.js.map +1 -0
  31. package/dist/core/ChannelWrite.d.ts +35 -30
  32. package/dist/core/ChannelWrite.d.ts.map +1 -1
  33. package/dist/core/ChannelWrite.js +49 -41
  34. package/dist/core/ChannelWrite.js.map +1 -1
  35. package/dist/core/Engine.d.ts +16 -10
  36. package/dist/core/Engine.d.ts.map +1 -1
  37. package/dist/core/Engine.js +309 -115
  38. package/dist/core/Engine.js.map +1 -1
  39. package/dist/core/EnvFlags.d.ts +6 -3
  40. package/dist/core/EnvFlags.d.ts.map +1 -1
  41. package/dist/core/EnvFlags.js +62 -60
  42. package/dist/core/EnvFlags.js.map +1 -1
  43. package/dist/core/ExecutorRegistry.d.ts +26 -0
  44. package/dist/core/ExecutorRegistry.d.ts.map +1 -0
  45. package/dist/core/ExecutorRegistry.js +99 -0
  46. package/dist/core/ExecutorRegistry.js.map +1 -0
  47. package/dist/core/PluginLoader.d.ts +6 -3
  48. package/dist/core/PluginLoader.d.ts.map +1 -1
  49. package/dist/core/PluginLoader.js +77 -73
  50. package/dist/core/PluginLoader.js.map +1 -1
  51. package/dist/core/ProviderInstantiate.d.ts +4 -2
  52. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  53. package/dist/core/ProviderInstantiate.js +23 -22
  54. package/dist/core/ProviderInstantiate.js.map +1 -1
  55. package/dist/core/SchemeRegistry.d.ts +1 -1
  56. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  57. package/dist/core/SchemeRegistry.js +3 -3
  58. package/dist/core/SchemeRegistry.js.map +1 -1
  59. package/dist/core/git-membership.d.ts +8 -0
  60. package/dist/core/git-membership.d.ts.map +1 -0
  61. package/dist/core/git-membership.js +125 -0
  62. package/dist/core/git-membership.js.map +1 -0
  63. package/dist/core/packet-wire.d.ts +47 -6
  64. package/dist/core/packet-wire.d.ts.map +1 -1
  65. package/dist/core/packet-wire.js +376 -312
  66. package/dist/core/packet-wire.js.map +1 -1
  67. package/dist/core/resolveForLoop.d.ts +16 -0
  68. package/dist/core/resolveForLoop.d.ts.map +1 -0
  69. package/dist/core/resolveForLoop.js +34 -0
  70. package/dist/core/resolveForLoop.js.map +1 -0
  71. package/dist/core/results.d.ts +40 -0
  72. package/dist/core/results.d.ts.map +1 -0
  73. package/dist/core/results.js +52 -0
  74. package/dist/core/results.js.map +1 -0
  75. package/dist/core/scheme-types.d.ts +7 -4
  76. package/dist/core/scheme-types.d.ts.map +1 -1
  77. package/dist/core/scheme-types.js +4 -4
  78. package/dist/core/scheme-types.js.map +1 -1
  79. package/dist/core/types.d.ts +26 -0
  80. package/dist/core/types.d.ts.map +1 -0
  81. package/dist/core/types.js +17 -0
  82. package/dist/core/types.js.map +1 -0
  83. package/dist/index.d.ts +1 -6
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +4 -43
  86. package/dist/index.js.map +1 -1
  87. package/dist/schemes/EffectPolicy.d.ts +6 -0
  88. package/dist/schemes/EffectPolicy.d.ts.map +1 -0
  89. package/dist/schemes/EffectPolicy.js +18 -0
  90. package/dist/schemes/EffectPolicy.js.map +1 -0
  91. package/dist/schemes/Exec.d.ts +1 -0
  92. package/dist/schemes/Exec.d.ts.map +1 -1
  93. package/dist/schemes/Exec.js +86 -45
  94. package/dist/schemes/Exec.js.map +1 -1
  95. package/dist/schemes/File.d.ts +0 -1
  96. package/dist/schemes/File.d.ts.map +1 -1
  97. package/dist/schemes/File.js +32 -69
  98. package/dist/schemes/File.js.map +1 -1
  99. package/dist/schemes/Known.d.ts.map +1 -1
  100. package/dist/schemes/Known.js +13 -13
  101. package/dist/schemes/Known.js.map +1 -1
  102. package/dist/schemes/Log.d.ts.map +1 -1
  103. package/dist/schemes/Log.js +8 -52
  104. package/dist/schemes/Log.js.map +1 -1
  105. package/dist/schemes/Plurnk.d.ts.map +1 -1
  106. package/dist/schemes/Plurnk.js +13 -13
  107. package/dist/schemes/Plurnk.js.map +1 -1
  108. package/dist/schemes/Skill.d.ts.map +1 -1
  109. package/dist/schemes/Skill.js +13 -13
  110. package/dist/schemes/Skill.js.map +1 -1
  111. package/dist/schemes/Unknown.d.ts.map +1 -1
  112. package/dist/schemes/Unknown.js +13 -13
  113. package/dist/schemes/Unknown.js.map +1 -1
  114. package/dist/schemes/_entry-crud.d.ts +5 -3
  115. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  116. package/dist/schemes/_entry-crud.js +55 -50
  117. package/dist/schemes/_entry-crud.js.map +1 -1
  118. package/dist/schemes/_entry-find.d.ts +10 -3
  119. package/dist/schemes/_entry-find.d.ts.map +1 -1
  120. package/dist/schemes/_entry-find.js +99 -77
  121. package/dist/schemes/_entry-find.js.map +1 -1
  122. package/dist/schemes/_entry-manifest.d.ts +6 -0
  123. package/dist/schemes/_entry-manifest.d.ts.map +1 -0
  124. package/dist/schemes/_entry-manifest.js +45 -0
  125. package/dist/schemes/_entry-manifest.js.map +1 -0
  126. package/dist/schemes/_entry-ops.d.ts +7 -4
  127. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  128. package/dist/schemes/_entry-ops.js +198 -316
  129. package/dist/schemes/_entry-ops.js.map +1 -1
  130. package/dist/schemes/_entry-send.d.ts +4 -1
  131. package/dist/schemes/_entry-send.d.ts.map +1 -1
  132. package/dist/schemes/_entry-send.js +57 -55
  133. package/dist/schemes/_entry-send.js.map +1 -1
  134. package/dist/server/ClientConnection.js +3 -3
  135. package/dist/server/ClientConnection.js.map +1 -1
  136. package/dist/server/Daemon.d.ts +5 -5
  137. package/dist/server/Daemon.d.ts.map +1 -1
  138. package/dist/server/Daemon.js +234 -176
  139. package/dist/server/Daemon.js.map +1 -1
  140. package/dist/server/clientTurn.d.ts +4 -1
  141. package/dist/server/clientTurn.d.ts.map +1 -1
  142. package/dist/server/clientTurn.js +19 -17
  143. package/dist/server/clientTurn.js.map +1 -1
  144. package/dist/server/dsl.d.ts +19 -16
  145. package/dist/server/dsl.d.ts.map +1 -1
  146. package/dist/server/dsl.js +127 -105
  147. package/dist/server/dsl.js.map +1 -1
  148. package/dist/server/envelope.d.ts +22 -19
  149. package/dist/server/envelope.d.ts.map +1 -1
  150. package/dist/server/envelope.js +116 -102
  151. package/dist/server/envelope.js.map +1 -1
  152. package/dist/server/logEntry.d.ts +4 -1
  153. package/dist/server/logEntry.d.ts.map +1 -1
  154. package/dist/server/logEntry.js +41 -39
  155. package/dist/server/logEntry.js.map +1 -1
  156. package/dist/server/methods/_dispatchAsClient.d.ts +3 -1
  157. package/dist/server/methods/_dispatchAsClient.d.ts.map +1 -1
  158. package/dist/server/methods/_dispatchAsClient.js +31 -29
  159. package/dist/server/methods/_dispatchAsClient.js.map +1 -1
  160. package/dist/server/methods/discover.d.ts +3 -1
  161. package/dist/server/methods/discover.d.ts.map +1 -1
  162. package/dist/server/methods/discover.js +8 -6
  163. package/dist/server/methods/discover.js.map +1 -1
  164. package/dist/server/methods/entry_read.d.ts +4 -1
  165. package/dist/server/methods/entry_read.d.ts.map +1 -1
  166. package/dist/server/methods/entry_read.js +54 -52
  167. package/dist/server/methods/entry_read.js.map +1 -1
  168. package/dist/server/methods/log_read.d.ts +4 -1
  169. package/dist/server/methods/log_read.d.ts.map +1 -1
  170. package/dist/server/methods/log_read.js +38 -36
  171. package/dist/server/methods/log_read.js.map +1 -1
  172. package/dist/server/methods/loop_cancel.d.ts +3 -1
  173. package/dist/server/methods/loop_cancel.d.ts.map +1 -1
  174. package/dist/server/methods/loop_cancel.js +19 -17
  175. package/dist/server/methods/loop_cancel.js.map +1 -1
  176. package/dist/server/methods/loop_resolve.d.ts +3 -1
  177. package/dist/server/methods/loop_resolve.d.ts.map +1 -1
  178. package/dist/server/methods/loop_resolve.js +42 -40
  179. package/dist/server/methods/loop_resolve.js.map +1 -1
  180. package/dist/server/methods/loop_run.d.ts +3 -1
  181. package/dist/server/methods/loop_run.d.ts.map +1 -1
  182. package/dist/server/methods/loop_run.js +104 -102
  183. package/dist/server/methods/loop_run.js.map +1 -1
  184. package/dist/server/methods/op_copy.d.ts +3 -1
  185. package/dist/server/methods/op_copy.d.ts.map +1 -1
  186. package/dist/server/methods/op_copy.js +25 -23
  187. package/dist/server/methods/op_copy.js.map +1 -1
  188. package/dist/server/methods/op_dispatch.d.ts +3 -1
  189. package/dist/server/methods/op_dispatch.d.ts.map +1 -1
  190. package/dist/server/methods/op_dispatch.js +18 -16
  191. package/dist/server/methods/op_dispatch.js.map +1 -1
  192. package/dist/server/methods/op_edit.d.ts +3 -1
  193. package/dist/server/methods/op_edit.d.ts.map +1 -1
  194. package/dist/server/methods/op_edit.js +23 -21
  195. package/dist/server/methods/op_edit.js.map +1 -1
  196. package/dist/server/methods/op_exec.d.ts +3 -1
  197. package/dist/server/methods/op_exec.d.ts.map +1 -1
  198. package/dist/server/methods/op_exec.js +20 -18
  199. package/dist/server/methods/op_exec.js.map +1 -1
  200. package/dist/server/methods/op_find.d.ts +3 -1
  201. package/dist/server/methods/op_find.d.ts.map +1 -1
  202. package/dist/server/methods/op_find.js +23 -21
  203. package/dist/server/methods/op_find.js.map +1 -1
  204. package/dist/server/methods/op_hide.d.ts +3 -1
  205. package/dist/server/methods/op_hide.d.ts.map +1 -1
  206. package/dist/server/methods/op_hide.js +23 -21
  207. package/dist/server/methods/op_hide.js.map +1 -1
  208. package/dist/server/methods/op_move.d.ts +3 -1
  209. package/dist/server/methods/op_move.d.ts.map +1 -1
  210. package/dist/server/methods/op_move.js +23 -21
  211. package/dist/server/methods/op_move.js.map +1 -1
  212. package/dist/server/methods/op_parse.d.ts +3 -1
  213. package/dist/server/methods/op_parse.d.ts.map +1 -1
  214. package/dist/server/methods/op_parse.js +24 -22
  215. package/dist/server/methods/op_parse.js.map +1 -1
  216. package/dist/server/methods/op_read.d.ts +3 -1
  217. package/dist/server/methods/op_read.d.ts.map +1 -1
  218. package/dist/server/methods/op_read.js +23 -21
  219. package/dist/server/methods/op_read.js.map +1 -1
  220. package/dist/server/methods/op_send.d.ts +3 -1
  221. package/dist/server/methods/op_send.d.ts.map +1 -1
  222. package/dist/server/methods/op_send.js +22 -20
  223. package/dist/server/methods/op_send.js.map +1 -1
  224. package/dist/server/methods/op_show.d.ts +3 -1
  225. package/dist/server/methods/op_show.d.ts.map +1 -1
  226. package/dist/server/methods/op_show.js +23 -21
  227. package/dist/server/methods/op_show.js.map +1 -1
  228. package/dist/server/methods/ping.d.ts +3 -1
  229. package/dist/server/methods/ping.d.ts.map +1 -1
  230. package/dist/server/methods/ping.js +8 -6
  231. package/dist/server/methods/ping.js.map +1 -1
  232. package/dist/server/methods/providers_list.d.ts +3 -1
  233. package/dist/server/methods/providers_list.d.ts.map +1 -1
  234. package/dist/server/methods/providers_list.js +19 -17
  235. package/dist/server/methods/providers_list.js.map +1 -1
  236. package/dist/server/methods/session_attach.d.ts +3 -1
  237. package/dist/server/methods/session_attach.d.ts.map +1 -1
  238. package/dist/server/methods/session_attach.js +43 -41
  239. package/dist/server/methods/session_attach.js.map +1 -1
  240. package/dist/server/methods/session_create.d.ts +3 -1
  241. package/dist/server/methods/session_create.d.ts.map +1 -1
  242. package/dist/server/methods/session_create.js +51 -49
  243. package/dist/server/methods/session_create.js.map +1 -1
  244. package/dist/server/methods/session_list.d.ts +3 -1
  245. package/dist/server/methods/session_list.d.ts.map +1 -1
  246. package/dist/server/methods/session_list.js +9 -7
  247. package/dist/server/methods/session_list.js.map +1 -1
  248. package/dist/server/methods/session_runs.d.ts +3 -1
  249. package/dist/server/methods/session_runs.d.ts.map +1 -1
  250. package/dist/server/methods/session_runs.js +19 -17
  251. package/dist/server/methods/session_runs.js.map +1 -1
  252. package/dist/server/methods/session_set_persona.d.ts +3 -1
  253. package/dist/server/methods/session_set_persona.d.ts.map +1 -1
  254. package/dist/server/methods/session_set_persona.js +28 -26
  255. package/dist/server/methods/session_set_persona.js.map +1 -1
  256. package/dist/server/methods/session_set_root.d.ts +3 -1
  257. package/dist/server/methods/session_set_root.d.ts.map +1 -1
  258. package/dist/server/methods/session_set_root.js +31 -29
  259. package/dist/server/methods/session_set_root.js.map +1 -1
  260. package/dist/server/noProposals.d.ts +6 -0
  261. package/dist/server/noProposals.d.ts.map +1 -0
  262. package/dist/server/noProposals.js +37 -0
  263. package/dist/server/noProposals.js.map +1 -0
  264. package/dist/server/yolo.d.ts +3 -1
  265. package/dist/server/yolo.d.ts.map +1 -1
  266. package/dist/server/yolo.js +15 -13
  267. package/dist/server/yolo.js.map +1 -1
  268. package/package.json +71 -32
  269. package/bin/plurnk-service.js +0 -112
  270. /package/migrations/{001_schema.sql → 0000-00-00.01_schema.sql} +0 -0
@@ -1,18 +1,27 @@
1
+ var _a;
1
2
  import { PlurnkParser, PlurnkParseError } from "@plurnk/plurnk-grammar";
2
3
  import { Mimetypes, emptyRegistry } from "@plurnk/plurnk-mimetypes";
3
- import { writeEntry } from "../schemes/_entry-crud.js";
4
+ import EntryCrud from "../schemes/_entry-crud.js";
5
+ import EntryManifest from "../schemes/_entry-manifest.js";
6
+ import GitMembership from "./git-membership.js";
4
7
  import { DEFAULT_LOOP_FLAGS } from "./scheme-types.js";
5
- import { sliceLinesRaw, isBinaryMimetype } from "@plurnk/plurnk-schemes";
6
- // Plain JS module shared with bin/digest.js so wire projection and
7
- // digest projection are structurally one function. tsconfig.build.json
8
- // has allowJs:true so this gets copied through to dist/.
9
- import { packetToWireMessages } from "./packet-wire.js";
8
+ import { LineMarkerOps, MimetypeBinary } from "../content/index.js";
9
+ // Shared module imported by both Engine and bin/digest.ts, so wire
10
+ // projection and digest projection are structurally one function — no
11
+ // drift between wire and digest possible.
12
+ import PacketWire from "./packet-wire.js";
10
13
  // SPEC §3.6: writer must be in target scheme's manifest.writableBy.
11
14
  // SHOW/HIDE/READ/FIND are not gated — they touch visibility metadata or read.
12
15
  const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC"]);
13
16
  const DEFAULT_PREVIEW_BUDGET = 256;
14
17
  const DEFAULT_MAX_STRIKES = 3;
15
18
  const DEFAULT_MAX_COMMANDS = 99;
19
+ const DEFAULT_BUDGET_CEILING = 0.9;
20
+ // Substituted into the budget readout after the assembled packet is measured
21
+ // (the figure depends on the packet's own rendered size — chicken/egg).
22
+ const TOKENS_FREE_PLACEHOLDER = "{{tokensFree}}";
23
+ const TOKEN_USAGE_PLACEHOLDER = "{{tokenUsage}}";
24
+ const TOKEN_PERCENT_PLACEHOLDER = "{{tokenPercent}}";
16
25
  const readBudget = () => {
17
26
  const raw = process.env.PLURNK_ENTRY_SIZE_DEFAULT_TOKENS;
18
27
  if (raw === undefined || raw.length === 0)
@@ -22,6 +31,18 @@ const readBudget = () => {
22
31
  return DEFAULT_PREVIEW_BUDGET;
23
32
  return n;
24
33
  };
34
+ // PLURNK_BUDGET_CEILING is dual-mode: <=1 is a fraction of the provider's
35
+ // context window, >1 is an absolute token wall — lets a demo pin a tiny
36
+ // ceiling regardless of the model's real window to force the grinder.
37
+ const readCeiling = () => {
38
+ const raw = process.env.PLURNK_BUDGET_CEILING;
39
+ if (raw === undefined || raw.length === 0)
40
+ return DEFAULT_BUDGET_CEILING;
41
+ const n = Number.parseFloat(raw);
42
+ if (!Number.isFinite(n) || n <= 0)
43
+ return DEFAULT_BUDGET_CEILING;
44
+ return n;
45
+ };
25
46
  const readMaxStrikes = () => {
26
47
  const raw = process.env.PLURNK_MAX_STRIKES;
27
48
  if (raw === undefined || raw.length === 0)
@@ -131,41 +152,59 @@ const fingerprintOp = (stmt) => {
131
152
  }
132
153
  return base;
133
154
  };
134
- // Per-turn fingerprint: sorted set of per-op fingerprints, joined. Order
135
- // within a turn doesn't matter — we want the SET of activities.
136
- export const fingerprintTurn = (ops) => {
137
- return ops.map(fingerprintOp).toSorted().join(",");
138
- };
139
- // Rail #39 cycle detector. For each candidate period k in [1, maxCyclePeriod],
140
- // check whether the last k*minCycles entries form minCycles repetitions of the
141
- // same length-k pattern. O(maxCyclePeriod × minCycles × max k) ≈ tiny. Rummy
142
- // parallel: src/plugins/error/error.js detectCycle.
143
- export const detectCycle = (history, minCycles, maxCyclePeriod) => {
144
- for (let k = 1; k <= maxCyclePeriod; k++) {
145
- const needed = k * minCycles;
146
- if (history.length < needed)
147
- continue;
148
- const tail = history.slice(-needed);
149
- const cycle = tail.slice(0, k);
150
- let match = true;
151
- outer: for (let rep = 0; rep < minCycles; rep++) {
152
- for (let j = 0; j < k; j++) {
153
- if (tail[rep * k + j] !== cycle[j]) {
154
- match = false;
155
- break outer;
155
+ class Engine {
156
+ static computeCeiling(contextSize, config) {
157
+ // Absolute wall (config > 1) is window-independent — the point of the >1
158
+ // mode is to pin a ceiling even when the provider reports no window; cap at
159
+ // the real window when one is known. Ratio mode needs a window to scale.
160
+ if (config > 1)
161
+ return contextSize === null ? Math.floor(config) : Math.min(Math.floor(config), contextSize);
162
+ return contextSize === null ? null : Math.floor(contextSize * config);
163
+ }
164
+ // Per-turn fingerprint: sorted set of per-op fingerprints, joined. Order
165
+ // within a turn doesn't matter we want the SET of activities.
166
+ static fingerprintTurn(ops) {
167
+ return ops.map(fingerprintOp).toSorted().join(",");
168
+ }
169
+ // Rail #39 cycle detector. For each candidate period k in [1, maxCyclePeriod],
170
+ // check whether the last k*minCycles entries form minCycles repetitions of the
171
+ // same length-k pattern. O(maxCyclePeriod × minCycles × max k) ≈ tiny. Rummy
172
+ // parallel: src/plugins/error/error.js detectCycle.
173
+ static detectCycle(history, minCycles, maxCyclePeriod) {
174
+ for (let k = 1; k <= maxCyclePeriod; k++) {
175
+ const needed = k * minCycles;
176
+ if (history.length < needed)
177
+ continue;
178
+ const tail = history.slice(-needed);
179
+ const cycle = tail.slice(0, k);
180
+ let match = true;
181
+ outer: for (let rep = 0; rep < minCycles; rep++) {
182
+ for (let j = 0; j < k; j++) {
183
+ if (tail[rep * k + j] !== cycle[j]) {
184
+ match = false;
185
+ break outer;
186
+ }
156
187
  }
157
188
  }
189
+ if (match)
190
+ return { detected: true, period: k, cycles: minCycles };
158
191
  }
159
- if (match)
160
- return { detected: true, period: k, cycles: minCycles };
192
+ return { detected: false };
161
193
  }
162
- return { detected: false };
163
- };
164
- export default class Engine {
165
194
  #db;
166
195
  #schemes;
167
196
  #mimetypes;
168
197
  #previewBudget;
198
+ #budgetCeiling;
199
+ // Write-time tokenizer (SPEC §14.2). Synchronous per the provider
200
+ // contract (§2.1). Populated from the active provider's countTokens via
201
+ // the Daemon; a divisor tripwire stands in only for bare/standalone
202
+ // construction before a provider is wired (same boot affordance as
203
+ // Mimetypes, §4.5). Real counts come from provider.countTokens.
204
+ #tokenize;
205
+ // Boot-discovered runtime executors. Daemon builds + sets via
206
+ // setExecutors at start(); undefined until then (and in bare tests).
207
+ #executors;
169
208
  // Per-loop transient buffer of actionless failures pending surface in the
170
209
  // NEXT packet's user.telemetry.errors[]. Drained by #buildTelemetryErrors.
171
210
  // Map<loopId, TelemetryError[]>. SPEC §15.1.
@@ -200,7 +239,7 @@ export default class Engine {
200
239
  // status code but has no way to surface why the loop degraded.
201
240
  // Per-grammar 0.17.0 protocol — see SPEC §15.1.
202
241
  #telemetryEventNotify;
203
- constructor({ db, schemes, mimetypes, streamEventNotify, wakeRunNotify, telemetryEventNotify }) {
242
+ constructor({ db, schemes, mimetypes, streamEventNotify, wakeRunNotify, telemetryEventNotify, tokenize }) {
204
243
  this.#db = db;
205
244
  this.#schemes = schemes;
206
245
  this.#streamEventNotify = streamEventNotify;
@@ -214,6 +253,16 @@ export default class Engine {
214
253
  discovery: { registry: emptyRegistry(), handlers: new Map() },
215
254
  });
216
255
  this.#previewBudget = readBudget();
256
+ this.#budgetCeiling = readCeiling();
257
+ // Tripwire default matches the Mimetypes boot affordance (SPEC §4.5):
258
+ // the divisor stands in only until the provider-backed tokenizer is
259
+ // wired by the Daemon. Real counts come from provider.countTokens.
260
+ this.#tokenize = tokenize ?? ((text) => Math.ceil(text.length / 4));
261
+ }
262
+ // Late injection: the executor registry is async-built at daemon start()
263
+ // (discover + probe), after Engine construction.
264
+ setExecutors(executors) {
265
+ this.#executors = executors;
217
266
  }
218
267
  #pushTelemetry(sessionId, loopId, event) {
219
268
  const existing = this.#telemetryBuffer.get(loopId);
@@ -305,6 +354,12 @@ export default class Engine {
305
354
  turnNumber: turnIds.length + 1, maxTurns,
306
355
  });
307
356
  turnIds.push(turn.turnId);
357
+ // SPEC §14.4: budget hard-stop — packet won't fit even collapsed → abandon.
358
+ if (turn.budgetHardStop) {
359
+ await this.#db.engine_loop_cancel.run({ loop_id: loopId });
360
+ cleanup("forceful", "budget_overflow");
361
+ return { turnIds, finalStatus: 499, hitMaxTurns: false, reason: "budget_overflow" };
362
+ }
308
363
  // Rail #39: cycle detection. Push this turn's fingerprint to
309
364
  // history, scan for repetition patterns. Detection bumps
310
365
  // turnErrors so the strike system handles abandonment
@@ -315,9 +370,12 @@ export default class Engine {
315
370
  // reason for treating the turn as a failure, not its own alert.
316
371
  const state = this.#strikeState.get(loopId) ?? { streak: 0, turnErrors: 0, history: [] };
317
372
  state.history.push(turn.fingerprint);
318
- const cycle = detectCycle(state.history, minCycles, maxCyclePeriod);
373
+ const cycle = _a.detectCycle(state.history, minCycles, maxCyclePeriod);
319
374
  if (cycle.detected)
320
375
  state.turnErrors++;
376
+ // SPEC §14.4: a non-soft grinder fire counts toward the strike streak.
377
+ if (turn.budgetStruck)
378
+ state.turnErrors++;
321
379
  this.#strikeState.set(loopId, state);
322
380
  // Rail #38: strike accounting. Three sources strike a turn:
323
381
  // 1. recordedFailed — any action-entry at hard failure status
@@ -409,13 +467,61 @@ export default class Engine {
409
467
  nextActionIndex++;
410
468
  }
411
469
  }
470
+ // plurnk://manifest.json — rewritten EVERY turn (a live view of the
471
+ // entry set, which changes each turn). A derived view like the index,
472
+ // NOT an action — written directly (Engine.inject's path): no log entry,
473
+ // no sequence slot, not dispatched. The catalog body is built in the
474
+ // schemes layer (_entry-manifest); the engine only orchestrates the
475
+ // per-turn write. Does not list itself.
476
+ const systemCtx = {
477
+ db: this.#db, sessionId, runId, loopId, turnId,
478
+ writer: "system",
479
+ signal: this.#loopAborts.get(loopId)?.signal,
480
+ streamEventNotify: this.#streamEventNotify,
481
+ wakeRunNotify: this.#wakeRunNotify,
482
+ tokenize: this.#tokenize,
483
+ mimetypes: this.#mimetypes,
484
+ pushTelemetry: (event) => this.#pushTelemetry(sessionId, loopId, event),
485
+ };
486
+ // SPEC §14.3 D4/D5 — git-ls-files workspace membership, resolved at
487
+ // prompt-composition (EMI is eager + relevance-bounded). When the
488
+ // session's project_root is a git working tree, tracked files are
489
+ // members without a client `add`; active members are materialized
490
+ // (disk → body channel + visibility) so they surface in the index
491
+ // below. No-ops on headless / non-git sessions. Runs BEFORE the
492
+ // manifest + index build so this turn's packet reflects them.
493
+ await GitMembership.indexGitMembership(systemCtx);
494
+ await EntryCrud.writeEntry("manifest.json", {
495
+ channels: { body: { content: await EntryManifest.buildManifestBody(systemCtx, this.#previewBudget), mimetype: "application/json" } },
496
+ tags: [],
497
+ }, systemCtx, "plurnk");
412
498
  // Build the spec'd packet (Packet.json) request half. #buildLog
413
499
  // queries log_entries scoped to the run — the prompt entry just
414
500
  // written (if turn 1) is part of that query result.
415
- const requestPacket = await this.#buildRequestPacket({
501
+ let requestPacket = await this.#buildRequestPacket({
416
502
  initialMessages: messages, persona, requirements, runId, loopId,
417
503
  currentTurnSeq: seq, provider,
418
504
  });
505
+ // SPEC §14.4 — budget grinder, pre-LLM: reclaim window on actual overflow.
506
+ const enforced = await this.#enforceBudget({
507
+ packet: requestPacket, provider, runId, loopId, turnId, sessionId, turnNumber,
508
+ rebuild: (telemetryErrors) => this.#buildRequestPacket({
509
+ initialMessages: messages, persona, requirements, runId, loopId,
510
+ currentTurnSeq: seq, provider, telemetryErrors,
511
+ }),
512
+ });
513
+ requestPacket = enforced.packet;
514
+ if (!enforced.fit) {
515
+ // Hard 413: won't fit even with only the manifest left. Skip the LLM,
516
+ // close the turn, and let runLoop abandon (499).
517
+ const hardPacket = this.#completePacket(requestPacket, { content: "", ops: [], reasoning: null }, null, provider);
518
+ await this.#db.engine_close_turn.run({
519
+ id: turnId, status: 413, packet: JSON.stringify(hardPacket),
520
+ usage_prompt: 0, usage_completion: 0, usage_cached: 0, usage_cost_pico: 0,
521
+ finish_reason: "budget_hard_stop", model: provider.model,
522
+ });
523
+ return { turnId, status: 413, statuses: [], fingerprint: "", budgetStruck: enforced.struck, budgetHardStop: true };
524
+ }
419
525
  const modelMessages = this.#packetToWireMessages(requestPacket);
420
526
  const response = await provider.generate({ messages: modelMessages, signal });
421
527
  // Engine splits wire-level response: emission (content, reasoning,
@@ -506,7 +612,7 @@ export default class Engine {
506
612
  // nothing. Strike accounting (engine-internal) treats it as a
507
613
  // struck turn; the model just sees an empty packet next turn.
508
614
  // Per SPEC §15.1 gamification policy.
509
- return { turnId, status: turnStatus, statuses, fingerprint: fingerprintTurn(packetAssistant.ops) };
615
+ return { turnId, status: turnStatus, statuses, fingerprint: _a.fingerprintTurn(packetAssistant.ops), budgetStruck: enforced.struck, budgetHardStop: false };
510
616
  }
511
617
  // Split the wire-level ProviderResponse into the two destinations:
512
618
  // packet.assistant gets the model's emission (content, ops, reasoning);
@@ -568,7 +674,7 @@ export default class Engine {
568
674
  // and §user) BEFORE the provider call. The same packet object is then
569
675
  // completed with assistant + assistantRaw after the model responds, so
570
676
  // the stored packet and the wire payload share one source of truth.
571
- async #buildRequestPacket({ initialMessages, persona: defaultPersona, requirements, runId, loopId, currentTurnSeq, provider, }) {
677
+ async #buildRequestPacket({ initialMessages, persona: defaultPersona, requirements, runId, loopId, currentTurnSeq, provider, telemetryErrors: presetTelemetry, }) {
572
678
  const byRole = (role) => initialMessages.filter((m) => m.role === role).map((m) => m.content).join("\n\n");
573
679
  const system_definition = byRole("system");
574
680
  // user.prompt sources from the loop's most recent prompt entry first
@@ -587,65 +693,117 @@ export default class Engine {
587
693
  const persona = (row?.persona !== undefined && row?.persona !== null) ? row.persona : defaultPersona;
588
694
  const index = await this.#buildIndex(runId, loopId);
589
695
  const log = await this.#buildLog(runId);
590
- const telemetryErrors = await this.#buildTelemetryErrors(loopId, currentTurnSeq);
696
+ const telemetryErrors = presetTelemetry ?? await this.#buildTelemetryErrors(loopId, currentTurnSeq);
591
697
  // Per-section render-cost subtotals via provider's tokenizer.
592
698
  // Engine approximates each section by tokenizing its serialized
593
699
  // form — wire-payload tokens may differ slightly because chat-
594
700
  // template scaffolding adds bytes, but the subtotal tracks "what
595
701
  // the model has to process" closely enough for budget diagnostics.
596
- const systemTokens = provider.countTokens(system_definition) +
597
- provider.countTokens(persona) +
598
- provider.countTokens(JSON.stringify(index)) +
599
- provider.countTokens(JSON.stringify(log));
600
- // user.telemetry.budgetshimmed with section-aggregate table.
601
- // Real per-scheme breakdown is the tokenomics chapter (SPEC.md §14.2);
602
- // depends on provider.getContextSize and write-time per-row tokens.
603
- const budget = this.#renderBudgetShim(systemTokens, provider, prompt, telemetryErrors);
604
- const userTokens = provider.countTokens(prompt) +
605
- provider.countTokens(JSON.stringify(telemetryErrors)) +
606
- provider.countTokens(budget) +
607
- provider.countTokens(requirements);
608
- return {
609
- system: {
610
- tokens: systemTokens,
611
- system_definition,
612
- persona,
613
- index,
614
- log,
615
- },
616
- user: {
617
- tokens: userTokens,
618
- prompt,
619
- telemetry: { budget, errors: telemetryErrors },
620
- system_requirements: requirements,
621
- },
702
+ const countTokens = (t) => provider.countTokens(t);
703
+ // Budget readout (SPEC.md §14.2). Two-pass: measure the wire-rendered
704
+ // index/log sections (budget-independent), install the readout with a
705
+ // tokensFree placeholder, measure the assembled total, resolve free,
706
+ // substitute. Subtotals come from the real render meta and fences
707
+ // included not a serialized approximation. ceiling is the provider's
708
+ // window × PLURNK_BUDGET_CEILING (null when no window is reported →
709
+ // headline omitted, section lines still shown).
710
+ const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
711
+ const scratch = {
712
+ system: { system_definition, persona, index, log },
713
+ user: { prompt, telemetry: { budget: "", errors: telemetryErrors }, system_requirements: requirements },
622
714
  };
715
+ const sections = PacketWire.measureBudgetSections(scratch, countTokens);
716
+ scratch.user.telemetry.budget = this.#renderBudget(sections, ceiling);
717
+ const total = countTokens(PacketWire.renderSystemContent(scratch.system)) + countTokens(PacketWire.renderUserContent(scratch.user));
718
+ const tokensFree = ceiling === null ? null : Math.max(0, ceiling - total);
719
+ const percent = ceiling === null ? null : Math.round((total / ceiling) * 100);
720
+ const budget = tokensFree === null
721
+ ? scratch.user.telemetry.budget
722
+ : scratch.user.telemetry.budget
723
+ .replace(TOKEN_USAGE_PLACEHOLDER, String(total))
724
+ .replace(TOKEN_PERCENT_PLACEHOLDER, String(percent))
725
+ .replace(TOKENS_FREE_PLACEHOLDER, String(tokensFree));
726
+ const system = { tokens: 0, system_definition, persona, index, log };
727
+ const user = { tokens: 0, prompt, telemetry: { budget, errors: telemetryErrors }, system_requirements: requirements };
728
+ system.tokens = countTokens(PacketWire.renderSystemContent(system));
729
+ user.tokens = countTokens(PacketWire.renderUserContent(user));
730
+ return { system, user };
731
+ }
732
+ // Budget readout body, rendered into the `# Plurnk System Budget` section.
733
+ // Headline `ceiling/free` only when a ceiling exists; section lines for the
734
+ // curatable index/log weight the model can HIDE back. tokensFree is a
735
+ // placeholder here — buildSystem substitutes it after measuring the packet.
736
+ #renderBudget(sections, ceiling) {
737
+ const lines = [];
738
+ if (ceiling !== null)
739
+ lines.push(`ceiling ${ceiling} · usage ${TOKEN_USAGE_PLACEHOLDER} (${TOKEN_PERCENT_PLACEHOLDER}%) · free ${TOKENS_FREE_PLACEHOLDER}`);
740
+ if (sections.index.channels > 0)
741
+ lines.push(`Index previews: ${sections.index.channels} channels, ${sections.index.tokens} tokens`);
742
+ if (sections.log.entries > 0) {
743
+ lines.push(`Log entries: ${sections.log.entries} entries, ${sections.log.tokens} tokens`);
744
+ if (sections.log.byScheme.length > 0) {
745
+ lines.push("| scheme | entries | tokens |", "|---|--:|--:|");
746
+ for (const s of sections.log.byScheme)
747
+ lines.push(`| ${s.scheme} | ${s.entries} | ${s.tokens} |`);
748
+ }
749
+ }
750
+ return lines.join("\n");
623
751
  }
624
- // user.telemetry.budgetSHIM. Real per-scheme breakdown +
625
- // context-window "free/percent-of-total" is SPEC.md §14.2; depends on
626
- // provider.getContextSize and write-time per-row tokens. Until then,
627
- // render the section-aggregate counts we already compute so the wire's
628
- // `# Plurnk System Budget` section is non-empty for picking-apart purposes.
629
- #renderBudgetShim(systemTokens, provider, prompt, telemetryErrors) {
630
- const userPromptTokens = provider.countTokens(prompt);
631
- const userErrTokens = provider.countTokens(JSON.stringify(telemetryErrors));
632
- const userTokens = userPromptTokens + userErrTokens;
633
- const total = systemTokens + userTokens;
634
- const pct = (n) => total === 0 ? "0.0%" : `${((n / total) * 100).toFixed(1)}%`;
635
- return [
636
- "| Section | Used | Percent |",
637
- "|---|---|---|",
638
- `| system | ${systemTokens} | ${pct(systemTokens)} |`,
639
- `| user | ${userTokens} | ${pct(userTokens)} |`,
640
- `| **Total** | **${total}** | **100.0%** |`,
641
- ].join("\n");
752
+ // SPEC §14.4the budget grinder. Runs pre-LLM (in runTurn, after the packet
753
+ // is built, before provider.generate); fires only on actual overflow. Two
754
+ // passes, re-measuring between. Hides (never deletes) the prior turn's logs,
755
+ // then the catalog except the manifest lifeline. The strike it raises and the
756
+ // hard-stop it can signal are returned to runLoop, which owns abandonment.
757
+ async #enforceBudget({ packet, provider, runId, loopId, turnId, sessionId, turnNumber, rebuild }) {
758
+ const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
759
+ const measure = (p) => p.system.tokens + p.user.tokens;
760
+ if (ceiling === null || measure(packet) <= ceiling)
761
+ return { packet, fit: true, struck: false };
762
+ const hidden = new Map();
763
+ const note = (scheme) => { hidden.set(scheme, (hidden.get(scheme) ?? 0) + 1); };
764
+ // Pass 1 prior-turn rollback: hide the latest emissions (the ones that
765
+ // pushed it over). No prior turn (turn 1, env overflow) → no-op → pass 2.
766
+ const priorLogs = await this.#db.engine_grinder_prior_turn_logs.all({ loop_id: loopId, turn_id: turnId });
767
+ for (const le of priorLogs)
768
+ note(le.scheme ?? "log");
769
+ if (priorLogs.length > 0)
770
+ await this.#db.engine_grinder_hide_prior_turn_logs.run({ loop_id: loopId, turn_id: turnId });
771
+ const errors = packet.user.telemetry.errors;
772
+ let current = priorLogs.length > 0 ? await rebuild(errors) : packet;
773
+ if (measure(current) <= ceiling) {
774
+ this.#emitBudgetOverflow(sessionId, loopId, hidden);
775
+ return { packet: current, fit: true, struck: turnNumber > 1 };
776
+ }
777
+ // Pass 2 — index collapse: hide every catalog entry except the manifest.
778
+ const catalog = await this.#db.engine_grinder_catalog.all({ run_id: runId, session_id: sessionId });
779
+ for (const c of catalog)
780
+ note(c.scheme);
781
+ if (catalog.length > 0) {
782
+ const pairs = JSON.stringify(catalog.map((c) => ({ entry_id: c.entry_id, channel: c.channel })));
783
+ await this.#db.engine_grinder_hide_catalog.run({ run_id: runId, pairs });
784
+ }
785
+ current = catalog.length > 0 ? await rebuild(errors) : current;
786
+ this.#emitBudgetOverflow(sessionId, loopId, hidden);
787
+ return { packet: current, fit: measure(current) <= ceiling, struck: turnNumber > 1 };
642
788
  }
643
- // Wire projection lives in ./packet-wire.js (plain JS) so Engine and
644
- // bin/digest.js import the exact same function structurally one
789
+ // The model-facing budget event (SPEC §14.4, §15.1): which entries left the
790
+ // window, by scheme — the model's own terms, no mechanism vocabulary. The
791
+ // strike this overflow triggers stays engine-internal (gamification policy).
792
+ #emitBudgetOverflow(sessionId, loopId, hidden) {
793
+ if (hidden.size === 0)
794
+ return;
795
+ this.#pushTelemetry(sessionId, loopId, {
796
+ source: "engine:rail",
797
+ kind: "budget_overflow",
798
+ hidden: [...hidden.entries()].map(([scheme, count]) => ({ scheme, count })),
799
+ });
800
+ }
801
+ // Wire projection lives in ./packet-wire.ts so Engine and
802
+ // bin/digest.ts import the exact same function — structurally one
645
803
  // implementation, no drift between wire and digest possible.
646
804
  // Format: markdown (user pick over rummy's XML alternative, 2026-05-22).
647
805
  #packetToWireMessages(packet) {
648
- return packetToWireMessages(packet);
806
+ return PacketWire.packetToWireMessages(packet);
649
807
  }
650
808
  // Complete the packet by adding the model's response. After this the
651
809
  // packet matches Packet.json fully and is ready for storage.
@@ -779,6 +937,7 @@ export default class Engine {
779
937
  content: result.preview,
780
938
  mimetype: row.mimetype,
781
939
  tokens: row.tokens,
940
+ lines: result.totalLines,
782
941
  };
783
942
  }
784
943
  return [...entries.values()];
@@ -793,7 +952,9 @@ export default class Engine {
793
952
  streamEventNotify: this.#streamEventNotify,
794
953
  wakeRunNotify: this.#wakeRunNotify,
795
954
  mimetypes: this.#mimetypes,
955
+ tokenize: this.#tokenize,
796
956
  pushTelemetry: (event) => this.#pushTelemetry(sessionId, loopId, event),
957
+ executors: this.#executors,
797
958
  };
798
959
  let result;
799
960
  let denial = this.#checkWritable(statement, origin);
@@ -846,6 +1007,13 @@ export default class Engine {
846
1007
  // 202 in the result the caller sees, so runTurn never branches on
847
1008
  // a pending state.
848
1009
  if (result.status === 202) {
1010
+ // Effect-gated auto-run (read/pure runtimes, plurnk-service#182):
1011
+ // no human gate, no loop/proposal notification. Accept + apply
1012
+ // in-process; the model sees the outcome directly, never a review.
1013
+ if (result.attrs?.inline === true) {
1014
+ const effective = await this.#runApplyResolution(statement, result, { decision: "accept" }, { sessionId, runId, loopId, turnId });
1015
+ return this.#applyResolution(logEntryId, effective);
1016
+ }
849
1017
  // Register the resolution waiter SYNCHRONOUSLY before any await
850
1018
  // yields. A same-tick resolveProposal() (e.g. from a test that
851
1019
  // awaits the onDispatch callback and immediately resolves) must
@@ -906,7 +1074,9 @@ export default class Engine {
906
1074
  writer: "model", signal: this.#loopAborts.get(loopId)?.signal,
907
1075
  streamEventNotify: this.#streamEventNotify,
908
1076
  wakeRunNotify: this.#wakeRunNotify,
1077
+ tokenize: this.#tokenize,
909
1078
  pushTelemetry: (event) => this.#pushTelemetry(sessionId, loopId, event),
1079
+ executors: this.#executors,
910
1080
  };
911
1081
  const applyResult = await handler.applyResolution({
912
1082
  attrs: (originalResult.attrs ?? {}),
@@ -920,14 +1090,14 @@ export default class Engine {
920
1090
  };
921
1091
  }
922
1092
  // Propagate applyResolution.outcome onto the accepted resolution
923
- // so the log entry's outcome column reflects operational metadata
924
- // (e.g. exec's "exit_N"). Without this, only failures get an
925
- // outcome on the durable record, and "ran cleanly but with a
926
- // notable detail" has nowhere to land.
927
- if (applyResult.outcome !== undefined && resolution.outcome === undefined) {
928
- return { ...resolution, outcome: applyResult.outcome };
929
- }
930
- return resolution;
1093
+ // (operational metadata, e.g. exec's "exit_N") AND its body — an
1094
+ // inline (read/pure) run returns its output as the body, which has
1095
+ // to reach the model-facing result this turn, not just stream to
1096
+ // the entry. Host accepts carry no body (fire-and-forget).
1097
+ const withOutcome = applyResult.outcome !== undefined && resolution.outcome === undefined
1098
+ ? { ...resolution, outcome: applyResult.outcome }
1099
+ : resolution;
1100
+ return applyResult.body === undefined ? withOutcome : { ...withOutcome, body: applyResult.body };
931
1101
  }
932
1102
  catch (err) {
933
1103
  return {
@@ -995,13 +1165,14 @@ export default class Engine {
995
1165
  signal: this.#loopAborts.get(loopId)?.signal,
996
1166
  streamEventNotify: this.#streamEventNotify,
997
1167
  wakeRunNotify: this.#wakeRunNotify,
1168
+ tokenize: this.#tokenize,
998
1169
  pushTelemetry: (event) => this.#pushTelemetry(sessionRow.session_id, loopId, event),
999
1170
  };
1000
1171
  const entry = {
1001
1172
  channels: { body: { content: prompt, mimetype: "text/markdown" } },
1002
1173
  tags: [],
1003
1174
  };
1004
- await writeEntry(pathname, entry, ctx, "plurnk");
1175
+ await EntryCrud.writeEntry(pathname, entry, ctx, "plurnk");
1005
1176
  return { loopId, turnSeq };
1006
1177
  }
1007
1178
  // Subscribe to proposal-pending events. Daemon registers a listener
@@ -1062,13 +1233,15 @@ export default class Engine {
1062
1233
  : decision === "reject" ? "rejected"
1063
1234
  : "loop_aborted";
1064
1235
  const outcome = resolution.outcome ?? defaultOutcome;
1065
- // rx is the model-facing operation result. ONLY status outcome
1066
- // is operational (security/admin) and stays on the log_entries
1067
- // column for forensics; body was an input echo with no value to
1068
- // the model; target/path lives in log_entries metadata and
1069
- // surfaces via the log section's `target` field uniformly.
1236
+ // rx is the model-facing operation result. Status always; outcome is
1237
+ // operational (stays on log_entries for forensics, never model-facing).
1238
+ // Body is normally dropped — the propose preview was an input echo
1239
+ // EXCEPT an inline auto-run (read/pure) carries its run output AS the
1240
+ // body, which is exactly the "what happened" the model needs this turn.
1070
1241
  // Per AGENTS.md "Operational hygiene on what the model sees."
1071
- const rx = JSON.stringify({ status });
1242
+ const rx = (decision === "accept" && resolution.body !== undefined)
1243
+ ? JSON.stringify({ status, body: resolution.body })
1244
+ : JSON.stringify({ status });
1072
1245
  await this.#db.engine_resolve_log_entry.run({
1073
1246
  id: logEntryId, state, outcome, status_rx: status, rx,
1074
1247
  });
@@ -1123,7 +1296,7 @@ export default class Engine {
1123
1296
  return { status: 403, error: `writer '${origin}' is not in writableBy for scheme '${schemeName}'` };
1124
1297
  }
1125
1298
  // Per-loop flag gating. Schemes self-declare their flag affinity in
1126
- // their manifest (proposes / excludedInAsk / requiresWeb /
1299
+ // their manifest (excludedInAsk / requiresWeb /
1127
1300
  // requiresInteraction); SchemeRegistry.resolveForLoop returns the
1128
1301
  // active set under the loop's persisted flags. Anything outside the
1129
1302
  // set returns 403 — action-entry-as-outcome carries the rejection.
@@ -1133,7 +1306,7 @@ export default class Engine {
1133
1306
  return null;
1134
1307
  const flags = await this.#loadLoopFlags(loopId);
1135
1308
  // Fast path: default flags gate nothing. (yolo never gates.)
1136
- if (!flags.noProposals && !flags.noWeb && !flags.noInteraction && flags.mode === "act")
1309
+ if (!flags.noWeb && !flags.noInteraction && flags.mode === "act")
1137
1310
  return null;
1138
1311
  const active = this.#schemes.resolveForLoop(flags);
1139
1312
  const check = (target) => {
@@ -1173,8 +1346,9 @@ export default class Engine {
1173
1346
  const srcHandler = this.#schemes.get(srcSchemeName);
1174
1347
  if (srcHandler === undefined || typeof srcHandler.deleteEntry !== "function")
1175
1348
  return { status: 501 };
1176
- // Null-body MOVE = delete the source entry (per SPEC §6.5)
1177
- if (dstPath === null) {
1349
+ // MOVE to /dev/null (the grammar's idiomatic delete) or a null-body
1350
+ // MOVE deletes the source entry. SPEC §6.5.
1351
+ if (dstPath === null || pathnameFromPath(dstPath) === "/dev/null") {
1178
1352
  const srcPathname = pathnameFromPath(srcPath);
1179
1353
  const delResult = await srcHandler.deleteEntry(srcPathname, ctx);
1180
1354
  return { status: delResult.status };
@@ -1206,12 +1380,12 @@ export default class Engine {
1206
1380
  if (srcResult.status !== 200 || srcResult.entry === null)
1207
1381
  return { status: 404, error: `COPY/MOVE source not found: ${srcSchemeName}://${srcPathname}` };
1208
1382
  const entry = srcResult.entry;
1209
- // Conflict check on destination
1210
- if (typeof dstHandler.readEntry === "function") {
1211
- const dstExists = await dstHandler.readEntry(dstPathname, ctx);
1212
- if (dstExists.status === 200)
1213
- return { status: 409, error: `COPY/MOVE destination exists: ${dstSchemeName}://${dstPathname}` };
1214
- }
1383
+ // Destination read the conflict/no-op verdict is deferred until the
1384
+ // to-be-written content is known (after <L> slice + tag resolution below),
1385
+ // so an identical re-copy resolves to 304 instead of a phantom 409.
1386
+ const dstExisting = typeof dstHandler.readEntry === "function"
1387
+ ? await dstHandler.readEntry(dstPathname, ctx)
1388
+ : null;
1215
1389
  // Mimetype compatibility check against the destination scheme's manifest
1216
1390
  const dstManifest = dstHandler.constructor.manifest;
1217
1391
  const dstChannels = dstManifest?.channels ?? {};
@@ -1230,10 +1404,10 @@ export default class Engine {
1230
1404
  if (lineMarker !== null) {
1231
1405
  const sliced = {};
1232
1406
  for (const [channelName, channelData] of Object.entries(entry.channels)) {
1233
- if (isBinaryMimetype(channelData.mimetype)) {
1407
+ if (MimetypeBinary.isBinaryMimetype(channelData.mimetype)) {
1234
1408
  return { status: 415, error: `cannot slice <L> on binary channel '${channelName}' (${channelData.mimetype})` };
1235
1409
  }
1236
- const r = sliceLinesRaw(channelData.content ?? "", lineMarker);
1410
+ const r = LineMarkerOps.sliceLinesRaw(channelData.content ?? "", lineMarker);
1237
1411
  if (r.status !== 200)
1238
1412
  return { status: r.status, error: r.error };
1239
1413
  sliced[channelName] = { ...channelData, content: r.text ?? "" };
@@ -1244,6 +1418,21 @@ export default class Engine {
1244
1418
  const tags = (Array.isArray(statement.signal) && statement.signal.length > 0)
1245
1419
  ? statement.signal
1246
1420
  : entry.tags;
1421
+ // 304/409 on an existing destination (SPEC §6.4): a re-copy that would write
1422
+ // exactly what's already there — same channel contents, same tags — is a no-op
1423
+ // (304), mirroring EDIT's 304-on-noop (§6.1). A divergent destination is a real
1424
+ // collision (409); COPY/MOVE never clobbers.
1425
+ if (dstExisting !== null && dstExisting.status === 200 && dstExisting.entry !== null) {
1426
+ const dstChannels = dstExisting.entry.channels;
1427
+ const writeNames = Object.keys(channels).sort();
1428
+ const dstNames = Object.keys(dstChannels).sort();
1429
+ const sameContent = writeNames.length === dstNames.length
1430
+ && writeNames.every((n, i) => n === dstNames[i] && (channels[n]?.content ?? "") === (dstChannels[n]?.content ?? ""));
1431
+ const sameTags = [...tags].sort().join("") === [...dstExisting.entry.tags].sort().join("");
1432
+ if (sameContent && sameTags)
1433
+ return { status: 304 };
1434
+ return { status: 409, error: `COPY/MOVE destination exists: ${dstSchemeName}://${dstPathname}` };
1435
+ }
1247
1436
  const writeResult = await dstHandler.writeEntry(dstPathname, { channels, tags }, ctx);
1248
1437
  return { status: writeResult.status, entryId: writeResult.entryId, created: writeResult.created };
1249
1438
  }
@@ -1322,6 +1511,8 @@ export default class Engine {
1322
1511
  }
1323
1512
  }
1324
1513
  const attrs = JSON.stringify(attrsObj);
1514
+ const txJson = JSON.stringify(statement);
1515
+ const rxJson = JSON.stringify(result);
1325
1516
  const row = await this.#db.engine_insert_log_entry.get({
1326
1517
  run_id: runId,
1327
1518
  loop_id: loopId,
@@ -1340,11 +1531,12 @@ export default class Engine {
1340
1531
  params: target.params,
1341
1532
  fragment: target.fragment,
1342
1533
  lineMarker: lineMarkerJson,
1343
- tx: JSON.stringify(statement),
1534
+ tx: txJson,
1344
1535
  mimetype_tx: "application/json",
1345
- rx: JSON.stringify(result),
1536
+ rx: rxJson,
1346
1537
  mimetype_rx: "application/json",
1347
1538
  status_rx: result.status,
1539
+ tokens: this.#tokenize(txJson) + this.#tokenize(rxJson),
1348
1540
  state: isProposed ? "proposed" : "resolved",
1349
1541
  outcome: null,
1350
1542
  attrs,
@@ -1375,4 +1567,6 @@ export default class Engine {
1375
1567
  return JSON.stringify(signal);
1376
1568
  }
1377
1569
  }
1570
+ _a = Engine;
1571
+ export default Engine;
1378
1572
  //# sourceMappingURL=Engine.js.map