@plurnk/plurnk-service 1.4.0 → 1.6.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 (302) hide show
  1. package/.env.defaults +37 -31
  2. package/INSTALL.md +16 -11
  3. package/README.md +1 -0
  4. package/SPEC.md +631 -397
  5. package/dist/Paths.d.ts.map +1 -1
  6. package/dist/Paths.js +2 -3
  7. package/dist/Paths.js.map +1 -1
  8. package/dist/build-info.json +1 -1
  9. package/dist/content/edit-collision.d.ts +5 -0
  10. package/dist/content/edit-collision.d.ts.map +1 -0
  11. package/dist/content/edit-collision.js +14 -0
  12. package/dist/content/edit-collision.js.map +1 -0
  13. package/dist/content/edit-receipt.d.ts +3 -2
  14. package/dist/content/edit-receipt.d.ts.map +1 -1
  15. package/dist/content/edit-receipt.js +58 -13
  16. package/dist/content/edit-receipt.js.map +1 -1
  17. package/dist/content/index.d.ts +5 -1
  18. package/dist/content/index.d.ts.map +1 -1
  19. package/dist/content/index.js +4 -1
  20. package/dist/content/index.js.map +1 -1
  21. package/dist/content/line-anchors.d.ts +41 -0
  22. package/dist/content/line-anchors.d.ts.map +1 -0
  23. package/dist/content/line-anchors.js +191 -0
  24. package/dist/content/line-anchors.js.map +1 -0
  25. package/dist/content/line-marker.d.ts +7 -3
  26. package/dist/content/line-marker.d.ts.map +1 -1
  27. package/dist/content/line-marker.js +2 -1
  28. package/dist/content/line-marker.js.map +1 -1
  29. package/dist/content/matcher.d.ts +1 -1
  30. package/dist/content/matcher.d.ts.map +1 -1
  31. package/dist/content/matcher.js +2 -2
  32. package/dist/content/matcher.js.map +1 -1
  33. package/dist/content/read-projector.d.ts +20 -0
  34. package/dist/content/read-projector.d.ts.map +1 -0
  35. package/dist/content/read-projector.js +122 -0
  36. package/dist/content/read-projector.js.map +1 -0
  37. package/dist/content/read-resolve.d.ts +2 -7
  38. package/dist/content/read-resolve.d.ts.map +1 -1
  39. package/dist/content/read-resolve.js +32 -107
  40. package/dist/content/read-resolve.js.map +1 -1
  41. package/dist/core/BranchReceipt.d.ts +0 -1
  42. package/dist/core/BranchReceipt.d.ts.map +1 -1
  43. package/dist/core/BranchReceipt.js +0 -3
  44. package/dist/core/BranchReceipt.js.map +1 -1
  45. package/dist/core/BudgetReadout.d.ts.map +1 -1
  46. package/dist/core/BudgetReadout.js +9 -2
  47. package/dist/core/BudgetReadout.js.map +1 -1
  48. package/dist/core/ChannelWrite.d.ts +3 -0
  49. package/dist/core/ChannelWrite.d.ts.map +1 -1
  50. package/dist/core/ChannelWrite.js +3 -3
  51. package/dist/core/ChannelWrite.js.map +1 -1
  52. package/dist/core/ChannelWrite.sql +4 -3
  53. package/dist/core/CoreSchemeServices.d.ts +10 -2
  54. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  55. package/dist/core/CoreSchemeServices.js.map +1 -1
  56. package/dist/core/Dispatcher.d.ts +14 -8
  57. package/dist/core/Dispatcher.d.ts.map +1 -1
  58. package/dist/core/Dispatcher.js +656 -312
  59. package/dist/core/Dispatcher.js.map +1 -1
  60. package/dist/core/DurableStatement.d.ts.map +1 -1
  61. package/dist/core/DurableStatement.js +2 -0
  62. package/dist/core/DurableStatement.js.map +1 -1
  63. package/dist/core/Engine.d.ts +24 -18
  64. package/dist/core/Engine.d.ts.map +1 -1
  65. package/dist/core/Engine.js +620 -392
  66. package/dist/core/Engine.js.map +1 -1
  67. package/dist/core/Engine.sql +188 -119
  68. package/dist/core/ExecutableTools.d.ts +10 -0
  69. package/dist/core/ExecutableTools.d.ts.map +1 -0
  70. package/dist/core/ExecutableTools.js +41 -0
  71. package/dist/core/ExecutableTools.js.map +1 -0
  72. package/dist/core/ExecutorRegistry.d.ts +3 -3
  73. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  74. package/dist/core/ExecutorRegistry.js +2 -2
  75. package/dist/core/ExecutorRegistry.js.map +1 -1
  76. package/dist/core/LogBody.d.ts +4 -1
  77. package/dist/core/LogBody.d.ts.map +1 -1
  78. package/dist/core/LogBody.js +46 -2
  79. package/dist/core/LogBody.js.map +1 -1
  80. package/dist/core/LogEntryProjection.d.ts +14 -0
  81. package/dist/core/LogEntryProjection.d.ts.map +1 -0
  82. package/dist/core/LogEntryProjection.js +38 -0
  83. package/dist/core/LogEntryProjection.js.map +1 -0
  84. package/dist/core/LoopLifecycle.d.ts +1 -2
  85. package/dist/core/LoopLifecycle.d.ts.map +1 -1
  86. package/dist/core/LoopLifecycle.js +1 -4
  87. package/dist/core/LoopLifecycle.js.map +1 -1
  88. package/dist/core/LoopLifecycle.sql +1 -4
  89. package/dist/core/ModelCall.d.ts +27 -0
  90. package/dist/core/ModelCall.d.ts.map +1 -0
  91. package/dist/core/ModelCall.js +125 -0
  92. package/dist/core/ModelCall.js.map +1 -0
  93. package/dist/core/PacketBuilder.d.ts +13 -12
  94. package/dist/core/PacketBuilder.d.ts.map +1 -1
  95. package/dist/core/PacketBuilder.js +146 -118
  96. package/dist/core/PacketBuilder.js.map +1 -1
  97. package/dist/core/ProblemLog.d.ts.map +1 -1
  98. package/dist/core/ProblemLog.js +1 -0
  99. package/dist/core/ProblemLog.js.map +1 -1
  100. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  101. package/dist/core/ProposalLifecycle.js +3 -4
  102. package/dist/core/ProposalLifecycle.js.map +1 -1
  103. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  104. package/dist/core/ProviderInstantiate.js +23 -6
  105. package/dist/core/ProviderInstantiate.js.map +1 -1
  106. package/dist/core/SchemeRegistry.js +7 -7
  107. package/dist/core/SchemeRegistry.js.map +1 -1
  108. package/dist/core/StrikeRail.d.ts +5 -2
  109. package/dist/core/StrikeRail.d.ts.map +1 -1
  110. package/dist/core/StrikeRail.js +10 -7
  111. package/dist/core/StrikeRail.js.map +1 -1
  112. package/dist/core/TerminalResult.d.ts +16 -0
  113. package/dist/core/TerminalResult.d.ts.map +1 -0
  114. package/dist/core/TerminalResult.js +49 -0
  115. package/dist/core/TerminalResult.js.map +1 -0
  116. package/dist/core/caps/CapsResolve.d.ts +5 -1
  117. package/dist/core/caps/CapsResolve.d.ts.map +1 -1
  118. package/dist/core/caps/CapsResolve.js +9 -5
  119. package/dist/core/caps/CapsResolve.js.map +1 -1
  120. package/dist/core/caps/DbChannelCaps.d.ts +1 -1
  121. package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
  122. package/dist/core/caps/DbChannelCaps.js +6 -4
  123. package/dist/core/caps/DbChannelCaps.js.map +1 -1
  124. package/dist/core/caps/DbEntryCaps.d.ts +2 -1
  125. package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
  126. package/dist/core/caps/DbEntryCaps.js +21 -8
  127. package/dist/core/caps/DbEntryCaps.js.map +1 -1
  128. package/dist/core/caps/DbNotifyCaps.d.ts +1 -1
  129. package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
  130. package/dist/core/caps/DbNotifyCaps.js +6 -4
  131. package/dist/core/caps/DbNotifyCaps.js.map +1 -1
  132. package/dist/core/caps/DbProjectionCaps.d.ts +1 -0
  133. package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
  134. package/dist/core/caps/DbProjectionCaps.js +29 -0
  135. package/dist/core/caps/DbProjectionCaps.js.map +1 -1
  136. package/dist/core/caps/DbSubscriptionCaps.d.ts +4 -6
  137. package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
  138. package/dist/core/caps/DbSubscriptionCaps.js +27 -13
  139. package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
  140. package/dist/core/caps/SchemeCtxImpl.d.ts +11 -3
  141. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  142. package/dist/core/caps/SchemeCtxImpl.js +10 -8
  143. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  144. package/dist/core/fork.d.ts.map +1 -1
  145. package/dist/core/fork.js +23 -7
  146. package/dist/core/fork.js.map +1 -1
  147. package/dist/core/fork.sql +35 -14
  148. package/dist/core/git-iso.d.ts +3 -2
  149. package/dist/core/git-iso.d.ts.map +1 -1
  150. package/dist/core/git-iso.js +27 -12
  151. package/dist/core/git-iso.js.map +1 -1
  152. package/dist/core/git-membership.d.ts +1 -0
  153. package/dist/core/git-membership.d.ts.map +1 -1
  154. package/dist/core/git-membership.js +87 -26
  155. package/dist/core/git-membership.js.map +1 -1
  156. package/dist/core/git-state.d.ts +8 -1
  157. package/dist/core/git-state.d.ts.map +1 -1
  158. package/dist/core/git-state.js +36 -17
  159. package/dist/core/git-state.js.map +1 -1
  160. package/dist/core/namespace.d.ts +1 -0
  161. package/dist/core/namespace.d.ts.map +1 -1
  162. package/dist/core/namespace.js +20 -0
  163. package/dist/core/namespace.js.map +1 -1
  164. package/dist/core/optimistic-settlement.d.ts +2 -0
  165. package/dist/core/optimistic-settlement.d.ts.map +1 -0
  166. package/dist/core/optimistic-settlement.js +14 -0
  167. package/dist/core/optimistic-settlement.js.map +1 -0
  168. package/dist/core/packet-wire.d.ts +5 -2
  169. package/dist/core/packet-wire.d.ts.map +1 -1
  170. package/dist/core/packet-wire.js +318 -114
  171. package/dist/core/packet-wire.js.map +1 -1
  172. package/dist/core/provider-accounting.d.ts +24 -0
  173. package/dist/core/provider-accounting.d.ts.map +1 -0
  174. package/dist/core/provider-accounting.js +76 -0
  175. package/dist/core/provider-accounting.js.map +1 -0
  176. package/dist/core/results.d.ts +4 -2
  177. package/dist/core/results.d.ts.map +1 -1
  178. package/dist/core/results.js +6 -0
  179. package/dist/core/results.js.map +1 -1
  180. package/dist/core/scheme-types.d.ts +0 -12
  181. package/dist/core/scheme-types.d.ts.map +1 -1
  182. package/dist/core/scheme-types.js.map +1 -1
  183. package/dist/core/search-gate.d.ts +2 -2
  184. package/dist/core/search-gate.d.ts.map +1 -1
  185. package/dist/core/search-gate.js +8 -8
  186. package/dist/core/search-gate.js.map +1 -1
  187. package/dist/core/token-ruler.js +1 -1
  188. package/dist/core/token-ruler.js.map +1 -1
  189. package/dist/core/turn-scheduler.js +1 -1
  190. package/dist/core/turn-scheduler.js.map +1 -1
  191. package/dist/core/worker-ops.sql +4 -4
  192. package/dist/core/workspace-settings.js +1 -1
  193. package/dist/core/workspace-settings.js.map +1 -1
  194. package/dist/digest/Digest.d.ts.map +1 -1
  195. package/dist/digest/Digest.js +349 -76
  196. package/dist/digest/Digest.js.map +1 -1
  197. package/dist/digest/digest.sql +42 -16
  198. package/dist/schemes/Exec.d.ts +4 -4
  199. package/dist/schemes/Exec.d.ts.map +1 -1
  200. package/dist/schemes/Exec.js +167 -134
  201. package/dist/schemes/Exec.js.map +1 -1
  202. package/dist/schemes/ExecOutputScheme.d.ts +4 -5
  203. package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
  204. package/dist/schemes/ExecOutputScheme.js +12 -20
  205. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  206. package/dist/schemes/File.d.ts +5 -6
  207. package/dist/schemes/File.d.ts.map +1 -1
  208. package/dist/schemes/File.js +35 -41
  209. package/dist/schemes/File.js.map +1 -1
  210. package/dist/schemes/Log.d.ts +16 -5
  211. package/dist/schemes/Log.d.ts.map +1 -1
  212. package/dist/schemes/Log.js +229 -262
  213. package/dist/schemes/Log.js.map +1 -1
  214. package/dist/schemes/Log.sql +50 -27
  215. package/dist/schemes/Prompt.d.ts +3 -4
  216. package/dist/schemes/Prompt.d.ts.map +1 -1
  217. package/dist/schemes/Prompt.js +5 -5
  218. package/dist/schemes/Prompt.js.map +1 -1
  219. package/dist/schemes/Skill.d.ts +4 -5
  220. package/dist/schemes/Skill.d.ts.map +1 -1
  221. package/dist/schemes/Skill.js +3 -3
  222. package/dist/schemes/Skill.js.map +1 -1
  223. package/dist/schemes/Worker.d.ts +7 -7
  224. package/dist/schemes/Worker.d.ts.map +1 -1
  225. package/dist/schemes/Worker.js +99 -84
  226. package/dist/schemes/Worker.js.map +1 -1
  227. package/dist/schemes/_entry-crud.d.ts +2 -2
  228. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  229. package/dist/schemes/_entry-crud.js +29 -10
  230. package/dist/schemes/_entry-crud.js.map +1 -1
  231. package/dist/schemes/_entry-crud.sql +11 -17
  232. package/dist/schemes/_entry-find.d.ts +41 -14
  233. package/dist/schemes/_entry-find.d.ts.map +1 -1
  234. package/dist/schemes/_entry-find.js +159 -166
  235. package/dist/schemes/_entry-find.js.map +1 -1
  236. package/dist/schemes/_entry-find.sql +2 -21
  237. package/dist/schemes/_entry-graph.sql +1 -1
  238. package/dist/schemes/_entry-manifest.d.ts +5 -17
  239. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  240. package/dist/schemes/_entry-manifest.js +28 -24
  241. package/dist/schemes/_entry-manifest.js.map +1 -1
  242. package/dist/schemes/_entry-ops.d.ts +13 -8
  243. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  244. package/dist/schemes/_entry-ops.js +98 -119
  245. package/dist/schemes/_entry-ops.js.map +1 -1
  246. package/dist/schemes/_entry-ops.sql +31 -6
  247. package/dist/schemes/_entry-semantic.d.ts +5 -7
  248. package/dist/schemes/_entry-semantic.d.ts.map +1 -1
  249. package/dist/schemes/_entry-semantic.js +10 -31
  250. package/dist/schemes/_entry-semantic.js.map +1 -1
  251. package/dist/schemes/_entry-semantic.sql +2 -2
  252. package/dist/schemes/_entry-send.js +4 -4
  253. package/dist/schemes/_entry-send.js.map +1 -1
  254. package/dist/schemes/_search-index.d.ts.map +1 -1
  255. package/dist/schemes/_search-index.js +16 -6
  256. package/dist/schemes/_search-index.js.map +1 -1
  257. package/dist/schemes/exec-abort.js +2 -2
  258. package/dist/schemes/exec-abort.js.map +1 -1
  259. package/dist/server/BranchBatches.d.ts +1 -0
  260. package/dist/server/BranchBatches.d.ts.map +1 -1
  261. package/dist/server/BranchBatches.js +1 -0
  262. package/dist/server/BranchBatches.js.map +1 -1
  263. package/dist/server/Daemon.d.ts +6 -8
  264. package/dist/server/Daemon.d.ts.map +1 -1
  265. package/dist/server/Daemon.js +238 -68
  266. package/dist/server/Daemon.js.map +1 -1
  267. package/dist/server/DaemonModule.d.ts +3 -3
  268. package/dist/server/DaemonModule.d.ts.map +1 -1
  269. package/dist/server/branch-batch.sql +3 -1
  270. package/dist/server/client-input.d.ts +2 -1
  271. package/dist/server/client-input.d.ts.map +1 -1
  272. package/dist/server/client-input.js +7 -1
  273. package/dist/server/client-input.js.map +1 -1
  274. package/dist/server/drain.sql +8 -7
  275. package/dist/server/envelope.d.ts +0 -2
  276. package/dist/server/envelope.d.ts.map +1 -1
  277. package/dist/server/envelope.js +0 -1
  278. package/dist/server/envelope.js.map +1 -1
  279. package/dist/server/envelope.sql +3 -4
  280. package/dist/server/lifecycle-recovery.sql +44 -1
  281. package/dist/server/logEntry.d.ts +2 -1
  282. package/dist/server/logEntry.d.ts.map +1 -1
  283. package/dist/server/logEntry.js +15 -2
  284. package/dist/server/logEntry.js.map +1 -1
  285. package/dist/server/logEntry.sql +5 -1
  286. package/dist/service.d.ts.map +1 -1
  287. package/dist/service.js +13 -1
  288. package/dist/service.js.map +1 -1
  289. package/migrations/001_schema.sql +730 -221
  290. package/package.json +53 -46
  291. package/dist/core/BudgetOverflow.d.ts +0 -23
  292. package/dist/core/BudgetOverflow.d.ts.map +0 -1
  293. package/dist/core/BudgetOverflow.js +0 -61
  294. package/dist/core/BudgetOverflow.js.map +0 -1
  295. package/dist/core/caps/DbTagCaps.d.ts +0 -10
  296. package/dist/core/caps/DbTagCaps.d.ts.map +0 -1
  297. package/dist/core/caps/DbTagCaps.js +0 -44
  298. package/dist/core/caps/DbTagCaps.js.map +0 -1
  299. package/dist/matrix/live-specimen.d.ts +0 -9
  300. package/dist/matrix/live-specimen.d.ts.map +0 -1
  301. package/dist/matrix/live-specimen.js +0 -37
  302. package/dist/matrix/live-specimen.js.map +0 -1
@@ -1,5 +1,6 @@
1
+ var _a;
1
2
  import { PlurnkParser, PlurnkParseError, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
2
- import { RuntimeTag } from "@plurnk/plurnk-execs";
3
+ import { RuntimeInvocation, RuntimeTag } from "@plurnk/plurnk-execs";
3
4
  import Owner from "./Owner.js";
4
5
  const TERMINAL_SEND_SIGNALS = new Set([102, 200, 202, 300, 499]);
5
6
  const comparePosition = (a, b) => a.line - b.line || a.column - b.column;
@@ -7,7 +8,6 @@ import { Mimetypes, emptyRegistry } from "@plurnk/plurnk-mimetypes";
7
8
  import Meta from "@plurnk/plurnk-meta";
8
9
  import EntryCrud from "../schemes/_entry-crud.js";
9
10
  import SearchIndex from "../schemes/_search-index.js";
10
- import { markTerminal } from "../schemes/Worker.js";
11
11
  import GitMembership from "./git-membership.js";
12
12
  import GitState from "./git-state.js";
13
13
  import WorkspaceSettings from "./workspace-settings.js";
@@ -20,13 +20,14 @@ import LoopLifecycle from "./LoopLifecycle.js";
20
20
  import { readFile } from "node:fs/promises";
21
21
  import { fileURLToPath } from "node:url";
22
22
  import { setTimeout as delay } from "node:timers/promises";
23
+ import { randomUUID } from "node:crypto";
23
24
  // Shared module imported by both Engine and bin/digest.ts, so wire
24
25
  // projection and digest projection are structurally one function — no
25
26
  // drift between wire and digest possible.
26
27
  import PacketWire from "./packet-wire.js";
27
28
  import Results, { OperationFailureError } from "./results.js";
28
29
  import BranchReceipt from "./BranchReceipt.js";
29
- import BudgetOverflow from "./BudgetOverflow.js";
30
+ import TerminalResult from "./TerminalResult.js";
30
31
  import WorkerControlAddress from "./WorkerControlAddress.js";
31
32
  import JournalTurn from "./JournalTurn.js";
32
33
  // The engine's collaborators — each owns one machine; Engine owns the loop/turn
@@ -41,7 +42,22 @@ import Dispatcher from "./Dispatcher.js";
41
42
  import { observed, observedSync } from "../observe/spans.js";
42
43
  import { OPS_DISPATCHED, PROVIDER_CALLS, recordCounter } from "../observe/metrics.js";
43
44
  import { scheduleTurnOps } from "./turn-scheduler.js";
44
- const DEFAULT_MAX_STRIKES = 3;
45
+ import { readOptimisticSettlementMs } from "./optimistic-settlement.js";
46
+ import { providerRequestFromStorageRow, } from "./provider-accounting.js";
47
+ import ModelCall, { ModelCallPersistenceError, ProviderAccountingIntegrityError, } from "./ModelCall.js";
48
+ const DEFAULT_MAX_STRIKES = 6;
49
+ const RECORD_STREAM_MIMETYPES = new Set(["application/jsonl", "application/x-ndjson"]);
50
+ const baseMimetype = (mimetype) => mimetype.split(";", 1)[0].trim().toLowerCase();
51
+ // {§exec-stream} Active streams publish only independently meaningful units.
52
+ // Atomic documents wait for close; JSONL stops after its last complete record.
53
+ const streamPublicationEnd = (content, mimetype, cursor, closed) => {
54
+ const type = baseMimetype(mimetype);
55
+ if (closed || type.startsWith("text/"))
56
+ return content.length;
57
+ if (!RECORD_STREAM_MIMETYPES.has(type))
58
+ return cursor;
59
+ return Math.max(cursor, content.lastIndexOf("\n") + 1);
60
+ };
45
61
  const ENGINE_PROBLEMS = Object.freeze({
46
62
  max_commands_exceeded: {
47
63
  status: 429,
@@ -51,7 +67,7 @@ const ENGINE_PROBLEMS = Object.freeze({
51
67
  idle_turn: {
52
68
  status: 409,
53
69
  code: "idle-turn",
54
- detail: "SEND[102] was emitted without an operation to continue from.",
70
+ detail: "`## SEND0 [102]` was emitted without an operation to continue from.",
55
71
  },
56
72
  });
57
73
  // The prompt entry target - prompt:///<loop>/<N>, self-only ({§prompt-self-only}):
@@ -102,7 +118,7 @@ const readMaxCommands = () => {
102
118
  return n;
103
119
  };
104
120
  // PLURNK_SERVICE_FILES_ITEMS — the turn-0 catalog preview. null = off;
105
- // -1 = the complete one-level map; positive N caps its file rows. 0 / unset = off.
121
+ // -1 = the ordinary markerless page; positive N explicitly caps file rows. 0 / unset = off.
106
122
  const normalizeFilesItems = (n) => (!Number.isFinite(n) || n === 0 ? null : n < 0 ? -1 : n);
107
123
  const readFilesItems = () => {
108
124
  const raw = process.env.PLURNK_SERVICE_FILES_ITEMS;
@@ -110,13 +126,55 @@ const readFilesItems = () => {
110
126
  return null;
111
127
  return normalizeFilesItems(Number.parseInt(raw, 10));
112
128
  };
113
- import { ProviderError, providerCostFor, providerCostUsd, validateProviderCost, scopeEnvToAlias, resolveActiveAlias } from "@plurnk/plurnk-providers";
129
+ import { aggregateProviderAccounting, ProviderError, resolveActiveAlias, scopeEnvToAlias, } from "@plurnk/plurnk-providers";
114
130
  import { validateGbnf } from "@plurnk/gbnf";
115
131
  import ProviderInstantiate from "./ProviderInstantiate.js";
132
+ // A generated rail may constrain only the sampled continuation while the chat
133
+ // template contributes a prefix that llama-server preserves in its response.
134
+ // The artifact names the alternate root that composes those bytes for evidence
135
+ // grading; arbitrary grammars without the declaration retain their transport root.
136
+ const grammarConstraint = (grammar) => {
137
+ const declarations = [...grammar.matchAll(/^# @plurnk-response-root ([A-Za-z][A-Za-z0-9-]*)$/gm)];
138
+ if (declarations.length === 0)
139
+ return { transport: grammar, response: grammar };
140
+ if (declarations.length !== 1)
141
+ throw new Error("GBNF constraint declares multiple @plurnk-response-root values");
142
+ const responseRoot = declarations[0][1];
143
+ if (!grammar.split("\n").some((line) => line.startsWith(`${responseRoot} ::=`))) {
144
+ throw new Error(`GBNF constraint declares missing response root ${responseRoot}`);
145
+ }
146
+ const roots = [...grammar.matchAll(/^root ::= ([A-Za-z][A-Za-z0-9-]*)$/gm)];
147
+ if (roots.length !== 1)
148
+ throw new Error("GBNF constraint must declare exactly one simple root");
149
+ return {
150
+ transport: grammar,
151
+ response: grammar.replace(roots[0][0], `root ::= ${responseRoot}`),
152
+ };
153
+ };
154
+ const TOKEN_BUDGET_OVERFLOW_DETAIL = "Token Budget Overflow: Token Usage exceeded Token Ceiling. Newest log items were automatically FOLDed to fit within token budget. Curate the log and/or perform more conservatively scoped or chunked retrieval operations to recover.";
155
+ const tokenBudgetOverflowFailure = (pressure) => Results.failure("engine:context", "token-budget-overflow", 413, TOKEN_BUDGET_OVERFLOW_DETAIL, {}, { ...pressure });
156
+ const budgetPressure = (usage, ceiling) => {
157
+ if (usage <= ceiling)
158
+ throw new Error("context-envelope admission requires positive ruler debt");
159
+ return { usage, ceiling, deficit: usage - ceiling };
160
+ };
161
+ const contextEnvelopeFailure = (pressure, admission) => Results.failure("engine:context", "context-envelope-admission-failed", 413, `The configured context envelope cannot admit this request: ${admission.detail}.`, {}, {
162
+ ...pressure,
163
+ stage: "context-envelope-admission",
164
+ contextAdmission: admission.reason,
165
+ contextCapacity: admission.capacity,
166
+ ...(admission.measurement === undefined ? {} : {
167
+ promptTokens: admission.measurement.tokens,
168
+ tokenKind: admission.measurement.kind,
169
+ tokenSource: admission.measurement.source,
170
+ }),
171
+ retryable: false,
172
+ });
116
173
  // Runtime normalization for a disposition the engine refuses or resolves as a
117
174
  // continue after dispatch ({§send}). Every admitted emission itself ends in an
118
175
  // explicit disposition SEND ({§emission-admission}).
119
176
  const TURN_STATUS_IMPLICIT_CONTINUE = 102;
177
+ const INVALID_EMISSION_RECOVERY_MESSAGE = "Your previous response contained an unrecoverable syntax error. No operations were performed. Try again.";
120
178
  const DEFAULT_MIN_CYCLES = 3;
121
179
  const DEFAULT_MAX_CYCLE_PERIOD = 4;
122
180
  const readPositiveInt = (envVar, fallback) => {
@@ -141,7 +199,7 @@ const DEFAULT_LOOP_TIMEOUT_MS = 86400000;
141
199
  const readLoopTimeoutMs = () => readPositiveInt("PLURNK_SERVICE_LOOP_TIMEOUT", DEFAULT_LOOP_TIMEOUT_MS);
142
200
  // The wall's abort reason — runLoop branches a mid-turn teardown to the 504 terminal on it.
143
201
  const LOOP_TIMEOUT_REASON = "loop_timeout";
144
- export default class Engine {
202
+ class Engine {
145
203
  static fingerprintTurn(ops) {
146
204
  return StrikeRail.fingerprintTurn(ops);
147
205
  }
@@ -158,7 +216,7 @@ export default class Engine {
158
216
  // Boot-discovered runtime executors. Daemon builds + sets via
159
217
  // setExecutors at start(); undefined until then (and in bare tests).
160
218
  #executors;
161
- // {§send-premature-terminate}/SEND[202]<T> — park deadlines by loopId, written at dispatch (the
219
+ // {§send-premature-terminate}/scoped SEND signal 202 — park deadlines by loopId, written at dispatch (the
162
220
  // marker's seconds; -1 = indefinite), consumed by the daemon's drain park-exit to schedule
163
221
  // the deadline wake. In-memory: a daemon restart drops pending deadlines (documented).
164
222
  parkDeadlines = new Map();
@@ -170,9 +228,6 @@ export default class Engine {
170
228
  #notices;
171
229
  #problems;
172
230
  #strikes;
173
- // {§grinder-hard-413-recovery} - loops granted their one over-ceiling recovery turn. Cleared on a
174
- // fitting turn so a later independent overflow can earn a fresh recovery, and at loop cleanup.
175
- #hardOverflowRecovery = new Set();
176
231
  #packets;
177
232
  searchGate = new SearchGate();
178
233
  #proposals;
@@ -208,7 +263,13 @@ export default class Engine {
208
263
  if (!invalidate && this.#workspaceWarmStatus.get(workspaceId)?.phase === "complete") {
209
264
  return Promise.resolve();
210
265
  }
211
- const state = { dirty: false, materialize, ctx, promise: Promise.resolve() };
266
+ const state = {
267
+ dirty: false,
268
+ materialize,
269
+ ctx,
270
+ abort: new AbortController(),
271
+ promise: Promise.resolve(),
272
+ };
212
273
  // Register before publishing the first synchronous Notice. A
213
274
  // listener may request another warm from that callback; it must join
214
275
  // this state rather than opening a second pump in the re-entrant gap.
@@ -231,10 +292,14 @@ export default class Engine {
231
292
  completed: 0, total: 1, percent: 0, level: "info",
232
293
  });
233
294
  try {
295
+ const signal = current.signal === undefined
296
+ ? state.abort.signal
297
+ : AbortSignal.any([current.signal, state.abort.signal]);
298
+ const cancellable = { ...current, signal };
234
299
  if (shouldMaterialize)
235
- await GitMembership.indexGitMembership(current);
300
+ await GitMembership.indexGitMembership(cancellable);
236
301
  await SearchIndex.maintain({
237
- ...current,
302
+ ...cancellable,
238
303
  pushNotice: (notice) => {
239
304
  if (notice.kind === "embed_progress"
240
305
  && typeof notice.completed === "number"
@@ -277,9 +342,26 @@ export default class Engine {
277
342
  workspaceDerivationStatus(workspaceId) {
278
343
  return this.#workspaceWarmStatus.get(workspaceId) ?? null;
279
344
  }
280
- // Awaited by Daemon.stop before the db closes.
281
- async drainDerivations() {
282
- await Promise.all([...this.#workspaceWarms.values()].map((state) => state.promise));
345
+ cancelDerivations(reason = new DOMException("derivations cancelled", "AbortError")) {
346
+ for (const state of this.#workspaceWarms.values()) {
347
+ if (!state.abort.signal.aborted)
348
+ state.abort.abort(reason);
349
+ }
350
+ }
351
+ // Awaited by Daemon.stop before the db closes. Shutdown supplies the exact
352
+ // cancellation reason it owns; every unrelated failure remains visible.
353
+ async drainDerivations(ignoredReason) {
354
+ const results = await Promise.allSettled([...this.#workspaceWarms.values()].map((state) => state.promise));
355
+ const errors = results
356
+ .filter((result) => result.status === "rejected")
357
+ .flatMap((result) => result.reason instanceof AggregateError
358
+ ? [...result.reason.errors]
359
+ : [result.reason])
360
+ .filter((error) => error !== ignoredReason);
361
+ if (errors.length === 1)
362
+ throw errors[0];
363
+ if (errors.length > 1)
364
+ throw new AggregateError(errors, "derivation drain failed");
283
365
  }
284
366
  async drainWorkspaceDerivations(workspaceId) {
285
367
  await this.#workspaceWarms.get(workspaceId)?.promise;
@@ -294,9 +376,9 @@ export default class Engine {
294
376
  // parse): warn once per message, never per turn; the turn records railsVerdict "unverifiable".
295
377
  static #railGapWarned = new Set();
296
378
  static #warnRailVerdictGapOnce(message) {
297
- if (Engine.#railGapWarned.has(message))
379
+ if (_a.#railGapWarned.has(message))
298
380
  return;
299
- Engine.#railGapWarned.add(message);
381
+ _a.#railGapWarned.add(message);
300
382
  process.stderr.write(`plurnk-engine: rail verdict unavailable — the configured grammar did not parse in @plurnk/gbnf (${message})\n`);
301
383
  }
302
384
  static #requireGrammarEvidence(response) {
@@ -330,7 +412,8 @@ export default class Engine {
330
412
  discovery: { registry: emptyRegistry(), handlers: new Map(), skipped: [] },
331
413
  });
332
414
  // {§tokenomics-agnostic-ruler} — standalone construction and the daemon
333
- // use the same default; provider counting is confined to physical admission.
415
+ // use the same default; provider counting is confined to hard
416
+ // context-envelope admission.
334
417
  this.#tokenize = tokenize ?? rulerCount;
335
418
  const executors = () => this.#executors;
336
419
  const loopSignal = (loopId) => this.#loopAborts.get(loopId)?.signal;
@@ -340,7 +423,6 @@ export default class Engine {
340
423
  this.#packets = new PacketBuilder({
341
424
  db,
342
425
  schemes,
343
- problems: this.#problems,
344
426
  executors,
345
427
  });
346
428
  this.#proposals = new ProposalLifecycle({
@@ -385,12 +467,16 @@ export default class Engine {
385
467
  if (this.#executors === undefined)
386
468
  throw new Error("registerRuntime: executor registry not wired yet");
387
469
  RuntimeTag.assert(tag, "module runtime");
470
+ const normalized = {
471
+ ...entry,
472
+ invocation: RuntimeInvocation.assert(entry.invocation, entry.namespaceOwner.name, tag),
473
+ };
388
474
  // Preflight both owners before either write; synchronous registration
389
475
  // then cannot leave a half-claimed namespace. {§plugin-namespace-arbitration}
390
- this.#executors.assertCanRegister(tag, entry.namespaceOwner);
391
- this.#schemes.assertRuntimeClaim(tag, entry.namespaceOwner);
392
- this.#schemes.registerRuntimeScheme(tag, entry.executor, entry.namespaceOwner, scheme);
393
- this.#executors.register(tag, entry);
476
+ this.#executors.assertCanRegister(tag, normalized.namespaceOwner);
477
+ this.#schemes.assertRuntimeClaim(tag, normalized.namespaceOwner);
478
+ this.#schemes.registerRuntimeScheme(tag, normalized.executor, normalized.namespaceOwner, scheme);
479
+ this.#executors.register(tag, normalized);
394
480
  }
395
481
  // Supply an explicitly configured local constraint; ANTLR remains the
396
482
  // language authority. {§grammar-enforcement-verified-at-boot}
@@ -414,9 +500,10 @@ export default class Engine {
414
500
  ? variant
415
501
  : fileURLToPath(import.meta.resolve(`@plurnk/plurnk-contracts/${variant}`));
416
502
  const text = await readFile(path, "utf8"); // unresolvable/unreadable throws — a configured rail never silently degrades
417
- this.#gbnfCache.set(variant, text);
503
+ const constraint = grammarConstraint(text);
504
+ this.#gbnfCache.set(variant, constraint);
418
505
  process.stderr.write(`plurnk-engine: GBNF constraint: ${alias || "(bare)"} → ${variant} (${text.length} chars)\n`);
419
- return text;
506
+ return constraint;
420
507
  }
421
508
  // A lineage's no-parent root; a root worker resolves to itself. Fail hard
422
509
  // when corruption leaves a worker without one. {§worker-primary}
@@ -426,6 +513,16 @@ export default class Engine {
426
513
  throw new Error(`resolveWorkerPrimary: worker ${workerId} has no lineage root — corrupt parent chain`);
427
514
  return root.id;
428
515
  }
516
+ async resolveWorkerProviderIdentity(workerId) {
517
+ const identity = await this.#db.engine_worker_provider_identity.get({ worker_id: workerId });
518
+ if (identity === undefined) {
519
+ throw new Error(`resolveWorkerProviderIdentity: worker ${workerId} has no lineage root — corrupt parent chain`);
520
+ }
521
+ return {
522
+ workerId: identity.worker_id,
523
+ primaryWorkerId: identity.primary_worker_id,
524
+ };
525
+ }
429
526
  promptBudgetFor(provider) {
430
527
  return this.#packets.promptBudgetFor(provider);
431
528
  }
@@ -433,6 +530,109 @@ export default class Engine {
433
530
  const tags = Meta.composeAttributions(this.#schemes.attributions(context), this.#executors?.attributions(context) ?? [], await this.#mimetypes.attributions(context), provider.attributions?.(context) ?? []);
434
531
  return [...tags];
435
532
  }
533
+ #providerAttributions(provider, context) {
534
+ return [...Meta.composeAttributions(provider.attributions?.(context) ?? [])];
535
+ }
536
+ static #providerFailure(error, signal) {
537
+ if (error instanceof ProviderError) {
538
+ return { status: error.problem.status, problem: error.problem };
539
+ }
540
+ if (signal?.aborted === true) {
541
+ const timedOut = signal.reason === LOOP_TIMEOUT_REASON;
542
+ return Results.failure("engine:provider", timedOut ? "provider-call-timeout" : "provider-call-cancelled", timedOut ? 504 : 499, timedOut
543
+ ? "The provider call was interrupted by the loop deadline."
544
+ : "The provider call was interrupted by loop cancellation.", {}, { stage: "provider-request", retryable: false });
545
+ }
546
+ console.error("Provider failed outside its Problem Details contract:", error);
547
+ return Results.failure("engine:provider", "provider-contract-violation", 502, "The provider failed without returning its required Problem Details.", {}, { stage: "provider-request", retryable: false });
548
+ }
549
+ async #runBareBatch({ statements, provider, turnId, modelCallSequenceStart, workspaceId, workerId, primaryWorkerId, loopSequence, turnSequence, signal, }) {
550
+ const prepared = [];
551
+ for (const [index, statement] of statements.entries()) {
552
+ const providerWorkerId = randomUUID();
553
+ const attributionContext = Object.freeze({
554
+ workspaceId: String(workspaceId),
555
+ workerId: providerWorkerId,
556
+ primaryWorkerId,
557
+ loop: loopSequence,
558
+ turn: turnSequence,
559
+ attempt: 1,
560
+ });
561
+ const attributions = this.#providerAttributions(provider, attributionContext);
562
+ const modelCall = await ModelCall.open(this.#db, {
563
+ turnId,
564
+ sequence: modelCallSequenceStart + index,
565
+ kind: "bare",
566
+ attributions,
567
+ model: provider.model,
568
+ });
569
+ prepared.push({ statement, modelCall, attributions, providerWorkerId });
570
+ }
571
+ const maxTokens = this.#packets.maxTokensFor(provider) ?? undefined;
572
+ const settlements = await Promise.allSettled(prepared.map(async ({ statement, modelCall, attributions, providerWorkerId }) => {
573
+ try {
574
+ const response = await observed("provider.generate", { model: provider.model, attempt: 1, kind: "bare" }, async (span) => {
575
+ try {
576
+ const generated = await provider.generate({
577
+ messages: [{ role: "user", content: statement.body }],
578
+ workerId: providerWorkerId,
579
+ primaryWorkerId,
580
+ signal,
581
+ maxTokens,
582
+ attributions: attributions.length > 0 ? attributions : undefined,
583
+ workspaceId: String(workspaceId),
584
+ loop: loopSequence,
585
+ turn: turnSequence,
586
+ observeRequest: modelCall.observeRequest,
587
+ callKind: "bare",
588
+ });
589
+ modelCall.assertAccounting(generated.accounting);
590
+ recordCounter(PROVIDER_CALLS, {
591
+ model: provider.model,
592
+ attempt: 1,
593
+ status: "resolved",
594
+ });
595
+ span.setAttribute("status", "resolved");
596
+ return generated;
597
+ }
598
+ catch (error) {
599
+ if (error instanceof ProviderError) {
600
+ modelCall.assertAccounting(error.accounting);
601
+ }
602
+ throw error;
603
+ }
604
+ });
605
+ await modelCall.observeResponse(response);
606
+ return {
607
+ statement,
608
+ modelCallId: modelCall.id,
609
+ result: Results.assert({
610
+ status: 200,
611
+ content: response.assistant.content,
612
+ mimetype: "text/plain",
613
+ }),
614
+ };
615
+ }
616
+ catch (error) {
617
+ if (error instanceof ModelCallPersistenceError || error instanceof ProviderAccountingIntegrityError) {
618
+ throw error;
619
+ }
620
+ const failure = _a.#providerFailure(error, signal);
621
+ if (error instanceof ProviderError && error.attempt !== undefined) {
622
+ await modelCall.observeResponse(error.attempt, failure);
623
+ }
624
+ else {
625
+ await modelCall.fail(failure);
626
+ }
627
+ return { statement, modelCallId: modelCall.id, result: failure };
628
+ }
629
+ }));
630
+ const internalFailure = settlements.find((settlement) => settlement.status === "rejected");
631
+ if (internalFailure !== undefined)
632
+ throw internalFailure.reason;
633
+ signal?.throwIfAborted();
634
+ return settlements.map((settlement) => settlement.value);
635
+ }
436
636
  // {§attribution} — reporting derives from exact provider-request evidence;
437
637
  // malformed durable tags fail here instead of being silently filtered.
438
638
  async loopAttributions(loopId) {
@@ -445,24 +645,22 @@ export default class Engine {
445
645
  });
446
646
  return [...Meta.composeAttributions(tags)];
447
647
  }
448
- // Loop totals are billing evidence; the latest-turn pair is the client
449
- // occupancy gauge. {§tokenomics-client-gauge}, {§notifications-loop-terminated}
648
+ // The request ledger is cardinal evidence; its aggregate and the latest-turn
649
+ // occupancy gauge are derived projections. {§tokenomics-client-gauge},
650
+ // {§notifications-loop-terminated}, {§provider-accounting}
450
651
  async loopUsage(loopId) {
451
652
  const row = await this.#db.engine_loop_usage.get({ loop_id: loopId });
452
- const parsedCosts = JSON.parse(row?.costs ?? "[]");
453
- if (!Array.isArray(parsedCosts))
454
- throw new TypeError(`loop ${loopId} monetary evidence is not an array`);
653
+ if (row === undefined)
654
+ throw new Error(`loopUsage: loop ${loopId} does not exist`);
655
+ const requests = await this.#db.engine_loop_provider_requests.all({ loop_id: loopId });
455
656
  return {
456
- promptTokens: row?.prompt ?? 0,
457
- completionTokens: row?.completion ?? 0,
458
- costUsd: row?.cost_usd ?? null,
459
- costs: parsedCosts.map((cost) => validateProviderCost(cost)),
460
- // Latest provider attempt on the latest turn, not the billed total.
461
- contextTokens: row?.context ?? 0,
657
+ accounting: aggregateProviderAccounting(requests.map(providerRequestFromStorageRow)),
658
+ // Latest physical request on the latest turn, not the billed total.
659
+ contextTokens: row.context,
462
660
  // Latest effective packet allowance; null when uncapped or unknown.
463
- promptBudget: row?.context_size ?? null,
661
+ promptBudget: row.context_size,
464
662
  // Latest turn's opaque provider metadata. {§meta-passthrough}
465
- meta: JSON.parse(row?.meta ?? "{}"),
663
+ meta: JSON.parse(row.meta ?? "{}"),
466
664
  };
467
665
  }
468
666
  // A mapped rail divergence is a CODE-POINT offset into the model's content;
@@ -485,11 +683,12 @@ export default class Engine {
485
683
  }
486
684
  return { line, column };
487
685
  }
488
- async runLoop({ provider, messages, requirements = "", workspaceId, workerId, loopId, maxTurns = 50, maxStrikes = readMaxStrikes(), minCycles = readPositiveInt("PLURNK_SERVICE_MIN_CYCLES", DEFAULT_MIN_CYCLES), maxCyclePeriod = readPositiveInt("PLURNK_SERVICE_MAX_CYCLE_PERIOD", DEFAULT_MAX_CYCLE_PERIOD), origin = "model", signal, onDispatch, }) {
686
+ async runLoop({ provider, childProvider = provider, messages, requirements = "", workspaceId, workerId, loopId, maxTurns = 50, maxStrikes = readMaxStrikes(), minCycles = readPositiveInt("PLURNK_SERVICE_MIN_CYCLES", DEFAULT_MIN_CYCLES), maxCyclePeriod = readPositiveInt("PLURNK_SERVICE_MAX_CYCLE_PERIOD", DEFAULT_MAX_CYCLE_PERIOD), origin = "model", signal, onDispatch, }) {
489
687
  // A 202 park suspends this durable loop and a later wake re-enters runLoop.
490
688
  // Its ceiling therefore counts every prior turn, not merely this process-local
491
689
  // execution segment.
492
690
  const turnIds = await this.#lifecycle.turnIds(loopId);
691
+ let invalidEmissionRecoveryEntryId = null;
493
692
  // Per-loop AbortController for scheme-side cancellation propagation.
494
693
  // Chained from the caller's `signal` so an external abort cascades.
495
694
  const loopAbort = new AbortController();
@@ -520,11 +719,11 @@ export default class Engine {
520
719
  return { turnIds, result, hitMaxTurns: false, reason: "loop_timeout" };
521
720
  };
522
721
  // Cleanup splits by termination kind:
523
- // - "graceful" (SEND[202] Accepted): in-flight streaming-scheme spawns
722
+ // - "graceful" (SEND signal 202 Accepted): in-flight streaming-scheme spawns
524
723
  // are ALLOWED to outlive the loop — they complete naturally, write final
525
724
  // channel state, and wake-on-completion (E.4) opens a fresh loop. 202 is
526
725
  // the only terminal that means "keep my async work."
527
- // - "forceful" (SEND[200] done, max_turns, strike, cancel, budget, 4xx/5xx):
726
+ // - "forceful" (SEND signal 200 done, max_turns, strike, cancel, context-envelope rejection, 4xx/5xx):
528
727
  // fire the loop-level abort so leftover spawns tear down. "Done" reaps.
529
728
  const cleanup = (kind, reason) => {
530
729
  clearTimeout(wall);
@@ -534,7 +733,6 @@ export default class Engine {
534
733
  this.#loopAborts.delete(loopId);
535
734
  this.#strikes.delete(loopId);
536
735
  this.searchGate.cleanup(loopId);
537
- this.#hardOverflowRecovery.delete(loopId);
538
736
  this.#notices.delete(loopId);
539
737
  };
540
738
  while (true) {
@@ -547,7 +745,7 @@ export default class Engine {
547
745
  // check, {§worker-lifecycle-wake-requeue-not-terminal}). The wake's intent is KEEP
548
746
  // RUNNING: re-claim atomically and continue — the injected prompt is already
549
747
  // this loop's next turn. Returning it as "external" broadcast a QUEUED loop
550
- // as a terminal result with status 100 — the delegation-flags flake.
748
+ // as a terminal result with status 100 — the delegation-flags race.
551
749
  await this.#db.engine_reclaim_queued_loop.run({ loop_id: loopId });
552
750
  continue; // claimed (or a racer flipped it first — the re-read decides)
553
751
  }
@@ -584,10 +782,6 @@ export default class Engine {
584
782
  cleanup("forceful", "max_turns");
585
783
  return { turnIds, result, hitMaxTurns: true, reason: "max_turns" };
586
784
  }
587
- // PLURNK_SERVICE_EXEC_WAIT_MS — a post-EXEC breath: if a spawn from the prior turn
588
- // is still in flight, give it a tunable beat to land in THIS turn's packet
589
- // before we assemble it. A fixed grace beat, never a wait-for-completion;
590
- // 0/unset = off. Abortable with the loop signal.
591
785
  const execHandler = this.#schemes.get("exec");
592
786
  // {§exec-hold-until-concluded} — hold matching runtime/effect
593
787
  // streams until conclusion or the fail-open cap, then resume the
@@ -600,19 +794,15 @@ export default class Engine {
600
794
  await delay(150, undefined, { signal });
601
795
  }
602
796
  }
603
- const execWaitMs = Number(process.env.PLURNK_SERVICE_EXEC_WAIT_MS ?? "0");
604
- if (execWaitMs > 0) {
605
- if (execHandler?.hasActiveSpawns?.(workerId) === true)
606
- await delay(execWaitMs, undefined, { signal });
607
- }
608
797
  let turn;
609
798
  const releaseWorkspace = await this.#acquireWorkspaceTurn(workspaceId, workerId);
610
799
  try {
611
800
  turn = await observed(// {§observability-boundary}
612
801
  "loop.turn", { workerId, "loop.id": loopId }, async (span) => {
613
802
  const t = await this.runTurn({
614
- provider, messages, requirements, workspaceId, workerId, loopId, origin, signal, onDispatch,
803
+ provider, childProvider, messages, requirements, workspaceId, workerId, loopId, origin, signal, onDispatch,
615
804
  turnNumber: turnIds.length + 1, maxTurns,
805
+ invalidEmissionRecoveryEntryId,
616
806
  });
617
807
  span.setAttribute("turn.id", t.turnId);
618
808
  return t;
@@ -635,10 +825,18 @@ export default class Engine {
635
825
  releaseWorkspace();
636
826
  }
637
827
  turnIds.push(turn.turnId);
638
- // Invalid provider emissions are retried beneath this turn and never
639
- // reach the strike rail. Exhausting that inner attempt budget is a
640
- // terminal generation failure, not one of the engine's three strikes.
828
+ // {§invalid-emission-attempts} Invalid provider emissions are retried beneath this turn and never
829
+ // reach the strike rail. The first consecutive exhaustion has already
830
+ // exposed its bounded lifeline through ordinary next-turn state. A
831
+ // second exhaustion is terminal; an admitted turn clears the sequence.
641
832
  if (turn.emissionExhausted) {
833
+ if (invalidEmissionRecoveryEntryId === null) {
834
+ if (turn.rejectedModelEntryId === undefined) {
835
+ throw new Error("an admitted invalid-emission recovery requires its rejected model-entry identity");
836
+ }
837
+ invalidEmissionRecoveryEntryId = turn.rejectedModelEntryId;
838
+ continue;
839
+ }
642
840
  const failure = Results.failure("engine:generation", "invalid-emission-exhausted", 500, `No valid PLAN...SEND turn was received after ${turn.emissionAttempts} emission attempts.`, {}, {
643
841
  attempts: turn.emissionAttempts,
644
842
  stage: "emission-validation",
@@ -650,25 +848,24 @@ export default class Engine {
650
848
  cleanup("forceful", "invalid_emission");
651
849
  return { turnIds, result, hitMaxTurns: false, reason: "invalid_emission" };
652
850
  }
653
- // SPEC {§grinder}: budget hard-stop — packet won't fit even collapsed → abandon.
851
+ invalidEmissionRecoveryEntryId = null;
852
+ // SPEC {§grinder}: the effective context envelope rejected the request.
654
853
  if (turn.budgetHardStop) {
655
- if (turn.budget === undefined) {
656
- throw new Error("a budget hard-stop requires its measured overflow");
854
+ if (turn.budgetFailure === undefined) {
855
+ throw new Error("a context-envelope hard-stop requires its exact failure");
657
856
  }
658
- const failure = BudgetOverflow.result(turn.budget.usage, turn.budget.ceiling, false);
659
- const result = await this.#lifecycle.finish(loopId, failure);
857
+ const result = await this.#lifecycle.finish(loopId, turn.budgetFailure);
660
858
  if (result === null)
661
859
  throw new Error(`loop ${loopId} became terminal before budget settlement`);
662
- cleanup("forceful", "budget_overflow");
663
- return { turnIds, result, hitMaxTurns: false, reason: "budget_overflow" };
860
+ cleanup("forceful", "context_envelope");
861
+ return { turnIds, result, hitMaxTurns: false, reason: "context_envelope" };
664
862
  }
665
- // {§engine-rails} — per-turn strike accounting (cycle detection, the
666
- // grinder/steer coupling, hard-failure statuses). StrikeRail owns the
863
+ // {§engine-rails} — per-turn strike accounting (cycle detection,
864
+ // steer coupling, hard operation outcomes). StrikeRail owns the
667
865
  // bookkeeping; runLoop owns abandonment.
668
866
  const verdict = this.#strikes.assess(loopId, {
669
867
  fingerprint: turn.fingerprint,
670
- statuses: turn.statuses,
671
- budgetStruck: turn.budgetStruck,
868
+ outcomes: turn.outcomes,
672
869
  steerStruck: turn.steerStruck,
673
870
  minCycles, maxCyclePeriod, maxStrikes,
674
871
  });
@@ -691,7 +888,11 @@ export default class Engine {
691
888
  }
692
889
  }
693
890
  }
694
- async runTurn({ provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, }) {
891
+ async runTurn({ provider, childProvider = provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, invalidEmissionRecoveryEntryId, }) {
892
+ const allowInvalidEmissionRecovery = invalidEmissionRecoveryEntryId === null;
893
+ const transientOpenLogEntryId = typeof invalidEmissionRecoveryEntryId === "number"
894
+ ? invalidEmissionRecoveryEntryId
895
+ : null;
695
896
  // === Turn-as-container model ===
696
897
  //
697
898
  // Turn rows are created at runTurn OPEN (status=102, placeholder
@@ -748,14 +949,14 @@ export default class Engine {
748
949
  // Model ops dispatch after these pre-model rows.
749
950
  let nextActionIndex = 1;
750
951
  const turnOpenPaths = [];
751
- // {§model-entry} — the worker's first turn opens with the model's own turn-0, mirrored OPEN: a
752
- // worked turn PLAN → the environment FINDs the foist ACTUALLY dispatches → SEND[102]. Built
952
+ // {§worker-initialization-entry} — the worker's first turn opens with a kernel-authored initialization: a
953
+ // worked turn PLAN → the environment FINDs the foist ACTUALLY dispatches → SEND signal 102. Built
753
954
  // from the real ops below (not a static print — we lean into the genuine echo paradigm) and
754
- // written at sequence 1, so it reads first as the emission with the foisted results following.
955
+ // written at sequence 1, so it reads first with the foisted results following.
755
956
  const turnZeroMoves = [];
756
957
  if (seq === 1) {
757
958
  if (workerFirstLoop)
758
- nextActionIndex = 2; // reserve sequence 1 for the turn-0 echo
959
+ nextActionIndex = 2; // reserve sequence 1 for initialization
759
960
  // Operator doc READs (PLURNK_SERVICE_MD_<ALIAS>, {§actor-boundary-doc-injection}). The docs were materialized
760
961
  // as worker://plurnk/<entry> entries by the plurnk worker (LoopDocs, via the
761
962
  // {§actor-boundary} keystone); foist a READ of each into THIS turn-0 so the model
@@ -788,7 +989,6 @@ export default class Engine {
788
989
  const promptPath = promptTarget(promptLoopSeq, seq);
789
990
  const entry = {
790
991
  channels: { body: { content: promptRow.prompt, mimetype: "text/markdown" } },
791
- tags: [],
792
992
  attributes: { openPaths },
793
993
  };
794
994
  await EntryCrud.writeEntry(promptPath.pathname, entry, systemCtx, "prompt", workerId);
@@ -859,102 +1059,81 @@ export default class Engine {
859
1059
  // The warm materialized membership before deriving. This second pass is the
860
1060
  // ordinary cheap change detector and captures any drift that landed meanwhile.
861
1061
  const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
862
- await this.#logFsFictions(workspaceId, fsDivergences);
1062
+ // {§packet-git-status} — one post-reconciliation snapshot supplies both
1063
+ // the compact packet summary and each causal file event's exact XY state.
1064
+ const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopAborts.get(loopId)?.signal);
1065
+ await this.#logFsFictions(workspaceId, fsDivergences, gitStatus);
863
1066
  // The refresh above may have changed bodies (including model/client edits
864
1067
  // since the startup warm). Re-derive to completion before packet/model
865
1068
  // construction. Membership is already current, so this pass does not
866
1069
  // consume the filesystem divergences a second time.
867
1070
  await this.#queueWorkspaceWarm(systemCtx, true, false);
868
1071
  // Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, {§actor-boundary-catalog-preview}):
869
- // FIND surveys foisted into the worker's first model turn so it opens with its catalog.
870
- // Folder-capable surfaces reveal one level with `*`; each deeper directory is an
871
- // actionable `dir/**` aggregate. The curated kernel docs remain recursive, so the
872
- // opening packet demonstrates both navigation forms. Empty results are orientation.
1072
+ // Four FIND surveys foisted into the worker's first model turn establish the project,
1073
+ // commons, private, and documentation surfaces in that order. Their `init`
1074
+ // classification lets the model curate this opening survey as one log set.
873
1075
  if (seq === 1) {
874
1076
  // {§operator-config-workspace-files-items} — workspace filesItems replaces the env default.
875
1077
  const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
876
1078
  const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
877
1079
  if (filesItems !== null && workerFirstLoop) { // {§actor-boundary-catalog-preview} — once per worker
878
- // engine_scheme_catalog_summary is the workspace-bounded scheme source: ordered,
879
- // one row per stored entry scheme. log:// is absent —
880
- // it lives in log_entries, not the catalog (present-mode, the # Log section).
881
1080
  const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
882
- // Entry-bearing plugin schemes foist alongside the four structural surveys below.
883
- const foistSchemes = catalogSchemes
884
- .filter((catalog) => catalog.scheme !== "prompt" && catalog.scheme !== "worker")
885
- .map(({ scheme, shallow_items }) => ({ scheme, shallow_items }));
886
- // Commons + project files always foist. An empty result establishes that the
887
- // surface exists and currently contains nothing.
888
- foistSchemes.push({
889
- scheme: "worker",
890
- shallow_items: catalogSchemes.find((c) => c.scheme === "worker")?.shallow_items ?? 0,
891
- });
892
- if (!foistSchemes.some((c) => c.scheme === "file"))
893
- foistSchemes.push({ scheme: "file", shallow_items: 0 }); // Empty project surface still receives its orienting FIND.
894
- for (const { scheme, shallow_items: shallowItems } of foistSchemes) {
895
- const isFile = scheme === "file";
896
- const pattern = this.#schemes.manifestFor(scheme)?.folderScopes === true ? "*" : "**";
897
- // Only the file map takes PLURNK_SERVICE_FILES_ITEMS as a first-N cap;
898
- // other schemes always render their complete one-level map. An empty file
899
- // survey has no range: `<1,0>` would turn useful orientation into an error.
900
- const cap = isFile && filesItems > 0 && shallowItems > 0 ? Math.min(filesItems, shallowItems) : null;
901
- // The file survey foists as the BARE relative glob — the path shape plurnk.md
902
- // teaches (`*`, `src/**`, `**/notes.md`; bare = project-relative) — so the turn-0
903
- // exemplar and the log rows the model reads never train a leading-slash or
904
- // file:/// habit the rest of the teaching contradicts.
905
- const catalogFind = {
906
- op: "FIND", suffix: "", signal: null,
907
- target: isFile ? { kind: "local", raw: pattern } : {
908
- kind: "url",
909
- raw: `${scheme}:///${pattern}`,
910
- scheme,
911
- username: null, password: null, hostname: null, port: null,
912
- pathname: `/${pattern}`,
913
- query: null, fragment: null,
1081
+ const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
1082
+ const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
1083
+ await Owner.kernelId(this.#db, workspaceId);
1084
+ const surveys = [
1085
+ {
1086
+ statement: {
1087
+ op: "FIND", suffix: "", signal: ["+init"],
1088
+ target: { kind: "local", raw: "*" },
1089
+ body: null,
1090
+ lineMarker: fileCap === null ? null : { marks: [1, fileCap] },
1091
+ position: UNKNOWN_POSITION,
914
1092
  },
915
- body: null,
916
- lineMarker: cap === null ? null : { marks: [1, cap] },
917
- position: UNKNOWN_POSITION,
918
- };
919
- await this.dispatch({
920
- statement: catalogFind, workspaceId, workerId, loopId, turnId,
921
- sequence: nextActionIndex, origin: "plurnk", onDispatch,
922
- });
1093
+ exemplar: `## FIND0 [+init] (*)${fileCap === null ? "" : ` <1,${fileCap}>`}`,
1094
+ },
1095
+ {
1096
+ statement: {
1097
+ op: "FIND", suffix: "", signal: ["+init"],
1098
+ target: { kind: "url", raw: "worker:///*", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: "/*", query: null, fragment: null },
1099
+ body: null, lineMarker: null, position: UNKNOWN_POSITION,
1100
+ },
1101
+ exemplar: "## FIND0 [+init] (worker:///*)",
1102
+ },
1103
+ {
1104
+ statement: {
1105
+ op: "FIND", suffix: "", signal: ["+init"],
1106
+ target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
1107
+ body: null, lineMarker: null, position: UNKNOWN_POSITION,
1108
+ },
1109
+ exemplar: "## FIND0 [+init] (worker://~/*)",
1110
+ },
1111
+ {
1112
+ statement: {
1113
+ op: "FIND", suffix: "", signal: ["+init", "+docs"],
1114
+ target: { kind: "url", raw: "worker://plurnk/docs/**", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/docs/**", query: null, fragment: null },
1115
+ body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
1116
+ },
1117
+ exemplar: "## FIND0 [+init,+docs] (worker://plurnk/docs/**) <1,-1>",
1118
+ },
1119
+ ];
1120
+ for (const { statement, exemplar } of surveys) {
1121
+ await this.dispatch({ statement, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
923
1122
  nextActionIndex++;
924
- // {§model-entry} — the same FIND, rendered back to DSL for the turn-0 echo (the model's
925
- // own survey, mirrored OPEN). The <L> cap rides as `<1,N>`, exactly as the model would type it.
926
- turnZeroMoves.push(`<<FIND(${isFile ? pattern : `${scheme}:///${pattern}`})${cap === null ? "" : `<1,${cap}>`}::FIND`);
1123
+ turnZeroMoves.push(exemplar);
927
1124
  }
928
- // The kernel's self-documenting surface — FIND(worker://plurnk/docs/**), uncapped,
929
- // always ({§schemes-directory}, published under {§entry-owner}).
930
- await Owner.kernelId(this.#db, workspaceId); // the row exists even before docs materialize — the empty survey is orienting, never 404
931
- const kernelDocsFind = {
932
- op: "FIND", suffix: "", signal: null,
933
- target: { kind: "url", raw: "worker://plurnk/docs/**", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/docs/**", query: null, fragment: null },
934
- body: null, lineMarker: null, position: UNKNOWN_POSITION,
935
- };
936
- await this.dispatch({ statement: kernelDocsFind, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
937
- nextActionIndex++;
938
- turnZeroMoves.push("<<FIND(worker://plurnk/docs/**)::FIND");
939
- // {§worker-scheme} — the building worker's own scratch gets the same complete
940
- // one-level map in its perspective alone. It always executes: an empty private
941
- // space is useful orientation, not grounds to hide the surface.
942
- const ownFind = {
943
- op: "FIND", suffix: "", signal: null,
944
- target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
945
- body: null, lineMarker: null, position: UNKNOWN_POSITION,
946
- };
947
- await this.dispatch({ statement: ownFind, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
948
- nextActionIndex++;
949
- turnZeroMoves.push("<<FIND(worker://~/*)::FIND"); // {§model-entry} — the own-space survey, into the turn-0 echo
950
1125
  }
951
- // {§model-entry} — mirror the model's turn-0 OPEN at sequence 1: PLAN → the FINDs actually
952
- // foisted above (real, their results already in the log) → SEND[102]. Dynamic — it reflects
1126
+ // {§worker-initialization-entry} — write the kernel's turn-0 initialization OPEN at sequence 1: PLAN → the FINDs actually
1127
+ // foisted above (real, their results already in the log) → SEND signal 102. Dynamic — it reflects
953
1128
  // the true survey, never a frozen print — and OPEN: the worked example the model orients on,
954
- // so the grammar can stay thin. Subsequent turns mirror the model's real output, folded.
1129
+ // so the grammar can stay thin.
955
1130
  if (workerFirstLoop) {
956
- const emission = ["<<PLAN:Initialize:PLAN", ...turnZeroMoves, "<<SEND[102]:Next, address the prompt from the initialized context.:SEND"].join("\n");
957
- await this.#dispatcher.writeModelEntry({ verbatim: emission, workerId, loopId, turnId, sequence: 1, folded: false, origin: "plurnk" });
1131
+ const initialization = [
1132
+ "# PLAN0\n* Initialization complete.\n* Next: address the prompt.",
1133
+ ...turnZeroMoves,
1134
+ "## SEND0 [102]\nNext, address the prompt.",
1135
+ ].join("\n\n");
1136
+ await this.#dispatcher.writeInitializationEntry({ verbatim: initialization, workerId, loopId, turnId, sequence: 1 });
958
1137
  }
959
1138
  }
960
1139
  // {§methods-loop-run-open-paths}: selected workspace paths belong to
@@ -986,10 +1165,8 @@ export default class Engine {
986
1165
  await this.#reapTurnScopedStreams(workerId);
987
1166
  nextActionIndex += await this.#materializeEnvironmentDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
988
1167
  nextActionIndex += await this.#materializeStreamDeltas({ workerId, loopId, turnId, fromSequence: nextActionIndex });
989
- // Git working-tree state is read once for the packet and threaded
990
- // (a service-side `git status` shell-out) and threaded into the budget
991
- // rebuild too so it isn't re-shelled on overflow.
992
- const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopAborts.get(loopId)?.signal);
1168
+ // The post-reconciliation Git snapshot above is threaded into the packet
1169
+ // and every budget rebuild; overflow never shells again.
993
1170
  // Notices are non-terminal observations, never operation-failure truth.
994
1171
  // Drain once and thread the same set through every grinder rebuild.
995
1172
  const notices = this.#notices.drain(loopId)
@@ -999,95 +1176,56 @@ export default class Engine {
999
1176
  // written (if turn 1) is part of that query result.
1000
1177
  let requestPacket = await this.#packets.buildRequestPacket({
1001
1178
  initialMessages: messages, requirements, workspaceId, workerId, loopId,
1002
- currentTurnSeq: seq, provider, gitStatus, notices,
1179
+ currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
1003
1180
  });
1004
1181
  // SPEC {§grinder} — budget grinder, pre-LLM: reclaim window on actual overflow.
1005
1182
  const enforced = await this.#packets.enforceBudget({
1006
- packet: requestPacket, provider, workerId, loopId, turnId,
1007
- // The overflow error row is minted at the turn's running sequence (nextActionIndex), pre-generate;
1008
- // runTurn advances the counter past it below so the post-generate dispatch rows never collide.
1009
- mintSequence: nextActionIndex,
1010
- // Rebuilds re-derive durable errors and retain the one drained notice
1011
- // set; neither path can duplicate or swallow a product failure.
1183
+ packet: requestPacket, provider, loopId, turnId,
1184
+ recordOverflow: async (pressure) => {
1185
+ await this.#problems.record({
1186
+ workerId,
1187
+ loopId,
1188
+ turnId,
1189
+ sequence: nextActionIndex++,
1190
+ origin: "plurnk",
1191
+ source: "engine",
1192
+ result: tokenBudgetOverflowFailure(pressure),
1193
+ });
1194
+ },
1012
1195
  rebuild: () => this.#packets.buildRequestPacket({
1013
1196
  initialMessages: messages, requirements, workspaceId, workerId, loopId,
1014
- currentTurnSeq: seq, provider, gitStatus, notices,
1197
+ currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
1015
1198
  }),
1016
1199
  });
1017
- if (enforced.recorded)
1018
- nextActionIndex += 1; // a fold-to-fit Problem consumed the reserved sequence
1019
1200
  requestPacket = enforced.packet;
1020
- let operationConstraint;
1021
1201
  if (!enforced.fit) {
1022
- // {§grinder-hard-413-recovery}/{§grinder-hard-413-abort} — admit
1023
- // one physically sendable constrained recovery turn; a physical
1024
- // overflow or consecutive policy overflow terminates immediately.
1025
- let physicalAdmission = await this.#packets.physicalAdmission(requestPacket, provider, this.#loopAborts.get(loopId)?.signal);
1026
- if (physicalAdmission.admitted && !this.#hardOverflowRecovery.has(loopId)) {
1202
+ // {§tokenomics-context-envelope-admission}: the overflow Problem is
1203
+ // already durable. Remaining ruler debt may still be admitted; only
1204
+ // the effective total context envelope can terminally reject the turn.
1205
+ const contextAdmission = await this.#packets.contextEnvelopeAdmission(requestPacket, provider, this.#loopAborts.get(loopId)?.signal);
1206
+ if (!contextAdmission.admitted) {
1027
1207
  const ceiling = this.#packets.ceilingFor(provider);
1028
1208
  if (ceiling === null) {
1029
- throw new Error("an unbounded prompt budget cannot enter budget recovery");
1209
+ throw new Error("an unbounded prompt budget cannot enter context-envelope admission");
1030
1210
  }
1211
+ const failure = contextEnvelopeFailure(budgetPressure(requestPacket.tokens, ceiling), contextAdmission);
1031
1212
  await this.#problems.record({
1032
1213
  workerId,
1033
1214
  loopId,
1034
1215
  turnId,
1035
1216
  sequence: nextActionIndex++,
1036
- origin: "model",
1217
+ origin: "plurnk",
1037
1218
  source: "engine",
1038
- result: BudgetOverflow.result(requestPacket.tokens, ceiling, true),
1219
+ result: failure,
1039
1220
  });
1040
- // Rebuild so the recovery-steer row just minted renders in THIS packet's log +
1041
- // errors sections (the same re-derive contract the soft grind uses).
1221
+ // Preserve the exact terminal evidence in the stored request. It is
1222
+ // never sent because admission has already failed.
1042
1223
  requestPacket = await this.#packets.buildRequestPacket({
1043
1224
  initialMessages: messages, requirements, workspaceId, workerId, loopId,
1044
- currentTurnSeq: seq, provider, gitStatus, notices,
1225
+ currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
1045
1226
  });
1046
- physicalAdmission = await this.#packets.physicalAdmission(requestPacket, provider, this.#loopAborts.get(loopId)?.signal);
1047
- if (physicalAdmission.admitted) {
1048
- this.#hardOverflowRecovery.add(loopId);
1049
- operationConstraint = {
1050
- code: "budget-recovery",
1051
- detail: "budget recovery is active",
1052
- allowedOperations: BudgetOverflow.recoveryOperations,
1053
- };
1054
- }
1055
- }
1056
- if (operationConstraint === undefined) {
1057
- // Hard 413: physically unsendable, or still over after the constrained recovery turn.
1058
- const ceiling = this.#packets.ceilingFor(provider);
1059
- if (ceiling === null) {
1060
- throw new Error("an unbounded prompt budget cannot hard-stop");
1061
- }
1062
- if (!enforced.recorded || !physicalAdmission.admitted) {
1063
- await this.#problems.record({
1064
- workerId,
1065
- loopId,
1066
- turnId,
1067
- sequence: nextActionIndex++,
1068
- origin: "plurnk",
1069
- source: "engine",
1070
- result: BudgetOverflow.result(requestPacket.tokens, ceiling, false, physicalAdmission.admitted
1071
- ? undefined
1072
- : {
1073
- reason: physicalAdmission.reason,
1074
- detail: physicalAdmission.detail,
1075
- capacity: physicalAdmission.capacity,
1076
- tokens: physicalAdmission.measurement?.tokens,
1077
- tokenKind: physicalAdmission.measurement?.kind,
1078
- tokenSource: physicalAdmission.measurement?.source,
1079
- }),
1080
- });
1081
- requestPacket = await this.#packets.buildRequestPacket({
1082
- initialMessages: messages, requirements, workspaceId, workerId, loopId,
1083
- currentTurnSeq: seq, provider, gitStatus, notices,
1084
- });
1085
- }
1086
- // Skip the LLM, close the turn, and let runLoop abandon.
1087
1227
  await this.#db.engine_close_turn.run({
1088
1228
  id: turnId, status: 413, packet: StoredPacket.stringify(requestPacket),
1089
- usage_prompt: 0, usage_completion: 0, usage_reasoning: 0, usage_cached: 0,
1090
- usage_cost: "[]", usage_cost_usd: 0,
1091
1229
  // The attempted turn retains its effective allowance even
1092
1230
  // when no provider exchange completed. {§tokenomics-client-gauge}
1093
1231
  usage_prompt_budget: this.#packets.promptBudgetFor(provider),
@@ -1096,22 +1234,16 @@ export default class Engine {
1096
1234
  return {
1097
1235
  turnId,
1098
1236
  status: 413,
1099
- statuses: [],
1237
+ outcomes: [],
1100
1238
  fingerprint: "",
1101
- budgetStruck: enforced.struck,
1102
1239
  budgetHardStop: true,
1103
1240
  steerStruck: false,
1104
1241
  emissionAttempts: 0,
1105
1242
  emissionExhausted: false,
1106
- budget: BudgetOverflow.measure(requestPacket.tokens, ceiling),
1243
+ budgetFailure: failure,
1107
1244
  };
1108
1245
  }
1109
1246
  }
1110
- else {
1111
- // A fitting turn clears the recovery grant; a later independent overflow can earn
1112
- // a fresh recovery turn (chronic overflow still strikes out via the rail).
1113
- this.#hardOverflowRecovery.delete(loopId);
1114
- }
1115
1247
  const modelMessages = PacketWire.packetToWireMessages(requestPacket);
1116
1248
  // Packet pressure and provider generation are independent. The grinder governs
1117
1249
  // only the request packet; maxTokens comes only from the provider envelope and
@@ -1119,54 +1251,38 @@ export default class Engine {
1119
1251
  let response;
1120
1252
  let splitResponse;
1121
1253
  let railGrammar;
1254
+ let railResponseGrammar;
1122
1255
  let railEvidence;
1123
1256
  let emissionAttempts = 0;
1124
- const usage = { prompt: 0, completion: 0, reasoning: 0, cached: 0 };
1125
- let usageCostUsd = 0;
1126
- const providerCosts = [];
1127
1257
  let providerCallInFlight = false;
1128
1258
  let providerAttemptSequence = 0;
1259
+ let providerAttemptId = null;
1260
+ let providerModelCall = null;
1129
1261
  let providerAttemptAttributions = [];
1130
1262
  const providerSignal = this.#loopAborts.get(loopId)?.signal ?? signal;
1131
1263
  // {§client-metadata}
1132
1264
  const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
1133
- const recordProviderAttempt = async (attemptResponse, attemptSplit, sequence, accepted, attributions) => {
1134
- const attemptUsage = attemptSplit.callMetadata.usage;
1135
- const attemptCost = providerCostFor(provider, attemptUsage, attemptResponse.charge);
1136
- const attemptCostUsd = providerCostUsd(attemptCost);
1137
- await this.#db.engine_record_turn_attempt.run({
1138
- turn_id: turnId,
1139
- sequence,
1265
+ const loopSeq = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
1266
+ const providerIdentity = await this.resolveWorkerProviderIdentity(workerId);
1267
+ const { workerId: providerWorkerId, primaryWorkerId } = providerIdentity;
1268
+ const classifyProviderAttempt = async (id, attemptSplit, sequence, accepted) => {
1269
+ const result = await this.#db.engine_classify_turn_attempt_response.run({
1270
+ id,
1140
1271
  accepted: accepted ? 1 : 0,
1141
- response: JSON.stringify(attemptResponse),
1142
1272
  parse_errors: JSON.stringify(attemptSplit.parseErrors),
1143
- attributions: JSON.stringify(attributions),
1144
- usage_prompt: attemptUsage.prompt,
1145
- usage_completion: attemptUsage.completion,
1146
- usage_reasoning: attemptUsage.reasoning,
1147
- usage_cached: attemptUsage.cached,
1148
- usage_cost: JSON.stringify(attemptCost),
1149
- usage_cost_usd: attemptCostUsd,
1150
- finish_reason: attemptSplit.callMetadata.finishReason,
1151
- model: attemptSplit.callMetadata.model,
1152
1273
  });
1153
- usage.prompt += attemptUsage.prompt;
1154
- usage.completion += attemptUsage.completion;
1155
- usage.reasoning += attemptUsage.reasoning;
1156
- usage.cached += attemptUsage.cached;
1157
- providerCosts.push(attemptCost);
1158
- usageCostUsd = usageCostUsd === null || attemptCostUsd === null
1159
- ? null
1160
- : usageCostUsd + attemptCostUsd;
1274
+ if (result.changes !== 1) {
1275
+ throw new Error(`emission attempt ${id} was not awaiting classification`);
1276
+ }
1161
1277
  emissionAttempts = sequence;
1162
1278
  };
1163
1279
  try {
1164
1280
  // {§turn-lifecycle}: bracket the complete provider-attempt window with liveness notices.
1165
1281
  if (!signal?.aborted)
1166
1282
  this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
1167
- const loopSeq = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
1168
- railGrammar = await this.#grammarConstraint(provider);
1169
- const primaryWorkerId = String(await this.resolveWorkerPrimary(workerId));
1283
+ const railConstraint = await this.#grammarConstraint(provider);
1284
+ railGrammar = railConstraint?.transport;
1285
+ railResponseGrammar = railConstraint?.response;
1170
1286
  const attemptLimit = readEmissionAttempts();
1171
1287
  const maxTokens = this.#packets.maxTokensFor(provider) ?? undefined;
1172
1288
  const strikeStreak = this.#strikes.streak(loopId);
@@ -1178,7 +1294,7 @@ export default class Engine {
1178
1294
  providerAttemptSequence = attempt;
1179
1295
  const attributionContext = Object.freeze({
1180
1296
  workspaceId: String(workspaceId),
1181
- workerId: String(workerId),
1297
+ workerId: providerWorkerId,
1182
1298
  primaryWorkerId,
1183
1299
  loop: loopSeq,
1184
1300
  turn: seq,
@@ -1186,40 +1302,67 @@ export default class Engine {
1186
1302
  });
1187
1303
  providerAttemptAttributions = await this.#attemptAttributions(provider, attributionContext);
1188
1304
  requestPacket = { ...requestPacket, attributions: providerAttemptAttributions };
1305
+ providerModelCall = await ModelCall.open(this.#db, {
1306
+ turnId,
1307
+ sequence: attempt,
1308
+ kind: "emission",
1309
+ attributions: providerAttemptAttributions,
1310
+ model: provider.model,
1311
+ });
1312
+ const attemptRow = await this.#db.engine_open_turn_attempt.get({
1313
+ model_call_id: providerModelCall.id,
1314
+ });
1315
+ if (attemptRow === undefined) {
1316
+ throw new Error(`Engine.runTurn: provider attempt ${attempt} did not open`);
1317
+ }
1318
+ providerAttemptId = attemptRow.id;
1189
1319
  providerCallInFlight = true;
1320
+ const currentModelCall = providerModelCall;
1190
1321
  const completedResponse = await observed(// {§observability-boundary}
1191
1322
  "provider.generate", { model: provider.model, attempt }, async (span) => {
1192
- const generated = await provider.generate({
1193
- messages: modelMessages,
1194
- workerId: String(workerId),
1195
- primaryWorkerId,
1196
- signal: providerSignal,
1197
- grammar: railGrammar,
1198
- maxTokens,
1199
- strikes: strikeStreak,
1200
- attributions: providerAttemptAttributions.length > 0
1201
- ? providerAttemptAttributions
1202
- : undefined,
1203
- client: client ?? undefined,
1204
- workspaceId: String(workspaceId),
1205
- loop: loopSeq,
1206
- turn: seq,
1207
- }); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
1208
- recordCounter(PROVIDER_CALLS, {
1209
- model: provider.model,
1210
- attempt,
1211
- status: "resolved",
1212
- });
1213
- span.setAttribute("status", "resolved");
1214
- return generated;
1323
+ try {
1324
+ const generated = await provider.generate({
1325
+ messages: modelMessages,
1326
+ workerId: providerWorkerId,
1327
+ primaryWorkerId,
1328
+ signal: providerSignal,
1329
+ grammar: railGrammar,
1330
+ maxTokens,
1331
+ strikes: strikeStreak,
1332
+ attributions: providerAttemptAttributions.length > 0
1333
+ ? providerAttemptAttributions
1334
+ : undefined,
1335
+ client: client ?? undefined,
1336
+ workspaceId: String(workspaceId),
1337
+ loop: loopSeq,
1338
+ turn: seq,
1339
+ observeRequest: currentModelCall.observeRequest,
1340
+ callKind: "emission",
1341
+ }); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
1342
+ currentModelCall.assertAccounting(generated.accounting);
1343
+ providerCallInFlight = false;
1344
+ recordCounter(PROVIDER_CALLS, {
1345
+ model: provider.model,
1346
+ attempt,
1347
+ status: "resolved",
1348
+ });
1349
+ span.setAttribute("status", "resolved");
1350
+ return generated;
1351
+ }
1352
+ catch (error) {
1353
+ if (error instanceof ProviderError) {
1354
+ currentModelCall.assertAccounting(error.accounting);
1355
+ }
1356
+ throw error;
1357
+ }
1215
1358
  });
1216
1359
  response = completedResponse;
1217
- providerCallInFlight = false;
1360
+ await currentModelCall.observeResponse(completedResponse);
1218
1361
  railEvidence = railGrammar === undefined
1219
1362
  ? undefined
1220
- : Engine.#requireGrammarEvidence(completedResponse);
1363
+ : _a.#requireGrammarEvidence(completedResponse);
1221
1364
  splitResponse = this.#splitResponse(completedResponse);
1222
- await recordProviderAttempt(completedResponse, splitResponse, attempt, splitResponse.emissionValid, providerAttemptAttributions);
1365
+ await classifyProviderAttempt(attemptRow.id, splitResponse, attempt, splitResponse.emissionValid);
1223
1366
  if (splitResponse.emissionValid)
1224
1367
  break;
1225
1368
  }
@@ -1229,22 +1372,27 @@ export default class Engine {
1229
1372
  catch (err) {
1230
1373
  // This handler owns only provider-call failures. Parser, cost, SQL,
1231
1374
  // and engine-contract failures retain their original source.
1375
+ if (err instanceof ModelCallPersistenceError || err instanceof ProviderAccountingIntegrityError)
1376
+ throw err;
1232
1377
  if (!providerCallInFlight)
1233
1378
  throw err;
1379
+ providerCallInFlight = false;
1380
+ if (providerAttemptId === null || providerModelCall === null) {
1381
+ throw new Error("provider call failed without durable model-call and attempt identities", { cause: err });
1382
+ }
1383
+ const failure = _a.#providerFailure(err, providerSignal);
1234
1384
  // {§provider-interrupted-attempt} — a provider-declared interruption
1235
1385
  // carries response evidence without becoming a completed exchange.
1236
1386
  // Persist it as an unaccepted attempt before settling the failure.
1237
1387
  if (err instanceof ProviderError && err.attempt !== undefined) {
1238
1388
  response = err.attempt;
1389
+ await providerModelCall.observeResponse(response, failure);
1239
1390
  splitResponse = this.#splitResponse(response);
1240
- await recordProviderAttempt(response, splitResponse, providerAttemptSequence, false, providerAttemptAttributions);
1391
+ await classifyProviderAttempt(providerAttemptId, splitResponse, providerAttemptSequence, false);
1241
1392
  }
1242
1393
  else {
1243
- providerCosts.push({
1244
- kind: "unknown",
1245
- reason: "provider call failed without response-bearing charge evidence",
1246
- });
1247
- usageCostUsd = null;
1394
+ await providerModelCall.fail(failure);
1395
+ emissionAttempts = providerAttemptSequence;
1248
1396
  }
1249
1397
  // {§turn-never-blank} — a ProviderError means no completed exchange exists.
1250
1398
  // Persist its exact RFC 9457 result before propagating it. Grammar evidence
@@ -1258,12 +1406,6 @@ export default class Engine {
1258
1406
  id: turnId,
1259
1407
  status: providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499,
1260
1408
  packet: StoredPacket.stringify(requestPacket),
1261
- usage_prompt: usage.prompt,
1262
- usage_completion: usage.completion,
1263
- usage_reasoning: usage.reasoning,
1264
- usage_cached: usage.cached,
1265
- usage_cost: JSON.stringify(providerCosts),
1266
- usage_cost_usd: usageCostUsd,
1267
1409
  usage_prompt_budget: this.#packets.promptBudgetFor(provider),
1268
1410
  finish_reason: splitResponse?.callMetadata.finishReason ?? null,
1269
1411
  model: splitResponse?.callMetadata.model ?? provider.model,
@@ -1271,15 +1413,6 @@ export default class Engine {
1271
1413
  });
1272
1414
  throw err;
1273
1415
  }
1274
- const failure = err instanceof ProviderError
1275
- ? { status: err.problem.status, problem: err.problem }
1276
- : (() => {
1277
- console.error("Provider failed outside its Problem Details contract:", err);
1278
- return Results.failure("engine:provider", "provider-contract-violation", 502, "The provider failed without returning its required Problem Details.", {}, {
1279
- stage: "provider-request",
1280
- retryable: false,
1281
- });
1282
- })();
1283
1416
  const recorded = await this.#problems.record({
1284
1417
  workerId,
1285
1418
  loopId,
@@ -1296,12 +1429,6 @@ export default class Engine {
1296
1429
  id: turnId,
1297
1430
  status: recorded.result.status,
1298
1431
  packet: StoredPacket.stringify(requestPacket),
1299
- usage_prompt: usage.prompt,
1300
- usage_completion: usage.completion,
1301
- usage_reasoning: usage.reasoning,
1302
- usage_cached: usage.cached,
1303
- usage_cost: JSON.stringify(providerCosts),
1304
- usage_cost_usd: usageCostUsd,
1305
1432
  usage_prompt_budget: this.#packets.promptBudgetFor(provider),
1306
1433
  finish_reason: splitResponse?.callMetadata.finishReason ?? null,
1307
1434
  model: splitResponse?.callMetadata.model ?? provider.model,
@@ -1309,20 +1436,36 @@ export default class Engine {
1309
1436
  });
1310
1437
  throw new OperationFailureError(recorded.result, { cause: err });
1311
1438
  }
1312
- if (response === undefined || splitResponse === undefined) {
1439
+ if (response === undefined || splitResponse === undefined || providerModelCall === null) {
1313
1440
  throw new Error("provider attempt loop completed without a response");
1314
1441
  }
1442
+ const emissionModelCallId = providerModelCall.id;
1315
1443
  if (!splitResponse.emissionValid) {
1444
+ // {§invalid-emission-attempts} The first consecutive exhaustion
1445
+ // publishes only the raw final response and generic recovery fact.
1446
+ let rejectedModelEntryId;
1447
+ if (allowInvalidEmissionRecovery) {
1448
+ rejectedModelEntryId = await this.#dispatcher.writeModelEntry({
1449
+ verbatim: splitResponse.packetAssistant.content,
1450
+ workerId,
1451
+ loopId,
1452
+ turnId,
1453
+ sequence: nextActionIndex,
1454
+ folded: true,
1455
+ modelCallId: emissionModelCallId,
1456
+ admission: "rejected",
1457
+ });
1458
+ this.#notices.push(workspaceId, loopId, {
1459
+ source: "engine:grammar",
1460
+ kind: "invalid_emission",
1461
+ level: "error",
1462
+ message: INVALID_EMISSION_RECOVERY_MESSAGE,
1463
+ });
1464
+ }
1316
1465
  await this.#db.engine_close_turn.run({
1317
1466
  id: turnId,
1318
- status: 500,
1467
+ status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
1319
1468
  packet: StoredPacket.stringify(requestPacket),
1320
- usage_prompt: usage.prompt,
1321
- usage_completion: usage.completion,
1322
- usage_reasoning: usage.reasoning,
1323
- usage_cached: usage.cached,
1324
- usage_cost: JSON.stringify(providerCosts),
1325
- usage_cost_usd: usageCostUsd,
1326
1469
  usage_prompt_budget: this.#packets.promptBudgetFor(provider),
1327
1470
  finish_reason: splitResponse.callMetadata.finishReason,
1328
1471
  model: splitResponse.callMetadata.model,
@@ -1330,14 +1473,14 @@ export default class Engine {
1330
1473
  });
1331
1474
  return {
1332
1475
  turnId,
1333
- status: 500,
1334
- statuses: [],
1476
+ status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
1477
+ outcomes: [],
1335
1478
  fingerprint: "",
1336
- budgetStruck: enforced.struck,
1337
1479
  budgetHardStop: false,
1338
1480
  steerStruck: false,
1339
1481
  emissionAttempts,
1340
1482
  emissionExhausted: true,
1483
+ ...(rejectedModelEntryId === undefined ? {} : { rejectedModelEntryId }),
1341
1484
  };
1342
1485
  }
1343
1486
  // {§packet-stored-shape} — admitted emission data extends the packet;
@@ -1348,7 +1491,7 @@ export default class Engine {
1348
1491
  }
1349
1492
  // Non-fatal provider transport notices on an accepted turn. Forward each
1350
1493
  // Notice with a content-offset `line:col`;
1351
- // the model resolves it against its own emission — READ the folded `model` mirror row at the
1494
+ // the model resolves it against its own emission — READ the folded model-emission row at the
1352
1495
  // cited lines ({§model-entry}) — not an embedded snippet that would duplicate the emission.
1353
1496
  for (const notice of response.notices ?? []) {
1354
1497
  const located = typeof notice.position === "number"
@@ -1370,12 +1513,14 @@ export default class Engine {
1370
1513
  if (railGrammar !== undefined) {
1371
1514
  if (railEvidence === undefined)
1372
1515
  throw new Error("configured GBNF response has no final grammar evidence");
1516
+ if (railResponseGrammar === undefined)
1517
+ throw new Error("configured GBNF has no response grammar");
1373
1518
  let verdict = null;
1374
1519
  try {
1375
- verdict = validateGbnf(railGrammar, railEvidence.input);
1520
+ verdict = validateGbnf(railResponseGrammar, railEvidence.input);
1376
1521
  }
1377
1522
  catch (cause) {
1378
- Engine.#warnRailVerdictGapOnce(cause.message);
1523
+ _a.#warnRailVerdictGapOnce(cause.message);
1379
1524
  }
1380
1525
  railKeys = {
1381
1526
  railsAttached: railEvidence.transported ? "client" : "withheld",
@@ -1442,12 +1587,6 @@ export default class Engine {
1442
1587
  id: turnId,
1443
1588
  status: turnStatus,
1444
1589
  packet: StoredPacket.stringify(packet),
1445
- usage_prompt: usage.prompt,
1446
- usage_completion: usage.completion,
1447
- usage_reasoning: usage.reasoning,
1448
- usage_cached: usage.cached,
1449
- usage_cost: JSON.stringify(providerCosts),
1450
- usage_cost_usd: usageCostUsd,
1451
1590
  usage_prompt_budget: this.#packets.promptBudgetFor(provider), // {§tokenomics-client-gauge}
1452
1591
  finish_reason: callMetadata.finishReason,
1453
1592
  model: callMetadata.model,
@@ -1471,22 +1610,19 @@ export default class Engine {
1471
1610
  // (the PLAN/SEND ops, zero actions), which is its only coherent meaning.
1472
1611
  let realCommands = 0;
1473
1612
  const admittedOps = packetAssistant.ops.filter((op) => {
1474
- const constrained = operationConstraint !== undefined
1475
- && !operationConstraint.allowedOperations.includes(op.op);
1476
1613
  return op.op === "PLAN"
1477
1614
  || op === sendOp
1478
- || constrained
1479
1615
  || realCommands++ < maxCommands;
1480
1616
  });
1481
1617
  const opsToDispatch = scheduleTurnOps(admittedOps);
1482
- await this.#dispatcher.prepareEditBatches(opsToDispatch.filter((statement) => statement.op === "EDIT"
1483
- && (operationConstraint === undefined
1484
- || operationConstraint.allowedOperations.includes(statement.op))), {
1618
+ await this.#dispatcher.prepareEditBatches(opsToDispatch.filter((statement) => statement.op === "EDIT"), {
1485
1619
  workspaceId, workerId, loopId, turnId,
1486
1620
  origin, onDispatch,
1487
1621
  });
1488
1622
  const droppedCount = opsCount - opsToDispatch.length;
1489
- const statuses = [];
1623
+ const bareStatements = opsToDispatch.filter((statement) => statement.op === "BARE");
1624
+ let bareResults = null;
1625
+ const outcomes = [];
1490
1626
  // Running counter — a multi-file READ writes N rows from one statement (rowsWritten),
1491
1627
  // so the next op's sequence picks up after them. Collapses to nextActionIndex+i when
1492
1628
  // every op writes one row (the common case).
@@ -1513,7 +1649,7 @@ export default class Engine {
1513
1649
  retryable: false,
1514
1650
  }),
1515
1651
  });
1516
- statuses.push(recorded.result.status);
1652
+ outcomes.push({ op: null, status: recorded.result.status });
1517
1653
  onDispatch?.(recorded.id);
1518
1654
  }
1519
1655
  };
@@ -1522,20 +1658,59 @@ export default class Engine {
1522
1658
  && typeof statement.signal === "number"
1523
1659
  && TERMINAL_SEND_SIGNALS.has(statement.signal)) {
1524
1660
  await recordRecoverableParseErrors();
1661
+ // {§worker-optimistic-settlement} — SEND judges the refreshed
1662
+ // lifecycle after this turn's own fast streams receive one
1663
+ // bounded opportunity to conclude. This is not a sleep and
1664
+ // does not delay a later turn for older monitored streams.
1665
+ const execHandler = this.#schemes.get("exec");
1666
+ await execHandler?.settleTurnSpawns?.(workerId, turnId, readOptimisticSettlementMs(), providerSignal);
1525
1667
  }
1526
1668
  const result = await observed(// {§observability-boundary}
1527
1669
  "op.dispatch", { op: statement.op }, async (span) => {
1528
- const dispatchResult = await this.#dispatcher.dispatch({
1529
- statement, workspaceId, workerId, loopId, turnId,
1530
- sequence: rowSeq,
1531
- ...(operationConstraint === undefined ? {} : { operationConstraint }),
1532
- origin, onDispatch,
1533
- });
1670
+ let dispatchResult;
1671
+ if (statement.op === "BARE") {
1672
+ if (bareResults === null) {
1673
+ const batch = await this.#runBareBatch({
1674
+ statements: bareStatements,
1675
+ provider: childProvider,
1676
+ turnId,
1677
+ modelCallSequenceStart: providerAttemptSequence + 1,
1678
+ workspaceId,
1679
+ workerId,
1680
+ primaryWorkerId,
1681
+ loopSequence: loopSeq,
1682
+ turnSequence: seq,
1683
+ signal: providerSignal,
1684
+ });
1685
+ bareResults = new Map(batch.map((item) => [item.statement, item]));
1686
+ }
1687
+ const bare = bareResults.get(statement);
1688
+ if (bare === undefined) {
1689
+ throw new Error("BARE statement reached dispatch without its batch result");
1690
+ }
1691
+ dispatchResult = await this.#dispatcher.recordBareResult({
1692
+ statement,
1693
+ workspaceId,
1694
+ workerId,
1695
+ loopId,
1696
+ turnId,
1697
+ sequence: rowSeq,
1698
+ origin,
1699
+ onDispatch,
1700
+ }, bare.result, bare.modelCallId);
1701
+ }
1702
+ else {
1703
+ dispatchResult = await this.#dispatcher.dispatch({
1704
+ statement, workspaceId, workerId, loopId, turnId,
1705
+ sequence: rowSeq,
1706
+ origin, onDispatch,
1707
+ });
1708
+ }
1534
1709
  span.setAttribute("status", dispatchResult.status);
1535
1710
  recordCounter(OPS_DISPATCHED, { op: statement.op, status: dispatchResult.status });
1536
1711
  return dispatchResult;
1537
1712
  });
1538
- statuses.push(result.status);
1713
+ outcomes.push({ op: statement.op, status: result.status });
1539
1714
  for (const normalization of result.scopeNormalizations ?? []) {
1540
1715
  this.#notices.push(workspaceId, loopId, {
1541
1716
  source: "engine:slicer",
@@ -1591,7 +1766,7 @@ export default class Engine {
1591
1766
  }
1592
1767
  : {
1593
1768
  stage: "turn",
1594
- recovery: "Perform an operation before continuing with SEND[102].",
1769
+ recovery: "Perform an operation before continuing with `## SEND0 [102]`.",
1595
1770
  retryable: false,
1596
1771
  };
1597
1772
  await this.#problems.record({
@@ -1617,14 +1792,22 @@ export default class Engine {
1617
1792
  ? response.assistant.reasoningEncrypted
1618
1793
  : undefined;
1619
1794
  if (packetAssistant.content.trim().length > 0 || reasoningItems !== undefined) {
1620
- await this.#dispatcher.writeModelEntry({ verbatim: packetAssistant.content, workerId, loopId, turnId, sequence: errSeq++, folded: true, ...(reasoningItems !== undefined ? { reasoningItems } : {}) });
1795
+ await this.#dispatcher.writeModelEntry({
1796
+ verbatim: packetAssistant.content,
1797
+ workerId,
1798
+ loopId,
1799
+ turnId,
1800
+ sequence: errSeq++,
1801
+ folded: true,
1802
+ modelCallId: emissionModelCallId,
1803
+ ...(reasoningItems !== undefined ? { reasoningItems } : {}),
1804
+ });
1621
1805
  }
1622
1806
  return {
1623
1807
  turnId,
1624
1808
  status: turnStatus,
1625
- statuses,
1809
+ outcomes,
1626
1810
  fingerprint: StrikeRail.fingerprintTurn(packetAssistant.ops),
1627
- budgetStruck: enforced.struck,
1628
1811
  budgetHardStop: false,
1629
1812
  steerStruck,
1630
1813
  emissionAttempts,
@@ -1633,7 +1816,8 @@ export default class Engine {
1633
1816
  }
1634
1817
  // Split the wire-level ProviderResponse into the two destinations:
1635
1818
  // packet.assistant gets the model's emission (content, ops, reasoning);
1636
- // Turn columns get the call-metadata (usage, finishReason, model).
1819
+ // Turn columns get accepted-call metadata (finishReason, model). Physical
1820
+ // request usage and cost have their own cardinal persistence path.
1637
1821
  // {§provider-surface} Text-fragment scraping policy lives
1638
1822
  // here — engine owns the parse and the scraping rule, providers stay
1639
1823
  // grammar-unaware.
@@ -1647,10 +1831,8 @@ export default class Engine {
1647
1831
  const preParsedOps = assistant.ops;
1648
1832
  const ops = [];
1649
1833
  // PLAN is an ordinary op — emitted by the model, dispatched, and passed to the
1650
- // client as a log entry. No special hoisting into the reasoning field (that
1651
- // only structured operations are executable). Interstitial free text is DROPPED — the prior
1652
- // #free-text-capture synthesis of SEND[103] log ops was retired as tech debt
1653
- // (grammar 0.70 forbids free text between ops, so a prose-only turn strikes 422).
1834
+ // client as a log entry. No special hoisting into the reasoning field. Only
1835
+ // structured operations are executable; interstitial text is not an operation.
1654
1836
  // Full PlurnkParseError context is preserved on rejected attempt evidence;
1655
1837
  // warnings remain admissible Notices. {§parse-diagnostics}
1656
1838
  const parseErrors = [];
@@ -1671,7 +1853,6 @@ export default class Engine {
1671
1853
  if (item.kind === "statement") {
1672
1854
  ops.push(item.statement);
1673
1855
  }
1674
- // Free text (kind "text") is dropped — #free-text-capture retired (above).
1675
1856
  else if (item.kind === "error") {
1676
1857
  const err = item.error;
1677
1858
  if (err instanceof PlurnkParseError) {
@@ -1726,7 +1907,7 @@ export default class Engine {
1726
1907
  const reasoning = assistant.reasoning ?? null;
1727
1908
  return {
1728
1909
  packetAssistant: { content: assistant.content, ops, reasoning },
1729
- callMetadata: { usage: assistant.usage, finishReason: assistant.finishReason, model: assistant.model },
1910
+ callMetadata: { finishReason: assistant.finishReason, model: assistant.model },
1730
1911
  parseErrors,
1731
1912
  recoverableParseErrors: emissionValid ? recoverableParseErrors : [],
1732
1913
  parseNotices,
@@ -1758,11 +1939,36 @@ export default class Engine {
1758
1939
  || r.op === null || r.status_rx === null)
1759
1940
  continue;
1760
1941
  const termination = r.kind === "loop_termination";
1761
- const rx = termination
1762
- ? BranchReceipt.append(markTerminal(r.terminated_by, r.rx) ?? `loop "${r.prompt ?? ""}" ended (${r.status_rx})`, await BranchReceipt.render(this.#db, r.producer_worker_id))
1763
- : r.rx;
1764
- if (rx === null)
1942
+ if (r.rx === null)
1765
1943
  throw new Error(`ambient event ${r.event_id} has no materializable result`);
1944
+ const terminal = termination
1945
+ ? TerminalResult.parse(r.rx, `ambient loop-termination event ${r.event_id}`)
1946
+ : null;
1947
+ if (terminal !== null && terminal.status !== r.status_rx) {
1948
+ throw new Error(`ambient loop-termination event ${r.event_id} status ${r.status_rx} does not match its terminal result status ${terminal.status}`);
1949
+ }
1950
+ let attrs = r.attrs ?? "{}";
1951
+ const parsedTags = JSON.parse(r.tags ?? "[]");
1952
+ if (!Array.isArray(parsedTags) || !parsedTags.every((tag) => typeof tag === "string" && tag.length > 0)) {
1953
+ throw new TypeError(`ambient event ${r.event_id} tags must be an array of nonempty strings`);
1954
+ }
1955
+ const tags = [...new Set(parsedTags)].toSorted();
1956
+ if (tags.length !== parsedTags.length || tags.some((tag, index) => tag !== parsedTags[index])) {
1957
+ throw new TypeError(`ambient event ${r.event_id} tags must be unique and sorted`);
1958
+ }
1959
+ if (terminal !== null) {
1960
+ const inherited = JSON.parse(attrs);
1961
+ if (inherited === null || typeof inherited !== "object" || Array.isArray(inherited)) {
1962
+ throw new TypeError(`ambient loop-termination event ${r.event_id} attrs must be an object`);
1963
+ }
1964
+ const receipt = await BranchReceipt.render(this.#db, r.producer_worker_id);
1965
+ attrs = JSON.stringify({
1966
+ ...inherited,
1967
+ kind: "loop_termination",
1968
+ ...(r.terminated_by === null ? {} : { terminatedBy: r.terminated_by }),
1969
+ ...(receipt === null ? {} : { receipt }),
1970
+ });
1971
+ }
1766
1972
  const inserted = await this.#db.engine_insert_ambient_delta.get({
1767
1973
  worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
1768
1974
  event_id: r.event_id,
@@ -1771,12 +1977,21 @@ export default class Engine {
1771
1977
  scheme: r.scheme,
1772
1978
  hostname: r.hostname,
1773
1979
  pathname: r.pathname,
1774
- rx,
1775
- mimetype_rx: termination ? "text/markdown" : "application/json",
1980
+ rx: r.rx,
1981
+ mimetype_rx: "application/json",
1776
1982
  status: r.status_rx,
1777
- expanded: termination && r.status_rx >= 200 && r.status_rx < 300 ? 1 : 0,
1778
- attrs: r.attrs ?? "{}",
1983
+ expanded: terminal !== null && terminal.status >= 200 && terminal.status < 300 ? 1 : 0,
1984
+ attrs,
1779
1985
  });
1986
+ const materialized = inserted ?? await this.#db.engine_ambient_delta_id.get({
1987
+ worker_id: workerId,
1988
+ event_id: r.event_id,
1989
+ });
1990
+ if (materialized === undefined)
1991
+ throw new Error(`ambient event ${r.event_id} has no observer log row after materialization`);
1992
+ for (const tag of tags) {
1993
+ await this.#db.log_write_tag.run({ log_entry_id: materialized.id, tag });
1994
+ }
1780
1995
  if (inserted !== undefined)
1781
1996
  written++;
1782
1997
  }
@@ -1801,11 +2016,10 @@ export default class Engine {
1801
2016
  return this.#liveSubscriptions.cancel(subscriptionId);
1802
2017
  }
1803
2018
  // {§env-delta} — exec streams as an instance of the ambient-observe machine:
1804
- // each turn, emit each owned channel's unshown byte-delta as a foisted READ row. It is 200
1805
- // while the channel streams and preserves the exact terminal result when closed. Ongoing
1806
- // deltas fold; the terminal delta auto-OPENs. The cursor is the
1807
- // streamEnd recorded on the channel's prior delta — no exec-specific surfacing, just the
1808
- // env-observe loop with a byte cursor where env-delta uses a timestamp. {§exec-stream}
2019
+ // each turn, emit each owned channel's next publishable content as a foisted READ row. It is
2020
+ // 200 while the channel streams and preserves the exact terminal result when closed. Ongoing
2021
+ // observations fold; the terminal observation auto-OPENs. The cursor is the streamEnd recorded
2022
+ // on the channel's prior observation. {§exec-stream}
1809
2023
  async #materializeStreamDeltas(args) {
1810
2024
  const { workerId, loopId, turnId, fromSequence } = args;
1811
2025
  const channels = await this.#db.engine_worker_stream_channels.all({ worker_id: workerId });
@@ -1847,14 +2061,15 @@ export default class Engine {
1847
2061
  }
1848
2062
  return result;
1849
2063
  };
1850
- if (ch.content.length <= cursor) {
1851
- // The cursor-terminal race (owner's dogfood find): a channel written in one final
2064
+ const publishEnd = streamPublicationEnd(ch.content, ch.mimetype, cursor, closed);
2065
+ if (publishEnd <= cursor) {
2066
+ // The cursor-terminal race: a channel written in one final
1852
2067
  // burst gets fully shown FOLDED while still active; the close then has zero new
1853
- // bytes and the auto-OPEN terminal delta never fired — the model was never shown
2068
+ // content and the auto-OPEN terminal observation never fired — the model was never shown
1854
2069
  // the conclusion of a stream whose result it already holds folded. The same
1855
- // observation is required when the stream produced zero bytes: completion is
1856
- // information independently of payload. Emit the terminal marker ONCE: open,
1857
- // terse, carrying the close status ({§tokenomics-fetch-fits-free}).
2070
+ // observation is required when the channel produced no publishable content:
2071
+ // completion is information independently of payload. Text streams retain their
2072
+ // terse marker; structured channels emit a bodyless typed conclusion.
1858
2073
  if (closed && priorAttrs.terminal !== true) {
1859
2074
  const streamTarget = renderTarget({
1860
2075
  scheme: ch.runtime,
@@ -1867,12 +2082,15 @@ export default class Engine {
1867
2082
  ? `full output already delivered above; READ ${streamTarget} to revisit`
1868
2083
  : "stream produced no output";
1869
2084
  const sequence = fromSequence + written;
2085
+ const content = baseMimetype(ch.mimetype).startsWith("text/")
2086
+ ? `[ stream closed (${ch.close_status ?? 200}) - ${pointer} ]`
2087
+ : "";
1870
2088
  await this.#db.engine_insert_stream_delta.run({
1871
2089
  worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
1872
2090
  scheme: ch.runtime, pathname: ch.coord, fragment: visibleFragment,
1873
2091
  rx: JSON.stringify(await terminalResult({
1874
- content: `[ stream closed (${ch.close_status ?? 200}) - ${pointer} ]`,
1875
- mimetype: "text/stream",
2092
+ content,
2093
+ mimetype: ch.mimetype,
1876
2094
  }, sequence)),
1877
2095
  status: terminal?.status ?? 200,
1878
2096
  attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: true }),
@@ -1886,16 +2104,19 @@ export default class Engine {
1886
2104
  // into one sequence (lines N..M, then M+1..), not N independent "1:" restarts. {§exec-stream}
1887
2105
  const startLine = (ch.content.slice(0, cursor).match(/\n/g)?.length ?? 0) + 1;
1888
2106
  const sequence = fromSequence + written;
1889
- const result = closed
1890
- ? await terminalResult({ content: ch.content.slice(cursor), mimetype: "text/stream", startLine }, sequence)
1891
- : { status: 200, content: ch.content.slice(cursor), mimetype: "text/stream", startLine };
2107
+ const content = ch.content.slice(cursor, publishEnd);
2108
+ const terminalDelivery = closed && publishEnd === ch.content.length;
2109
+ const fields = { content, mimetype: ch.mimetype, startLine };
2110
+ const result = terminalDelivery
2111
+ ? await terminalResult(fields, sequence)
2112
+ : { status: 200, ...fields };
1892
2113
  await this.#db.engine_insert_stream_delta.run({
1893
2114
  worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
1894
2115
  scheme: ch.runtime, pathname: ch.coord, fragment: visibleFragment,
1895
2116
  rx: JSON.stringify(result),
1896
2117
  status: result.status,
1897
- attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: closed }),
1898
- expanded: closed ? 1 : 0, // {§exec-stream} — terminal delta auto-OPENs; ongoing folds
2118
+ attrs: JSON.stringify({ streamEnd: publishEnd, terminal: terminalDelivery }),
2119
+ expanded: terminalDelivery ? 1 : 0, // {§exec-stream} — terminal observation auto-OPENs; ongoing folds
1899
2120
  });
1900
2121
  written++;
1901
2122
  }
@@ -1903,9 +2124,10 @@ export default class Engine {
1903
2124
  }
1904
2125
  // {§env-delta-filesystem-narration} {§membership-emi-divergence-signal}
1905
2126
  // — journal project-file divergence once through the reserved actor.
1906
- async #logFsFictions(workspaceId, divergences) {
2127
+ async #logFsFictions(workspaceId, divergences, gitStatus) {
1907
2128
  if (divergences.length === 0)
1908
2129
  return;
2130
+ const gitByPath = new Map(gitStatus?.files.map(({ path, status }) => [path, status]) ?? []);
1909
2131
  const worker = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name: "plurnk" })
1910
2132
  ?? await this.#db.envelope_insert_worker.get({ workspace_id: workspaceId, name: "plurnk", origin: "plurnk" });
1911
2133
  if (worker === undefined)
@@ -1919,14 +2141,18 @@ export default class Engine {
1919
2141
  const span = editedSpan(d.before, d.after);
1920
2142
  await this.#db.engine_insert_log_entry.get({
1921
2143
  worker_id: worker.id, loop_id: loop.id, turn_id: turn.id, sequence: sequence++,
1922
- origin: "plurnk", source: "file", op: "EDIT", suffix: "", signal: null,
2144
+ origin: "plurnk", source: "file", model_call_id: null,
2145
+ op: "EDIT", suffix: "", signal: null,
1923
2146
  // Match Dispatcher.#extractTarget: a bare file address has NULL scheme
1924
2147
  // only in log target metadata; its entry identity remains `file`.
1925
2148
  scheme: null, username: null, password: null, hostname: null, port: null,
1926
2149
  pathname: d.pathname, query: null, fragment: null, lineMarker: null,
1927
2150
  tx: "", mimetype_tx: "text/plain",
1928
2151
  rx: JSON.stringify({ status: 200, entryId: d.entryId, channel: d.channel, span }), mimetype_rx: "application/json",
1929
- status_rx: 200, tokens: 0, state: "resolved", outcome: null, attrs: "{}",
2152
+ status_rx: 200, tokens: 0, state: "resolved", outcome: null,
2153
+ attrs: gitByPath.has(d.pathname)
2154
+ ? JSON.stringify({ git: gitByPath.get(d.pathname) })
2155
+ : "{}",
1930
2156
  });
1931
2157
  }
1932
2158
  }
@@ -1957,6 +2183,7 @@ export default class Engine {
1957
2183
  sequence,
1958
2184
  origin: "plurnk",
1959
2185
  source: null,
2186
+ model_call_id: null,
1960
2187
  op: "prompt",
1961
2188
  suffix: "",
1962
2189
  signal: null,
@@ -2083,11 +2310,12 @@ export default class Engine {
2083
2310
  };
2084
2311
  const entry = {
2085
2312
  channels: { body: { content: prompt, mimetype: "text/markdown" } },
2086
- tags: [],
2087
2313
  attributes: { openPaths },
2088
2314
  };
2089
2315
  await EntryCrud.writeEntry(pathname, entry, ctx, "prompt", workerId);
2090
2316
  return { loopId, turnSeq };
2091
2317
  }
2092
2318
  }
2319
+ _a = Engine;
2320
+ export default Engine;
2093
2321
  //# sourceMappingURL=Engine.js.map