@plurnk/plurnk-service 1.6.1 → 1.8.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 (310) hide show
  1. package/.env.defaults +29 -42
  2. package/INSTALL.md +49 -10
  3. package/README.md +11 -1
  4. package/SPEC.md +702 -287
  5. package/digest-sql/channel-dispositions/dispositions.sql +15 -0
  6. package/digest-sql/channel-state/state.sql +6 -0
  7. package/digest-sql/curation/curation.sql +5 -0
  8. package/digest-sql/derivations/derivations.sql +4 -0
  9. package/digest-sql/embeddings/embeddings.sql +4 -0
  10. package/digest-sql/entry-dispositions/dispositions.sql +15 -0
  11. package/digest-sql/entry-state/state.sql +6 -0
  12. package/digest-sql/token-counts/token-counts.sql +2 -0
  13. package/dist/Paths.d.ts +2 -5
  14. package/dist/Paths.d.ts.map +1 -1
  15. package/dist/Paths.js +9 -21
  16. package/dist/Paths.js.map +1 -1
  17. package/dist/build-info.json +1 -1
  18. package/dist/content/read-projector.d.ts.map +1 -1
  19. package/dist/content/read-projector.js +4 -3
  20. package/dist/content/read-projector.js.map +1 -1
  21. package/dist/core/BudgetReadout.d.ts.map +1 -1
  22. package/dist/core/BudgetReadout.js +12 -22
  23. package/dist/core/BudgetReadout.js.map +1 -1
  24. package/dist/core/ChannelWrite.d.ts.map +1 -1
  25. package/dist/core/ChannelWrite.js.map +1 -1
  26. package/dist/core/ChannelWrite.sql +2 -1
  27. package/dist/core/ClientInteractions.d.ts +19 -0
  28. package/dist/core/ClientInteractions.d.ts.map +1 -0
  29. package/dist/core/ClientInteractions.js +124 -0
  30. package/dist/core/ClientInteractions.js.map +1 -0
  31. package/dist/core/ClientInteractions.sql +34 -0
  32. package/dist/core/CoreSchemeServices.d.ts +8 -2
  33. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  34. package/dist/core/CoreSchemeServices.js +7 -1
  35. package/dist/core/CoreSchemeServices.js.map +1 -1
  36. package/dist/core/Dispatcher.d.ts +9 -6
  37. package/dist/core/Dispatcher.d.ts.map +1 -1
  38. package/dist/core/Dispatcher.js +74 -125
  39. package/dist/core/Dispatcher.js.map +1 -1
  40. package/dist/core/Engine.d.ts +26 -9
  41. package/dist/core/Engine.d.ts.map +1 -1
  42. package/dist/core/Engine.js +125 -44
  43. package/dist/core/Engine.js.map +1 -1
  44. package/dist/core/Engine.sql +112 -107
  45. package/dist/core/ExecutorRegistry.d.ts +19 -5
  46. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  47. package/dist/core/ExecutorRegistry.js +144 -18
  48. package/dist/core/ExecutorRegistry.js.map +1 -1
  49. package/dist/core/HostPaths.d.ts +29 -0
  50. package/dist/core/HostPaths.d.ts.map +1 -0
  51. package/dist/core/HostPaths.js +74 -0
  52. package/dist/core/HostPaths.js.map +1 -0
  53. package/dist/core/LegacyHome.d.ts +10 -0
  54. package/dist/core/LegacyHome.d.ts.map +1 -0
  55. package/dist/core/LegacyHome.js +170 -0
  56. package/dist/core/LegacyHome.js.map +1 -0
  57. package/dist/core/LogBody.d.ts +1 -1
  58. package/dist/core/LogBody.d.ts.map +1 -1
  59. package/dist/core/LogBody.js +2 -2
  60. package/dist/core/LogBody.js.map +1 -1
  61. package/dist/core/LogEntryProjection.js +1 -1
  62. package/dist/core/LogEntryProjection.js.map +1 -1
  63. package/dist/core/LogVisibility.d.ts +28 -0
  64. package/dist/core/LogVisibility.d.ts.map +1 -0
  65. package/dist/core/LogVisibility.js +215 -0
  66. package/dist/core/LogVisibility.js.map +1 -0
  67. package/dist/core/LoopLifecycle.d.ts +1 -0
  68. package/dist/core/LoopLifecycle.d.ts.map +1 -1
  69. package/dist/core/LoopLifecycle.js +8 -0
  70. package/dist/core/LoopLifecycle.js.map +1 -1
  71. package/dist/core/LoopLifecycle.sql +10 -0
  72. package/dist/core/OperatorConfig.d.ts +6 -0
  73. package/dist/core/OperatorConfig.d.ts.map +1 -0
  74. package/dist/core/OperatorConfig.js +92 -0
  75. package/dist/core/OperatorConfig.js.map +1 -0
  76. package/dist/core/OverflowTurn.d.ts +11 -0
  77. package/dist/core/OverflowTurn.d.ts.map +1 -0
  78. package/dist/core/OverflowTurn.js +77 -0
  79. package/dist/core/OverflowTurn.js.map +1 -0
  80. package/dist/core/Owner.js +2 -2
  81. package/dist/core/Owner.js.map +1 -1
  82. package/dist/core/PacketBuilder.d.ts +3 -15
  83. package/dist/core/PacketBuilder.d.ts.map +1 -1
  84. package/dist/core/PacketBuilder.js +52 -113
  85. package/dist/core/PacketBuilder.js.map +1 -1
  86. package/dist/core/ProblemLog.js +1 -1
  87. package/dist/core/ProblemLog.js.map +1 -1
  88. package/dist/core/ProposalLifecycle.d.ts +3 -1
  89. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  90. package/dist/core/ProposalLifecycle.js +9 -26
  91. package/dist/core/ProposalLifecycle.js.map +1 -1
  92. package/dist/core/ProviderInstantiate.d.ts +9 -5
  93. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  94. package/dist/core/ProviderInstantiate.js +99 -104
  95. package/dist/core/ProviderInstantiate.js.map +1 -1
  96. package/dist/core/ResourceMutations.d.ts +2 -2
  97. package/dist/core/ResourceMutations.d.ts.map +1 -1
  98. package/dist/core/ResourceMutations.js +13 -11
  99. package/dist/core/ResourceMutations.js.map +1 -1
  100. package/dist/core/SchemeRegistry.d.ts +18 -11
  101. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  102. package/dist/core/SchemeRegistry.js +201 -53
  103. package/dist/core/SchemeRegistry.js.map +1 -1
  104. package/dist/core/ToolResources.d.ts +19 -0
  105. package/dist/core/ToolResources.d.ts.map +1 -0
  106. package/dist/core/ToolResources.js +122 -0
  107. package/dist/core/ToolResources.js.map +1 -0
  108. package/dist/core/Turn.d.ts +27 -0
  109. package/dist/core/Turn.d.ts.map +1 -0
  110. package/dist/core/Turn.js +43 -0
  111. package/dist/core/Turn.js.map +1 -0
  112. package/dist/core/Turn.sql +72 -0
  113. package/dist/core/TurnOps.d.ts +7 -0
  114. package/dist/core/TurnOps.d.ts.map +1 -0
  115. package/dist/core/TurnOps.js +63 -0
  116. package/dist/core/TurnOps.js.map +1 -0
  117. package/dist/core/TurnRunner.d.ts +15 -5
  118. package/dist/core/TurnRunner.d.ts.map +1 -1
  119. package/dist/core/TurnRunner.js +1050 -900
  120. package/dist/core/TurnRunner.js.map +1 -1
  121. package/dist/core/WorkerName.d.ts +1 -1
  122. package/dist/core/WorkerName.d.ts.map +1 -1
  123. package/dist/core/WorkspaceGate.d.ts +1 -0
  124. package/dist/core/WorkspaceGate.d.ts.map +1 -1
  125. package/dist/core/WorkspaceGate.js +39 -0
  126. package/dist/core/WorkspaceGate.js.map +1 -1
  127. package/dist/core/caps/CoreInteractionCaps.d.ts +9 -0
  128. package/dist/core/caps/CoreInteractionCaps.d.ts.map +1 -0
  129. package/dist/core/caps/CoreInteractionCaps.js +14 -0
  130. package/dist/core/caps/CoreInteractionCaps.js.map +1 -0
  131. package/dist/core/caps/SchemeCtxImpl.d.ts +2 -1
  132. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  133. package/dist/core/caps/SchemeCtxImpl.js +3 -0
  134. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  135. package/dist/core/env-defaults.d.ts.map +1 -1
  136. package/dist/core/env-defaults.js +7 -5
  137. package/dist/core/env-defaults.js.map +1 -1
  138. package/dist/core/fork.d.ts.map +1 -1
  139. package/dist/core/fork.js +15 -4
  140. package/dist/core/fork.js.map +1 -1
  141. package/dist/core/fork.sql +57 -27
  142. package/dist/core/git-env.d.ts +0 -1
  143. package/dist/core/git-env.d.ts.map +1 -1
  144. package/dist/core/git-env.js +0 -3
  145. package/dist/core/git-env.js.map +1 -1
  146. package/dist/core/git-membership.d.ts.map +1 -1
  147. package/dist/core/git-membership.js +10 -26
  148. package/dist/core/git-membership.js.map +1 -1
  149. package/dist/core/git-state.d.ts.map +1 -1
  150. package/dist/core/git-state.js +13 -14
  151. package/dist/core/git-state.js.map +1 -1
  152. package/dist/core/operation-target-groups.d.ts +3 -0
  153. package/dist/core/operation-target-groups.d.ts.map +1 -0
  154. package/dist/core/operation-target-groups.js +53 -0
  155. package/dist/core/operation-target-groups.js.map +1 -0
  156. package/dist/core/packet-inject.d.ts +0 -1
  157. package/dist/core/packet-inject.d.ts.map +1 -1
  158. package/dist/core/packet-inject.js +6 -13
  159. package/dist/core/packet-inject.js.map +1 -1
  160. package/dist/core/packet-wire.d.ts +4 -0
  161. package/dist/core/packet-wire.d.ts.map +1 -1
  162. package/dist/core/packet-wire.js +208 -44
  163. package/dist/core/packet-wire.js.map +1 -1
  164. package/dist/core/plurnk-uri.d.ts +1 -1
  165. package/dist/core/plurnk-uri.d.ts.map +1 -1
  166. package/dist/core/plurnk-uri.js +3 -3
  167. package/dist/core/plurnk-uri.js.map +1 -1
  168. package/dist/core/scheme-types.d.ts +2 -1
  169. package/dist/core/scheme-types.d.ts.map +1 -1
  170. package/dist/core/scheme-types.js.map +1 -1
  171. package/dist/core/teaching.d.ts.map +1 -1
  172. package/dist/core/teaching.js +4 -5
  173. package/dist/core/teaching.js.map +1 -1
  174. package/dist/core/worker-settings.d.ts +9 -0
  175. package/dist/core/worker-settings.d.ts.map +1 -0
  176. package/dist/core/worker-settings.js +25 -0
  177. package/dist/core/worker-settings.js.map +1 -0
  178. package/dist/core/workspace-settings.d.ts +0 -11
  179. package/dist/core/workspace-settings.d.ts.map +1 -1
  180. package/dist/core/workspace-settings.js +3 -47
  181. package/dist/core/workspace-settings.js.map +1 -1
  182. package/dist/digest/Digest.d.ts.map +1 -1
  183. package/dist/digest/Digest.js +164 -92
  184. package/dist/digest/Digest.js.map +1 -1
  185. package/dist/digest/digest.sql +8 -2
  186. package/dist/observe/genai.d.ts +9 -0
  187. package/dist/observe/genai.d.ts.map +1 -0
  188. package/dist/observe/genai.js +31 -0
  189. package/dist/observe/genai.js.map +1 -0
  190. package/dist/observe/spans.d.ts +3 -3
  191. package/dist/observe/spans.d.ts.map +1 -1
  192. package/dist/observe/spans.js +20 -5
  193. package/dist/observe/spans.js.map +1 -1
  194. package/dist/schemes/EffectPolicy.d.ts +3 -0
  195. package/dist/schemes/EffectPolicy.d.ts.map +1 -1
  196. package/dist/schemes/EffectPolicy.js +38 -6
  197. package/dist/schemes/EffectPolicy.js.map +1 -1
  198. package/dist/schemes/Exec.d.ts.map +1 -1
  199. package/dist/schemes/Exec.js +82 -18
  200. package/dist/schemes/Exec.js.map +1 -1
  201. package/dist/schemes/Log.d.ts +6 -2
  202. package/dist/schemes/Log.d.ts.map +1 -1
  203. package/dist/schemes/Log.js +73 -22
  204. package/dist/schemes/Log.js.map +1 -1
  205. package/dist/schemes/Log.sql +25 -7
  206. package/dist/schemes/Prompt.d.ts.map +1 -1
  207. package/dist/schemes/Prompt.js +1 -2
  208. package/dist/schemes/Prompt.js.map +1 -1
  209. package/dist/schemes/QuestionTool.d.ts +10 -0
  210. package/dist/schemes/QuestionTool.d.ts.map +1 -0
  211. package/dist/schemes/QuestionTool.js +81 -0
  212. package/dist/schemes/QuestionTool.js.map +1 -0
  213. package/dist/schemes/Skill.d.ts.map +1 -1
  214. package/dist/schemes/Skill.js +4 -1
  215. package/dist/schemes/Skill.js.map +1 -1
  216. package/dist/schemes/Worker.d.ts.map +1 -1
  217. package/dist/schemes/Worker.js +20 -3
  218. package/dist/schemes/Worker.js.map +1 -1
  219. package/dist/schemes/_entry-crud.sql +19 -1
  220. package/dist/schemes/_entry-find.d.ts +5 -3
  221. package/dist/schemes/_entry-find.d.ts.map +1 -1
  222. package/dist/schemes/_entry-find.js +102 -22
  223. package/dist/schemes/_entry-find.js.map +1 -1
  224. package/dist/schemes/_entry-find.sql +8 -6
  225. package/dist/schemes/_entry-graph.sql +2 -6
  226. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  227. package/dist/schemes/_entry-manifest.js +12 -1
  228. package/dist/schemes/_entry-manifest.js.map +1 -1
  229. package/dist/schemes/_search-index.d.ts.map +1 -1
  230. package/dist/schemes/_search-index.js +37 -25
  231. package/dist/schemes/_search-index.js.map +1 -1
  232. package/dist/server/Daemon.d.ts +69 -12
  233. package/dist/server/Daemon.d.ts.map +1 -1
  234. package/dist/server/Daemon.js +737 -133
  235. package/dist/server/Daemon.js.map +1 -1
  236. package/dist/server/DaemonModule.d.ts +31 -3
  237. package/dist/server/DaemonModule.d.ts.map +1 -1
  238. package/dist/server/DrainSupervisor.d.ts +12 -9
  239. package/dist/server/DrainSupervisor.d.ts.map +1 -1
  240. package/dist/server/DrainSupervisor.js +59 -10
  241. package/dist/server/DrainSupervisor.js.map +1 -1
  242. package/dist/server/client-input.d.ts +7 -2
  243. package/dist/server/client-input.d.ts.map +1 -1
  244. package/dist/server/client-input.js +65 -34
  245. package/dist/server/client-input.js.map +1 -1
  246. package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
  247. package/dist/server/dispatch-as-plurnk.js +28 -6
  248. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  249. package/dist/server/drain.sql +26 -11
  250. package/dist/server/envelope.d.ts +1 -1
  251. package/dist/server/envelope.d.ts.map +1 -1
  252. package/dist/server/envelope.js +4 -4
  253. package/dist/server/envelope.js.map +1 -1
  254. package/dist/server/envelope.sql +25 -1
  255. package/dist/server/lifecycle-recovery.sql +17 -1
  256. package/dist/server/logEntry.d.ts +2 -1
  257. package/dist/server/logEntry.d.ts.map +1 -1
  258. package/dist/server/logEntry.js +3 -2
  259. package/dist/server/logEntry.js.map +1 -1
  260. package/dist/server/logEntry.sql +7 -0
  261. package/dist/server/loop-model.d.ts +2 -2
  262. package/dist/server/loop-model.d.ts.map +1 -1
  263. package/dist/server/loop-model.js +23 -33
  264. package/dist/server/loop-model.js.map +1 -1
  265. package/dist/server/loopDocs.d.ts +1 -0
  266. package/dist/server/loopDocs.d.ts.map +1 -1
  267. package/dist/server/loopDocs.js +94 -14
  268. package/dist/server/loopDocs.js.map +1 -1
  269. package/dist/server/loopDocs.sql +12 -0
  270. package/dist/server/model-catalog.d.ts +3 -0
  271. package/dist/server/model-catalog.d.ts.map +1 -0
  272. package/dist/server/model-catalog.js +83 -0
  273. package/dist/server/model-catalog.js.map +1 -0
  274. package/dist/server/model-route.d.ts +7 -0
  275. package/dist/server/model-route.d.ts.map +1 -0
  276. package/dist/server/model-route.js +47 -0
  277. package/dist/server/model-route.js.map +1 -0
  278. package/dist/server/module-discovery.d.ts +13 -0
  279. package/dist/server/module-discovery.d.ts.map +1 -0
  280. package/dist/server/module-discovery.js +78 -0
  281. package/dist/server/module-discovery.js.map +1 -0
  282. package/dist/server/skillDocs.d.ts +9 -0
  283. package/dist/server/skillDocs.d.ts.map +1 -0
  284. package/dist/server/skillDocs.js +212 -0
  285. package/dist/server/skillDocs.js.map +1 -0
  286. package/dist/server/skillDocs.sql +14 -0
  287. package/dist/server/workspace-capabilities.sql +19 -0
  288. package/dist/service.d.ts.map +1 -1
  289. package/dist/service.js +182 -123
  290. package/dist/service.js.map +1 -1
  291. package/migrations/001_schema.sql +455 -65
  292. package/package.json +38 -37
  293. package/dist/core/ExecutableTools.d.ts +0 -10
  294. package/dist/core/ExecutableTools.d.ts.map +0 -1
  295. package/dist/core/ExecutableTools.js +0 -41
  296. package/dist/core/ExecutableTools.js.map +0 -1
  297. package/dist/core/JournalTurn.d.ts +0 -9
  298. package/dist/core/JournalTurn.d.ts.map +0 -1
  299. package/dist/core/JournalTurn.js +0 -14
  300. package/dist/core/JournalTurn.js.map +0 -1
  301. package/dist/core/JournalTurn.sql +0 -10
  302. package/dist/core/git-iso.d.ts +0 -13
  303. package/dist/core/git-iso.d.ts.map +0 -1
  304. package/dist/core/git-iso.js +0 -267
  305. package/dist/core/git-iso.js.map +0 -1
  306. package/dist/core/search-gate.d.ts +0 -17
  307. package/dist/core/search-gate.d.ts.map +0 -1
  308. package/dist/core/search-gate.js +0 -77
  309. package/dist/core/search-gate.js.map +0 -1
  310. package/dist/digest/Digest.sql +0 -1
@@ -1,7 +1,7 @@
1
1
  var _a;
2
2
  import { PlurnkParser, PlurnkParseError, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
3
3
  import Owner from "./Owner.js";
4
- const TERMINAL_SEND_SIGNALS = new Set([102, 200, 202, 300, 499]);
4
+ const TERMINAL_SEND_SIGNALS = new Set([102, 200, 202, 499]);
5
5
  const comparePosition = (a, b) => a.line - b.line || a.column - b.column;
6
6
  import Meta from "@plurnk/plurnk-meta";
7
7
  import EntryCrud from "../schemes/_entry-crud.js";
@@ -11,6 +11,14 @@ import WorkspaceSettings from "./workspace-settings.js";
11
11
  import { editedSpan } from "../content/index.js";
12
12
  import { promptPathname, promptLoopPrefix, renderTarget } from "./plurnk-uri.js";
13
13
  import { readFile } from "node:fs/promises";
14
+ // {§grammar-rail-registration} — bare names are the built-in rail namespace;
15
+ // everything else is an import specifier resolved through the Node chain.
16
+ export const resolveGrammarRailPath = (variant) => variant.startsWith("/") || variant.startsWith(".")
17
+ || variant.includes("/") || variant.includes(":")
18
+ ? (variant.startsWith("/") || variant.startsWith(".")
19
+ ? variant
20
+ : fileURLToPath(import.meta.resolve(variant)))
21
+ : fileURLToPath(import.meta.resolve(`@plurnk/plurnk-contracts/${variant}`));
14
22
  import { fileURLToPath } from "node:url";
15
23
  import { randomUUID } from "node:crypto";
16
24
  // Shared module imported by both Engine and bin/digest.ts, so wire
@@ -20,16 +28,22 @@ import PacketWire from "./packet-wire.js";
20
28
  import Results, { OperationFailureError } from "./results.js";
21
29
  import BranchReceipt from "./BranchReceipt.js";
22
30
  import TerminalResult from "./TerminalResult.js";
31
+ import LoopLifecycle from "./LoopLifecycle.js";
23
32
  import WorkerControlAddress from "./WorkerControlAddress.js";
24
- import JournalTurn from "./JournalTurn.js";
33
+ import Turn from "./Turn.js";
25
34
  import LogBody from "./LogBody.js";
35
+ import LogVisibility from "./LogVisibility.js";
26
36
  import StrikeRail from "./StrikeRail.js";
27
37
  import StoredPacket from "./StoredPacket.js";
28
38
  import { observed, observedSync } from "../observe/spans.js";
39
+ import { GEN_AI_REQUEST_SPAN, genAiRequestOptions, settleGenAiResponse, } from "../observe/genai.js";
29
40
  import { OPS_DISPATCHED, PROVIDER_CALLS, recordCounter } from "../observe/metrics.js";
30
41
  import { scheduleTurnOps } from "./turn-scheduler.js";
42
+ import { expandSafeUriTargetGroup } from "./operation-target-groups.js";
31
43
  import { readOptimisticSettlementMs } from "./optimistic-settlement.js";
32
44
  import ModelCall, { ModelCallPersistenceError, ProviderAccountingIntegrityError, } from "./ModelCall.js";
45
+ import OverflowTurn from "./OverflowTurn.js";
46
+ import TurnOps from "./TurnOps.js";
33
47
  const RECORD_STREAM_MIMETYPES = new Set(["application/jsonl", "application/x-ndjson"]);
34
48
  const baseMimetype = (mimetype) => mimetype.split(";", 1)[0].trim().toLowerCase();
35
49
  // {§exec-stream} Active streams publish only independently meaningful units.
@@ -56,8 +70,8 @@ const ENGINE_PROBLEMS = Object.freeze({
56
70
  });
57
71
  // The prompt entry target - prompt:///<loop>/<N>, self-only ({§prompt-self-only}):
58
72
  // the owner rides the owner_id column, the address carries only the loop coordinate.
59
- const promptTarget = (loopSeq, turnSeq) => {
60
- const storage = promptPathname(loopSeq, turnSeq);
73
+ const promptTarget = (loopSeq, promptOrdinal) => {
74
+ const storage = promptPathname(loopSeq, promptOrdinal);
61
75
  return {
62
76
  kind: "url", raw: `prompt://${storage}`,
63
77
  scheme: "prompt", username: null, password: null,
@@ -101,7 +115,7 @@ const readFilesItems = () => {
101
115
  return null;
102
116
  return normalizeFilesItems(Number.parseInt(raw, 10));
103
117
  };
104
- import { ProviderError, resolveActiveAlias, scopeEnvToAlias, } from "@plurnk/plurnk-providers";
118
+ import { ProviderError, scopeEnvToAlias, } from "@plurnk/plurnk-providers";
105
119
  import { validateGbnf } from "@plurnk/gbnf";
106
120
  import ProviderInstantiate from "./ProviderInstantiate.js";
107
121
  // A generated rail may constrain only the sampled continuation while the chat
@@ -126,8 +140,8 @@ const grammarConstraint = (grammar) => {
126
140
  response: grammar.replace(roots[0][0], `root ::= ${responseRoot}`),
127
141
  };
128
142
  };
129
- 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.";
130
- const curationOverflowFailure = (pressure) => Results.failure("engine:context", "token-budget-overflow", 413, TOKEN_BUDGET_OVERFLOW_DETAIL, {}, {
143
+ const TOKEN_BUDGET_OVERFLOW_HARD_DETAIL = "Context Token Budget Overflow: tokensActiveTotal exceeded tokensActiveMax, and the causal overflow turn could not make enough room.";
144
+ const curationOverflowFailure = (pressure) => Results.failure("engine:context", "token-budget-overflow", 413, TOKEN_BUDGET_OVERFLOW_HARD_DETAIL, {}, {
131
145
  usage: pressure.weight,
132
146
  ceiling: pressure.budget,
133
147
  deficit: pressure.excess,
@@ -162,11 +176,12 @@ class TurnRunner {
162
176
  #wakeWorkerNotify;
163
177
  #executors;
164
178
  #loopSignal;
179
+ #interactions;
165
180
  #warmWorkspace;
166
181
  #dispatch;
167
182
  #resolveWorkerProviderIdentity;
168
183
  #gbnfCache = new Map();
169
- constructor({ db, schemes, mimetypes, weigh, notices, problems, strikes, packets, dispatcher, liveSubscriptions, streamEventNotify, wakeWorkerNotify, executors, loopSignal, warmWorkspace, dispatch, resolveWorkerProviderIdentity, }) {
184
+ constructor({ db, schemes, mimetypes, weigh, notices, problems, strikes, packets, dispatcher, liveSubscriptions, streamEventNotify, wakeWorkerNotify, executors, loopSignal, interactions, warmWorkspace, dispatch, resolveWorkerProviderIdentity, }) {
170
185
  this.#db = db;
171
186
  this.#schemes = schemes;
172
187
  this.#mimetypes = mimetypes;
@@ -181,6 +196,7 @@ class TurnRunner {
181
196
  this.#wakeWorkerNotify = wakeWorkerNotify;
182
197
  this.#executors = executors;
183
198
  this.#loopSignal = loopSignal;
199
+ this.#interactions = interactions;
184
200
  this.#warmWorkspace = warmWorkspace;
185
201
  this.#dispatch = dispatch;
186
202
  this.#resolveWorkerProviderIdentity = resolveWorkerProviderIdentity;
@@ -194,7 +210,7 @@ class TurnRunner {
194
210
  _a.#railGapWarned.add(message);
195
211
  process.stderr.write(`plurnk-engine: rail verdict unavailable — the configured grammar did not parse in @plurnk/gbnf (${message})\n`);
196
212
  }
197
- static #requireGrammarEvidence(response) {
213
+ static #requireGrammarEvidence(response, allowWithheld) {
198
214
  const evidence = response.grammarEvidence;
199
215
  if (evidence === undefined) {
200
216
  throw new Error("provider contract violation: configured GBNF response omitted grammar evidence");
@@ -207,32 +223,42 @@ class TurnRunner {
207
223
  || input.slice(evidence.contentStart).join("") !== response.assistant.content) {
208
224
  throw new Error("provider contract violation: grammar evidence does not map exactly to assistant.content");
209
225
  }
226
+ if (!allowWithheld && !evidence.transported) {
227
+ throw new Error("provider contract violation: configured GBNF was not transported outside explicit debug mode");
228
+ }
210
229
  return evidence;
211
230
  }
212
231
  async #grammarConstraint(provider) {
213
- // Resolve through the registered or active alias; ambiguity and load
214
- // failures never degrade to unconstrained generation.
215
- // {§grammar-enforcement-verified-at-boot}
216
- const registered = ProviderInstantiate.aliasOf(provider);
217
- const fallback = registered === undefined ? resolveActiveAlias(process.env)?.alias : undefined;
218
- if (registered === undefined && fallback === undefined && Object.keys(process.env).some((k) => k.startsWith("PLURNK_PROVIDERS_GBNF_"))) {
219
- throw new Error("GBNF constraint: provider has no registered alias and no active alias resolves, while per-alias PLURNK_PROVIDERS_GBNF_* constraints are configured");
220
- }
221
- const alias = registered ?? fallback ?? "";
222
- const variant = scopeEnvToAlias(process.env, alias, ["PLURNK_PROVIDERS_GBNF"]).PLURNK_PROVIDERS_GBNF;
232
+ // A real alias scopes its rail; an alias-free direct route uses the
233
+ // global provider configuration. Unrelated alias settings never apply.
234
+ // {§grammar-configuration-admission}
235
+ ProviderInstantiate.assertGrammarConfigurationScope(provider);
236
+ const alias = ProviderInstantiate.configurationAliasOf(provider);
237
+ const scoped = alias === undefined
238
+ ? process.env
239
+ : scopeEnvToAlias(process.env, alias, [
240
+ "PLURNK_PROVIDERS_GBNF",
241
+ "PLURNK_PROVIDERS_GBNF_DEBUG",
242
+ ]);
243
+ const variant = scoped.PLURNK_PROVIDERS_GBNF;
223
244
  if (variant === undefined || variant === "" || variant === "0")
224
245
  return undefined;
246
+ const allowWithheld = scoped.PLURNK_PROVIDERS_GBNF_DEBUG !== undefined
247
+ && scoped.PLURNK_PROVIDERS_GBNF_DEBUG !== ""
248
+ && scoped.PLURNK_PROVIDERS_GBNF_DEBUG !== "0";
225
249
  const hit = this.#gbnfCache.get(variant);
226
250
  if (hit !== undefined)
227
- return hit;
228
- const path = variant.startsWith("/") || variant.startsWith(".")
229
- ? variant
230
- : fileURLToPath(import.meta.resolve(`@plurnk/plurnk-contracts/${variant}`));
251
+ return { ...hit, allowWithheld };
252
+ // {§grammar-rail-registration} — a bare name is a built-in rail subpath
253
+ // under @plurnk/plurnk-contracts; any other form is an import specifier
254
+ // (an operator file path or a package export subpath), so a third-party
255
+ // rail package plugs in with no built-in registry change.
256
+ const path = resolveGrammarRailPath(variant);
231
257
  const text = await readFile(path, "utf8"); // unresolvable/unreadable throws — a configured rail never silently degrades
232
258
  const constraint = grammarConstraint(text);
233
259
  this.#gbnfCache.set(variant, constraint);
234
260
  process.stderr.write(`plurnk-engine: GBNF constraint: ${alias || "(bare)"} → ${variant} (${text.length} chars)\n`);
235
- return constraint;
261
+ return { ...constraint, allowWithheld };
236
262
  }
237
263
  // A lineage's no-parent root; a root worker resolves to itself. Fail hard
238
264
  // when corruption leaves a worker without one. {§worker-primary}
@@ -280,7 +306,7 @@ class TurnRunner {
280
306
  }
281
307
  const settlements = await Promise.allSettled(prepared.map(async ({ statement, modelCall, attributions, providerWorkerId }) => {
282
308
  try {
283
- const response = await observed("provider.generate", { model: provider.model, attempt: 1, kind: "bare" }, async (span) => {
309
+ const response = await observed(GEN_AI_REQUEST_SPAN, { model: provider.model, attempt: 1, kind: "bare" }, async (span) => {
284
310
  try {
285
311
  const generated = await provider.generate({
286
312
  messages: [{ role: "user", content: statement.body }],
@@ -301,6 +327,7 @@ class TurnRunner {
301
327
  status: "resolved",
302
328
  });
303
329
  span.setAttribute("status", "resolved");
330
+ settleGenAiResponse(span, generated);
304
331
  return generated;
305
332
  }
306
333
  catch (error) {
@@ -309,7 +336,7 @@ class TurnRunner {
309
336
  }
310
337
  throw error;
311
338
  }
312
- });
339
+ }, genAiRequestOptions(ProviderInstantiate.aliasOf(provider) ?? "plurnk", provider.model));
313
340
  await modelCall.observeResponse(response);
314
341
  return {
315
342
  statement,
@@ -359,776 +386,44 @@ class TurnRunner {
359
386
  }
360
387
  return { line, column };
361
388
  }
362
- async runTurn({ provider, childProvider = provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, invalidEmissionRecoveryEntryId, }) {
363
- const allowInvalidEmissionRecovery = invalidEmissionRecoveryEntryId === null;
364
- const transientOpenLogEntryId = typeof invalidEmissionRecoveryEntryId === "number"
365
- ? invalidEmissionRecoveryEntryId
366
- : null;
367
- // === Turn-as-container model ===
368
- //
369
- // Turn rows are created at runTurn OPEN (status=102, placeholder
370
- // packet) so things can be written into the turn before the model
371
- // is called: the user prompt on turn 1; later, system signals or
372
- // injected Notices on any turn. The turn is CLOSED at
373
- // the end with the final packet + status + usage stats.
374
- //
375
- // sequence is "ordinal of stuff in this turn." Pre-model
376
- // writes consume low indices; model ops continue from there.
377
- const seqRow = await this.#db.engine_next_turn_sequence.get({ loop_id: loopId });
378
- const seq = seqRow.next;
379
- // loops.sequence is the loop's ordinal within the worker. Turn-0 foists that belong to the
380
- // WORKER (manifest preview, AGENTS, operator docs) gate on the worker's first loop, not every loop's
381
- // first turn ({§actor-boundary-catalog-preview}); per-loop foists such as
382
- // {§prompt-entry} still fire each loop. Read once, turn-1 only.
383
- const loopRow = seq === 1
384
- ? await this.#db.engine_get_loop_prompt.get({ loop_id: loopId })
385
- : undefined;
386
- const workerFirstLoop = (loopRow?.sequence ?? 0) === 1;
387
- const openRow = await this.#db.engine_open_turn.get({
388
- loop_id: loopId, sequence: seq,
389
- });
390
- if (openRow === undefined)
391
- throw new Error("Engine.runTurn: turn open returned no row");
392
- const turnId = openRow.id;
393
- // {§env-delta-log-pull} — establish a fresh worker's observation
394
- // baseline immediately after its first turn opens. A fork already has
395
- // its parent's cursor, so the NULL-guarded statement leaves it intact.
396
- // Events committed after this statement belong to this or a later
397
- // closed pull window; pre-existing state is read through the ordinary
398
- // shared-world projections in this first packet.
399
- await this.#db.engine_initialize_ambient_cursor.get({ workspace_id: workspaceId, worker_id: workerId });
400
- // Threaded per turn, never engine state, so concurrent loops on
401
- // different providers each read their own honest tokenizer values.
402
- const systemCtx = {
403
- db: this.#db, workspaceId, workerId, loopId, turnId,
404
- writer: "plurnk",
405
- signal: this.#loopSignal(loopId),
406
- streamEventNotify: this.#streamEventNotify,
407
- wakeWorkerNotify: this.#wakeWorkerNotify,
408
- weigh: this.#weighContent,
409
- mimetypes: this.#mimetypes,
410
- defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s),
411
- pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
412
- };
413
- // Pre-model writes. Each prompt the model has not seen yet becomes an
414
- // actionless `prompt` log row whose target is its durable prompt:// entry:
415
- // - Turn 1: loop.prompt is the initial user prompt.
416
- // - Turn N>1: only if Engine.inject (or wake-on-completion via
417
- // daemon.inject) wrote a prompt entry for this turn slot
418
- // between turn N-1 and N. Inject writes directly to entries;
419
- // we DON'T re-foist here for N>1.
420
- // Model ops dispatch after these pre-model rows.
421
- let nextActionIndex = 1;
422
- const turnOpenPaths = [];
423
- // {§worker-initialization-entry} — the worker's first turn opens with a kernel-authored initialization: a
424
- // worked turn PLAN → the environment FINDs the foist ACTUALLY dispatches → SEND signal 102. Built
425
- // from the real ops below (not a static print — we lean into the genuine echo paradigm) and
426
- // written at sequence 1, so it reads first with the foisted results following.
427
- const turnZeroMoves = [];
428
- if (seq === 1) {
429
- if (workerFirstLoop)
430
- nextActionIndex = 2; // reserve sequence 1 for initialization
431
- // Operator doc READs (PLURNK_SERVICE_MD_<ALIAS>, {§actor-boundary-doc-injection}). The docs were materialized
432
- // as worker://plurnk/<entry> entries by the plurnk worker (LoopDocs, via the
433
- // {§actor-boundary} keystone); foist a READ of each into THIS turn-0 so the model
434
- // reads them inline. It sees only the READ — the materializing EDIT
435
- // lives in the plurnk worker's log, never the model's.
436
- // {§operator-config-workspace-md-docs} — env docs union the workspace's client docs; foist a READ of
437
- // each materialized worker://plurnk/<alias>.md (LoopDocs materialized the same set).
438
- const { mdDocs } = await WorkspaceSettings.read(this.#db, workspaceId);
439
- // {§actor-boundary-doc-injection} — operator docs appear on the worker's first loop.
440
- for (const doc of workerFirstLoop ? await WorkspaceSettings.resolveDocs(mdDocs) : []) {
441
- const docTarget = {
442
- kind: "url", raw: `worker://plurnk/${doc.entryName}`, scheme: "worker",
443
- username: null, password: null, hostname: "plurnk", port: null,
444
- pathname: `/${doc.entryName}`, query: null, fragment: null,
445
- };
446
- const docRead = {
447
- op: "READ", suffix: "", signal: null, target: docTarget,
448
- lineMarker: null, body: null, position: UNKNOWN_POSITION,
449
- };
450
- await this.#dispatch({
451
- statement: docRead, workspaceId, workerId, loopId, turnId,
452
- sequence: nextActionIndex, origin: "plurnk", onDispatch,
453
- });
454
- nextActionIndex++;
455
- }
456
- const promptRow = loopRow; // {§prompt-entry} — per-loop; fires every loop's turn 1
457
- if (promptRow !== undefined && typeof promptRow.prompt === "string" && promptRow.prompt.length > 0) {
458
- const openPaths = assertOpenPaths(JSON.parse(promptRow.open_paths), `Loop ${loopId} open_paths`);
459
- const promptLoopSeq = promptRow.sequence; // the loop's per-worker sequence — model-facing, matching log coordinates (owner: the db id read as prompt/2/1)
460
- const promptPath = promptTarget(promptLoopSeq, seq);
461
- const entry = {
462
- channels: { body: { content: promptRow.prompt, mimetype: "text/markdown" } },
463
- attributes: { openPaths },
464
- };
465
- await EntryCrud.writeEntry(promptPath.pathname, entry, systemCtx, "prompt", workerId);
466
- turnOpenPaths.push(...openPaths);
467
- const promptLogId = await this.#writePromptLog({
468
- workerId,
469
- loopId,
470
- turnId,
471
- sequence: nextActionIndex,
472
- target: promptPath,
473
- content: promptRow.prompt,
474
- });
475
- onDispatch?.(promptLogId);
476
- nextActionIndex++;
477
- }
478
- }
479
- // {§prompt-loop-containment}: the loop contains every prompt that arrived
480
- // while it ran. Publish each undelivered frame as a prompt row, oldest
481
- // first, so rapid arrivals reach the model together exactly once.
482
- {
483
- const loopSeqRow = await this.#db.engine_loop_sequence.get({ loop_id: loopId });
484
- const loopSeq = loopSeqRow?.sequence ?? loopId;
485
- const prefix = promptLoopPrefix(loopSeq);
486
- const undelivered = (await this.#db.drain_undelivered_prompts_for_loop.all({
487
- owner_id: workerId,
488
- pattern: `${prefix}%`,
489
- prefix_len: prefix.length,
490
- loop_id: loopId,
491
- }))
492
- .filter((r) => typeof r.content === "string" && r.content.length > 0);
493
- for (const injectedRow of undelivered) {
494
- const attributes = parsePromptAttributes(injectedRow.attributes, `Prompt ${injectedRow.pathname} attributes`);
495
- const encodedOpenPaths = attributes.openPaths;
496
- if (encodedOpenPaths !== undefined) {
497
- turnOpenPaths.push(...assertOpenPaths(encodedOpenPaths, `Prompt ${injectedRow.pathname} openPaths`));
498
- }
499
- const ordinal = Number(injectedRow.pathname.split("/").filter(Boolean).at(-1));
500
- const injTarget = promptTarget(loopSeq, ordinal);
501
- const promptLogId = await this.#writePromptLog({
502
- workerId,
503
- loopId,
504
- turnId,
505
- sequence: nextActionIndex,
506
- target: injTarget,
507
- content: injectedRow.content,
508
- });
509
- onDispatch?.(promptLogId);
510
- nextActionIndex++;
511
- }
512
- }
513
- // The persistent search-index pass (_search-index.maintain) attaches
514
- // every readable entry/log projection to complete graph/FTS/vector derivations.
515
- // NOT an action: no log entry, no sequence slot,
516
- // not dispatched. There is no materialized manifest entry — the catalog
517
- // is served on demand by FIND: recursive when asked, shallow-mapped below.
518
- // {§semantic-embed-dedup} — one pass-wide semantic plan binds every
519
- // chunk counter to the derivation identity it produces.
520
- // SPEC {§membership} D4/D5 — git-ls-files workspace membership, resolved at
521
- // prompt-composition (EMI is eager + exhaustive — git is the only bound). When the
522
- // workspace's project_root is a git working tree, tracked files are
523
- // members without a client `add`; active members are materialized
524
- // (disk → body channel) so they appear in the catalog. No-ops
525
- // on headless / non-git workspaces. Runs BEFORE the derivation pump so
526
- // this turn's packet reflects them.
527
- // Workspace creation starts this eagerly. Joining here is the correctness
528
- // boundary: the model never runs against partial graph/vector coverage.
529
- await this.#warmWorkspace(systemCtx, false);
530
- // The warm materialized membership before deriving. This second pass is the
531
- // ordinary cheap change detector and captures any drift that landed meanwhile.
532
- const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
533
- // {§packet-git-status} — one post-reconciliation snapshot supplies both
534
- // the compact packet summary and each causal file event's exact XY state.
535
- const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopSignal(loopId));
536
- await this.#logFsFictions(workspaceId, fsDivergences, gitStatus);
537
- // The refresh above may have changed bodies (including model/client edits
538
- // since the startup warm). Re-derive to completion before packet/model
539
- // construction. Membership is already current, so this pass does not
540
- // consume the filesystem divergences a second time.
541
- await this.#warmWorkspace(systemCtx, true, false);
542
- // Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, {§actor-boundary-catalog-preview}):
543
- // Four FIND surveys foisted into the worker's first model turn establish the project,
544
- // commons, private, and documentation surfaces in that order. Their `init`
545
- // classification lets the model curate this opening survey as one log set.
546
- if (seq === 1) {
547
- // {§operator-config-workspace-files-items} — workspace filesItems replaces the env default.
548
- const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
549
- const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
550
- if (filesItems !== null && workerFirstLoop) { // {§actor-boundary-catalog-preview} — once per worker
551
- const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
552
- const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
553
- const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
554
- await Owner.kernelId(this.#db, workspaceId);
555
- const surveys = [
556
- {
557
- statement: {
558
- op: "FIND", suffix: "", signal: ["+init"],
559
- target: { kind: "local", raw: "*" },
560
- body: null,
561
- lineMarker: fileCap === null ? null : { marks: [1, fileCap] },
562
- position: UNKNOWN_POSITION,
563
- },
564
- exemplar: `## FIND0 [+init] (*)${fileCap === null ? "" : ` <1,${fileCap}>`}`,
565
- },
566
- {
567
- statement: {
568
- op: "FIND", suffix: "", signal: ["+init"],
569
- target: { kind: "url", raw: "worker:///*", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: "/*", query: null, fragment: null },
570
- body: null, lineMarker: null, position: UNKNOWN_POSITION,
571
- },
572
- exemplar: "## FIND0 [+init] (worker:///*)",
573
- },
574
- {
575
- statement: {
576
- op: "FIND", suffix: "", signal: ["+init"],
577
- target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
578
- body: null, lineMarker: null, position: UNKNOWN_POSITION,
579
- },
580
- exemplar: "## FIND0 [+init] (worker://~/*)",
581
- },
582
- {
583
- statement: {
584
- op: "FIND", suffix: "", signal: ["+init", "+docs"],
585
- target: { kind: "url", raw: "worker://plurnk/docs/**", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/docs/**", query: null, fragment: null },
586
- body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
587
- },
588
- exemplar: "## FIND0 [+init,+docs] (worker://plurnk/docs/**) <1,-1>",
589
- },
590
- ];
591
- for (const { statement, exemplar } of surveys) {
592
- await this.#dispatch({ statement, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
593
- nextActionIndex++;
594
- turnZeroMoves.push(exemplar);
595
- }
596
- }
597
- // {§worker-initialization-entry} — write the kernel's turn-0 initialization OPEN at sequence 1: PLAN → the FINDs actually
598
- // foisted above (real, their results already in the log) → SEND signal 102. Dynamic — it reflects
599
- // the true survey, never a frozen print — and OPEN: the worked example the model orients on,
600
- // so the grammar can stay thin.
601
- if (workerFirstLoop) {
602
- const initialization = [
603
- "# PLAN0\n* Initialization complete.\n* Next: address the prompt.",
604
- ...turnZeroMoves,
605
- "## SEND0 [102]\nNext, address the prompt.",
606
- ].join("\n\n");
607
- await this.#dispatcher.writeInitializationEntry({ verbatim: initialization, workerId, loopId, turnId, sequence: 1 });
608
- }
609
- }
610
- // {§methods-loop-run-open-paths}: selected workspace paths belong to
611
- // the prompt frame. Publish the frame, then dispatch ordinary core READs
612
- // in that same turn; missing/non-member paths retain their normal 4xx.
613
- for (const raw of turnOpenPaths) {
614
- const pathname = raw.startsWith("/") ? raw : `/${raw}`;
615
- const fileRead = {
616
- op: "READ", suffix: "", signal: null, lineMarker: null,
617
- target: {
618
- kind: "url", raw: `file://${pathname}`, scheme: "file",
619
- username: null, password: null, hostname: null, port: null,
620
- pathname, query: null, fragment: null,
621
- },
622
- body: null, position: UNKNOWN_POSITION,
623
- };
624
- await this.#dispatch({
625
- statement: fileRead, workspaceId, workerId, loopId, turnId,
626
- sequence: nextActionIndex, origin: "plurnk", onDispatch,
627
- });
628
- nextActionIndex++;
629
- }
630
- // {§env-delta-log-pull} — materialize ambient observations before packet
631
- // composition and reserve their action indices. {§exec-stream} owns the
632
- // distinct byte-cursor path for this worker's streams.
633
- // {§exec-poll} — EXEC `<0>` is turn-scoped: reap the worker's open turn-scoped streams (necessarily
634
- // from a prior turn — this runs before the turn's own spawns) so a `<0>` never survives into
635
- // the subsequent turn. The terminal output then surfaces born-OPEN via the stream-delta path.
636
- await this.#reapTurnScopedStreams(workerId);
637
- nextActionIndex += await this.#materializeEnvironmentDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
638
- nextActionIndex += await this.#materializeStreamDeltas({ workerId, loopId, turnId, fromSequence: nextActionIndex });
639
- // The post-reconciliation Git snapshot above is threaded into the packet
640
- // and every budget rebuild; overflow never shells again.
641
- // Notices are non-terminal observations, never operation-failure truth.
642
- // Drain once and thread the same set through every grinder rebuild.
643
- const notices = this.#notices.drain(loopId)
644
- .filter((event) => event.level !== "info");
645
- // Build the model request packet ({§packet-stored-shape}). The log build
646
- // queries log_entries scoped to the worker — the prompt entry just
647
- // written (if turn 1) is part of that query result.
648
- let promptProjection = "automatic";
649
- const buildPacket = () => this.#packets.buildRequestPacket({
650
- initialMessages: messages,
651
- requirements,
652
- workspaceId,
653
- workerId,
654
- loopId,
655
- currentTurnSeq: seq,
656
- provider,
657
- gitStatus,
658
- notices,
659
- transientOpenLogEntryId,
660
- promptProjection,
661
- });
662
- let requestPacket = await buildPacket();
663
- // SPEC {§grinder} — budget grinder, pre-LLM: reclaim window on actual overflow.
664
- const enforced = await this.#packets.enforceBudget({
665
- packet: requestPacket, provider, loopId, turnId,
666
- recordOverflow: async (pressure) => {
667
- await this.#problems.record({
668
- workerId,
669
- loopId,
670
- turnId,
671
- sequence: nextActionIndex++,
672
- origin: "plurnk",
673
- source: "engine",
674
- result: curationOverflowFailure(pressure),
675
- });
676
- },
677
- rebuild: buildPacket,
678
- });
679
- requestPacket = enforced.packet;
680
- let boundaryRolledBack = enforced.boundaryRolledBack;
681
- let modelMessages = PacketWire.packetToWireMessages(requestPacket);
682
- // Curation pressure and provider generation are independent. The
683
- // provider owns its configured total output envelope.
684
- let response;
685
- let splitResponse;
686
- let railGrammar;
687
- let railResponseGrammar;
688
- let railEvidence;
689
- let emissionAttempts = 0;
690
- let providerCallInFlight = false;
691
- let modelCallSequence = 0;
692
- let currentEmissionAttempt = 0;
693
- let providerAttemptId = null;
694
- let providerModelCall = null;
695
- let providerAttemptAttributions = [];
696
- const providerSignal = this.#loopSignal(loopId) ?? signal;
697
- // {§client-metadata}
698
- const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
699
- const loopSeq = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
700
- const providerIdentity = await this.#resolveWorkerProviderIdentity(workerId);
701
- const { workerId: providerWorkerId, primaryWorkerId } = providerIdentity;
702
- const classifyProviderAttempt = async (id, attemptSplit, emissionAttempt, accepted) => {
703
- const result = await this.#db.engine_classify_turn_attempt_response.run({
704
- id,
705
- accepted: accepted ? 1 : 0,
706
- parse_errors: JSON.stringify(attemptSplit.parseErrors),
707
- });
708
- if (result.changes !== 1) {
709
- throw new Error(`emission attempt ${id} was not awaiting classification`);
710
- }
711
- emissionAttempts = emissionAttempt;
712
- };
713
- const recoverCapacityPacket = async () => {
714
- let baselineMessages = modelMessages;
715
- if (promptProjection === "automatic") {
716
- promptProjection = "withheld";
717
- const candidate = await buildPacket();
718
- const candidateMessages = PacketWire.packetToWireMessages(candidate);
719
- if (JSON.stringify(candidateMessages) !== JSON.stringify(baselineMessages)) {
720
- requestPacket = candidate;
721
- modelMessages = candidateMessages;
722
- return true;
723
- }
724
- baselineMessages = candidateMessages;
725
- }
726
- if (!boundaryRolledBack) {
727
- await this.#packets.rollbackNewestBoundary(loopId, turnId);
728
- boundaryRolledBack = true;
729
- const candidate = await buildPacket();
730
- const candidateMessages = PacketWire.packetToWireMessages(candidate);
731
- if (JSON.stringify(candidateMessages) !== JSON.stringify(baselineMessages)) {
732
- requestPacket = candidate;
733
- modelMessages = candidateMessages;
734
- return true;
735
- }
736
- }
737
- return false;
738
- };
739
- try {
740
- // {§turn-lifecycle}: bracket the complete provider-attempt window with liveness notices.
741
- if (!signal?.aborted)
742
- this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
743
- const railConstraint = await this.#grammarConstraint(provider);
744
- railGrammar = railConstraint?.transport;
745
- railResponseGrammar = railConstraint?.response;
746
- const attemptLimit = readEmissionAttempts();
747
- const strikeStreak = this.#strikes.streak(loopId);
748
- for (let attempt = 1; attempt <= attemptLimit;) {
749
- // Capacity recovery may rebuild and resend the request without
750
- // consuming a grammar-emission attempt. Every logical provider
751
- // call still receives its own durable sequence and accounting.
752
- currentEmissionAttempt = attempt;
753
- modelCallSequence++;
754
- const attributionContext = Object.freeze({
755
- workspaceId: String(workspaceId),
756
- workerId: providerWorkerId,
757
- primaryWorkerId,
758
- loop: loopSeq,
759
- turn: seq,
760
- attempt: modelCallSequence,
761
- });
762
- providerAttemptAttributions = await this.#attemptAttributions(provider, attributionContext);
763
- requestPacket = { ...requestPacket, attributions: providerAttemptAttributions };
764
- providerModelCall = await ModelCall.open(this.#db, {
765
- turnId,
766
- sequence: modelCallSequence,
767
- kind: "emission",
768
- attributions: providerAttemptAttributions,
769
- model: provider.model,
770
- });
771
- const attemptRow = await this.#db.engine_open_turn_attempt.get({
772
- model_call_id: providerModelCall.id,
773
- });
774
- if (attemptRow === undefined) {
775
- throw new Error(`Engine.runTurn: provider call ${modelCallSequence} did not open`);
776
- }
777
- providerAttemptId = attemptRow.id;
778
- providerCallInFlight = true;
779
- const currentModelCall = providerModelCall;
780
- let completedResponse;
781
- try {
782
- completedResponse = await observed(// {§observability-boundary}
783
- "provider.generate", { model: provider.model, attempt: modelCallSequence }, async (span) => {
784
- try {
785
- const generated = await provider.generate({
786
- messages: modelMessages,
787
- workerId: providerWorkerId,
788
- primaryWorkerId,
789
- signal: providerSignal,
790
- grammar: railGrammar,
791
- strikes: strikeStreak,
792
- attributions: providerAttemptAttributions.length > 0
793
- ? providerAttemptAttributions
794
- : undefined,
795
- client: client ?? undefined,
796
- workspaceId: String(workspaceId),
797
- loop: loopSeq,
798
- turn: seq,
799
- observeRequest: currentModelCall.observeRequest,
800
- callKind: "emission",
801
- }); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
802
- currentModelCall.assertAccounting(generated.accounting);
803
- providerCallInFlight = false;
804
- recordCounter(PROVIDER_CALLS, {
805
- model: provider.model,
806
- attempt: modelCallSequence,
807
- status: "resolved",
808
- });
809
- span.setAttribute("status", "resolved");
810
- return generated;
811
- }
812
- catch (error) {
813
- if (error instanceof ProviderError) {
814
- currentModelCall.assertAccounting(error.accounting);
815
- }
816
- throw error;
817
- }
818
- });
819
- }
820
- catch (error) {
821
- if (!(error instanceof ProviderError)
822
- || error.kind !== "capacity_exceeded"
823
- || providerSignal?.aborted === true
824
- || !await recoverCapacityPacket()) {
825
- throw error;
826
- }
827
- const failure = _a.#providerFailure(error, providerSignal);
828
- await currentModelCall.fail(failure, error.capacity ?? null);
829
- providerCallInFlight = false;
830
- await this.#problems.record({
831
- workerId,
832
- loopId,
833
- turnId,
834
- sequence: nextActionIndex++,
835
- origin: "plurnk",
836
- source: "provider",
837
- result: failure,
838
- });
839
- // Include the durable recovery signal in the replacement
840
- // request while preserving the selected recovery posture.
841
- requestPacket = await buildPacket();
842
- modelMessages = PacketWire.packetToWireMessages(requestPacket);
843
- continue;
844
- }
845
- response = completedResponse;
846
- await currentModelCall.observeResponse(completedResponse);
847
- railEvidence = railGrammar === undefined
848
- ? undefined
849
- : _a.#requireGrammarEvidence(completedResponse);
850
- splitResponse = this.#splitResponse(completedResponse);
851
- await classifyProviderAttempt(attemptRow.id, splitResponse, attempt, splitResponse.emissionValid);
852
- if (splitResponse.emissionValid)
853
- break;
854
- attempt++;
855
- }
856
- if (!signal?.aborted)
857
- this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_generated", level: "info", message: "parsing model response" });
858
- }
859
- catch (err) {
860
- // This handler owns only provider-call failures. Parser, cost, SQL,
861
- // and engine-contract failures retain their original source.
862
- if (err instanceof ModelCallPersistenceError || err instanceof ProviderAccountingIntegrityError)
863
- throw err;
864
- if (!providerCallInFlight)
865
- throw err;
866
- providerCallInFlight = false;
867
- if (providerAttemptId === null || providerModelCall === null) {
868
- throw new Error("provider call failed without durable model-call and attempt identities", { cause: err });
869
- }
870
- const failure = _a.#providerFailure(err, providerSignal);
871
- const capacityFailure = err instanceof ProviderError && err.kind === "capacity_exceeded";
872
- // {§provider-interrupted-attempt} — a provider-declared interruption
873
- // carries response evidence without becoming a completed exchange.
874
- // Persist it as an unaccepted attempt before settling the failure.
875
- if (err instanceof ProviderError && err.attempt !== undefined) {
876
- response = err.attempt;
877
- await providerModelCall.observeResponse(response, failure);
878
- splitResponse = this.#splitResponse(response);
879
- await classifyProviderAttempt(providerAttemptId, splitResponse, currentEmissionAttempt, false);
880
- }
881
- else {
882
- await providerModelCall.fail(failure, err instanceof ProviderError ? err.capacity ?? null : null);
883
- if (!capacityFailure)
884
- emissionAttempts = currentEmissionAttempt;
885
- }
886
- // {§turn-never-blank} — a ProviderError means no completed exchange exists.
887
- // Persist its exact RFC 9457 result before propagating it. Grammar evidence
888
- // and its engine-owned verdict exist only on completed responses
889
- // ({§rail-truth-engine-verdict}). Cancellation is lifecycle truth, not a
890
- // provider failure. Close the
891
- // attempted turn without inventing an assistant response, then let
892
- // runLoop/Daemon settle the exact 504/499 loop result.
893
- if (providerSignal?.aborted) {
894
- await this.#db.engine_close_turn.run({
895
- id: turnId,
896
- status: providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499,
897
- packet: StoredPacket.stringify(requestPacket),
898
- usage_curation_budget: this.#packets.curationBudgetFor(provider),
899
- finish_reason: splitResponse?.callMetadata.finishReason ?? null,
900
- model: splitResponse?.callMetadata.model ?? provider.model,
901
- meta: JSON.stringify(response?.meta ?? {}),
902
- });
903
- throw err;
904
- }
905
- const recorded = await this.#problems.record({
906
- workerId,
907
- loopId,
908
- turnId,
909
- sequence: nextActionIndex,
910
- origin: "plurnk",
911
- source: "provider",
912
- result: failure,
913
- });
914
- // The provider call was attempted, but no completed exchange exists.
915
- // Persist the exact request half and failure status; omitting assistant
916
- // is materially different from fabricating an empty model turn.
917
- await this.#db.engine_close_turn.run({
918
- id: turnId,
919
- status: recorded.result.status,
920
- packet: StoredPacket.stringify(requestPacket),
921
- usage_curation_budget: this.#packets.curationBudgetFor(provider),
922
- finish_reason: splitResponse?.callMetadata.finishReason ?? null,
923
- model: splitResponse?.callMetadata.model ?? provider.model,
924
- meta: JSON.stringify(response?.meta ?? {}),
925
- });
926
- if (capacityFailure) {
927
- return {
928
- turnId,
929
- status: recorded.result.status,
930
- outcomes: [],
931
- fingerprint: "",
932
- capacityHardStop: true,
933
- capacityFailure: recorded.result,
934
- steerStruck: false,
935
- emissionAttempts,
936
- emissionExhausted: false,
937
- };
938
- }
939
- throw new OperationFailureError(recorded.result, { cause: err });
940
- }
941
- if (response === undefined || splitResponse === undefined || providerModelCall === null) {
942
- throw new Error("provider attempt loop completed without a response");
943
- }
944
- const emissionModelCallId = providerModelCall.id;
945
- if (!splitResponse.emissionValid) {
946
- // {§invalid-emission-attempts} The first consecutive exhaustion
947
- // publishes only the raw final response and generic recovery fact.
948
- let rejectedModelEntryId;
949
- if (allowInvalidEmissionRecovery) {
950
- rejectedModelEntryId = await this.#dispatcher.writeModelEntry({
951
- verbatim: splitResponse.packetAssistant.content,
952
- workerId,
953
- loopId,
954
- turnId,
955
- sequence: nextActionIndex,
956
- folded: true,
957
- modelCallId: emissionModelCallId,
958
- admission: "rejected",
959
- });
960
- this.#notices.push(workspaceId, loopId, {
961
- source: "engine:grammar",
962
- kind: "invalid_emission",
963
- level: "error",
964
- message: INVALID_EMISSION_RECOVERY_MESSAGE,
965
- });
966
- }
967
- await this.#db.engine_close_turn.run({
968
- id: turnId,
969
- status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
970
- packet: StoredPacket.stringify(requestPacket),
971
- usage_curation_budget: this.#packets.curationBudgetFor(provider),
972
- finish_reason: splitResponse.callMetadata.finishReason,
973
- model: splitResponse.callMetadata.model,
974
- meta: JSON.stringify(response.meta ?? {}),
975
- });
976
- return {
977
- turnId,
978
- status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
979
- outcomes: [],
980
- fingerprint: "",
981
- capacityHardStop: false,
982
- steerStruck: false,
983
- emissionAttempts,
984
- emissionExhausted: true,
985
- ...(rejectedModelEntryId === undefined ? {} : { rejectedModelEntryId }),
986
- };
987
- }
988
- // {§packet-stored-shape} — admitted emission data extends the packet;
989
- // provider-call metadata remains on the Turn row.
990
- const { packetAssistant, callMetadata, parseNotices, recoverableParseErrors, } = splitResponse; // raw assistant content is opaque — split, never interpreted — {§provider-guarantees-assistantraw-opaque}
991
- for (const notice of parseNotices) {
992
- this.#notices.push(workspaceId, loopId, notice);
993
- }
994
- // Non-fatal provider transport notices on an accepted turn. Forward each
995
- // Notice with a content-offset `line:col`;
996
- // the model resolves it against its own emission — READ the folded model-emission row at the
997
- // cited lines ({§model-entry}) — not an embedded snippet that would duplicate the emission.
998
- for (const notice of response.notices ?? []) {
999
- const located = typeof notice.position === "number"
1000
- ? this.#offsetToLineColumn(packetAssistant.content, notice.position)
1001
- : null;
1002
- this.#notices.push(workspaceId, loopId, {
1003
- source: notice.source,
1004
- kind: notice.kind,
1005
- message: notice.message ?? "",
1006
- level: notice.level,
1007
- ...(located !== null
1008
- ? { position: { type: "content-offset", line: located.line, column: located.column } }
1009
- : {}),
1010
- });
1011
- }
1012
- // Grade configured local evidence independently. Endpoint-owned
1013
- // constraints remain provider observations. {§rail-truth-engine-verdict}
1014
- let railKeys;
1015
- if (railGrammar !== undefined) {
1016
- if (railEvidence === undefined)
1017
- throw new Error("configured GBNF response has no final grammar evidence");
1018
- if (railResponseGrammar === undefined)
1019
- throw new Error("configured GBNF has no response grammar");
1020
- let verdict = null;
1021
- try {
1022
- verdict = validateGbnf(railResponseGrammar, railEvidence.input);
1023
- }
1024
- catch (cause) {
1025
- _a.#warnRailVerdictGapOnce(cause.message);
1026
- }
1027
- railKeys = {
1028
- railsAttached: railEvidence.transported ? "client" : "withheld",
1029
- railsVerdict: verdict?.status ?? "unverifiable",
1030
- };
1031
- if (verdict !== null && verdict.status !== "accept") {
1032
- const contentPosition = verdict.pos >= railEvidence.contentStart
1033
- ? verdict.pos - railEvidence.contentStart
1034
- : null;
1035
- const located = contentPosition === null
1036
- ? null
1037
- : this.#offsetToLineColumn(packetAssistant.content, contentPosition);
1038
- this.#notices.push(workspaceId, loopId, {
1039
- source: "engine:rails",
1040
- kind: "grammar_unenforced",
1041
- message: verdict.status === "reject"
1042
- ? `emission rejects the grammar at raw code point ${verdict.pos}`
1043
- : `emission is an incomplete grammar sentence (ends at raw code point ${verdict.pos})`,
1044
- level: "warn",
1045
- ...(located === null
1046
- ? {}
1047
- : { position: { type: "content-offset", line: located.line, column: located.column } }),
1048
- });
1049
- }
1050
- }
1051
- const opsCount = packetAssistant.ops.length;
1052
- const finalOp = packetAssistant.ops.at(-1);
389
+ // {§turn-ops-admission-path} — source acquisition ends before this seam.
390
+ // Every admitted producer program is scheduled, dispatched, recorded, and
391
+ // completed here; inference is only the model-specific way one program is
392
+ // acquired and supplied with BARE capability.
393
+ async #executeAdmittedTurn({ statements, source, sourceFolded, sourceModelCallId = null, sourceReasoningItems, origin, workspaceId, workerId, loopId, turnId, fromSequence, maxCommands = Number.POSITIVE_INFINITY, enforceIdle = false, failOnOperationError = false, recoverableParseErrors = [], bare, signal, onDispatch, }) {
394
+ const plan = statements[0];
395
+ const finalOp = statements.at(-1);
1053
396
  if (finalOp?.op !== "SEND") {
1054
- // Text emissions cannot reach this point without a disposition;
1055
- // this fail-hard guard also keeps Mock's trusted pre-parsed seam
1056
- // from creating runtime states that production admission forbids.
1057
- throw new Error("an admitted emission must end in a disposition SEND");
397
+ throw new Error("an admitted operation batch must end in a disposition SEND");
398
+ }
399
+ if (source !== null && plan?.op !== "PLAN") {
400
+ throw new Error("an admitted source-backed turnOps program must begin with PLAN");
1058
401
  }
1059
402
  const dispositionSignal = finalOp.signal;
1060
403
  if (typeof dispositionSignal !== "number" || !TERMINAL_SEND_SIGNALS.has(dispositionSignal)) {
1061
- throw new Error("an admitted emission must end in a disposition SEND");
404
+ throw new Error("an admitted turnOps program must end in a terminal disposition SEND");
1062
405
  }
1063
406
  const sendOp = finalOp;
1064
- // {§send} the terminal contract — engine error states verify a terminal claim against loop
1065
- // state, never trusting the model's code. They strike via turn.steerStruck
1066
- // ({§engine-rails}): the loop continues, the model sees the steering hint not the strike
1067
- // count, and a non-resolver spins out to the engine's 500.
407
+ let turnStatus = dispositionSignal;
1068
408
  let steerStruck = false;
1069
- // Engine errors raised this turn, minted as op='error' log rows after dispatch (they share the
1070
- // post-dispatch sequence counter). {§operation-result-uniform-error-channel}
1071
409
  const pendingEngineErrors = [];
1072
- // Terminal adjudication moved to the DISPATCHER ({§send-premature-terminate}, the unified
1073
- // pending set): the terminal SEND is judged AT ITS OWN DISPATCH — after the emission's
1074
- // earlier ops executed — so a same-turn KILL+[200] repairs in one turn and a same-turn
1075
- // WORK+[200] is caught. A refused final disposition (409) strikes via
1076
- // the dispatch-loop check below.
1077
- let turnStatus = dispositionSignal;
1078
- // Idle turn: an implicit-continue (102) that did no WORK — its ops are only PLAN/SEND, no mid op.
1079
- // The model continued with nothing to do.
1080
- const midOpsCount = packetAssistant.ops.filter((op) => op.op !== "PLAN" && op.op !== "SEND").length
410
+ const middleCount = statements.filter((statement) => statement.op !== "PLAN" && statement.op !== "SEND").length
1081
411
  + recoverableParseErrors.length;
1082
- if (!steerStruck && turnStatus === TURN_STATUS_IMPLICIT_CONTINUE && midOpsCount === 0) {
412
+ if (enforceIdle && turnStatus === TURN_STATUS_IMPLICIT_CONTINUE && middleCount === 0) {
1083
413
  steerStruck = true;
1084
414
  pendingEngineErrors.push("idle_turn");
1085
415
  }
1086
- // Close the turn with the final packet, status, and usage stats.
1087
- const packet = StoredPacket.admit(requestPacket, packetAssistant, response.assistantRaw);
1088
- await this.#db.engine_close_turn.run({
1089
- id: turnId,
1090
- status: turnStatus,
1091
- packet: StoredPacket.stringify(packet),
1092
- usage_curation_budget: this.#packets.curationBudgetFor(provider), // {§tokenomics-client-gauge}
1093
- finish_reason: callMetadata.finishReason,
1094
- model: callMetadata.model,
1095
- // Opaque provider metadata plus engine-authored rail keys.
1096
- // {§meta-passthrough}, {§rail-truth-engine-verdict}
1097
- meta: JSON.stringify({ ...(response.meta ?? {}), ...(railKeys ?? {}) }),
1098
- });
1099
- // Dispatch model ops starting at nextActionIndex (continues the
1100
- // turn's running counter after any pre-model writes).
1101
- //
1102
- // Max-commands ceiling: OFF by default (unlimited) — every generated op dispatches.
1103
- // A degenerate op-loop is a sampler failure guarded at generation, not by dropping
1104
- // already-generated work post-hoc. The ceiling is an OPT-IN operator/client bound:
1105
- // when set, overflow ops drop without per-op log entries (no forensics flood) and the
1106
- // model gets one notices signal next packet.
1107
- // {§operator-config-workspace-max-commands} — workspace maxCommands min()s the env ceiling.
1108
- const maxCommands = Math.min(readMaxCommands(), (await WorkspaceSettings.read(this.#db, workspaceId)).maxCommands ?? Number.POSITIVE_INFINITY);
1109
- // PLAN (intended goals) and the final disposition SEND are not actions —
1110
- // they always dispatch and never count against the cap. maxCommands
1111
- // bounds real actions only; maxCommands:0 still admits a plan and a disposition
1112
- // (the PLAN/SEND ops, zero actions), which is its only coherent meaning.
1113
416
  let realCommands = 0;
1114
- const admittedOps = packetAssistant.ops.filter((op) => {
1115
- return op.op === "PLAN"
1116
- || op === sendOp
1117
- || realCommands++ < maxCommands;
1118
- });
1119
- const opsToDispatch = scheduleTurnOps(admittedOps);
1120
- await this.#dispatcher.prepareEditBatches(opsToDispatch.filter((statement) => statement.op === "EDIT"), {
1121
- workspaceId, workerId, loopId, turnId,
1122
- origin, onDispatch,
1123
- });
1124
- const droppedCount = opsCount - opsToDispatch.length;
1125
- const bareStatements = opsToDispatch.filter((statement) => statement.op === "BARE");
417
+ const admitted = statements.filter((statement) => statement.op === "PLAN"
418
+ || statement === sendOp
419
+ || realCommands++ < maxCommands);
420
+ const scheduled = scheduleTurnOps(admitted.flatMap(expandSafeUriTargetGroup));
421
+ await this.#dispatcher.prepareEditBatches(scheduled.filter((statement) => statement.op === "EDIT"), { workspaceId, workerId, loopId, turnId, origin, onDispatch });
422
+ const droppedCount = statements.length - admitted.length;
423
+ const bareStatements = scheduled.filter((statement) => statement.op === "BARE");
1126
424
  let bareResults = null;
1127
425
  const outcomes = [];
1128
- // Running counter — a multi-file READ writes N rows from one statement (rowsWritten),
1129
- // so the next op's sequence picks up after them. Collapses to nextActionIndex+i when
1130
- // every op writes one row (the common case).
1131
- let rowSeq = nextActionIndex;
426
+ let rowSequence = fromSequence;
1132
427
  let parseErrorsRecorded = false;
1133
428
  const recordRecoverableParseErrors = async () => {
1134
429
  if (parseErrorsRecorded)
@@ -1139,8 +434,8 @@ class TurnRunner {
1139
434
  workerId,
1140
435
  loopId,
1141
436
  turnId,
1142
- sequence: rowSeq++,
1143
- origin: "model",
437
+ sequence: rowSequence++,
438
+ origin,
1144
439
  source: "grammar",
1145
440
  result: Results.failure("grammar:parser", "invalid-operation-syntax", 400, error.message, {}, {
1146
441
  line: error.line,
@@ -1155,39 +450,35 @@ class TurnRunner {
1155
450
  onDispatch?.(recorded.id);
1156
451
  }
1157
452
  };
1158
- for (const statement of opsToDispatch) {
1159
- if (statement.op === "SEND"
1160
- && typeof statement.signal === "number"
1161
- && TERMINAL_SEND_SIGNALS.has(statement.signal)) {
453
+ for (const statement of scheduled) {
454
+ if (statement === sendOp) {
1162
455
  await recordRecoverableParseErrors();
1163
- // {§worker-optimistic-settlement} — SEND judges the refreshed
1164
- // lifecycle after this turn's own fast streams receive one
1165
- // bounded opportunity to conclude. This is not a sleep and
1166
- // does not delay a later turn for older monitored streams.
1167
456
  const execHandler = this.#schemes.get("exec");
1168
- await execHandler?.settleTurnSpawns?.(workerId, turnId, readOptimisticSettlementMs(), providerSignal);
457
+ await execHandler?.settleTurnSpawns?.(workerId, turnId, readOptimisticSettlementMs(), signal);
1169
458
  }
1170
- const result = await observed(// {§observability-boundary}
1171
- "op.dispatch", { op: statement.op }, async (span) => {
459
+ const result = await observed("op.dispatch", { op: statement.op }, async (span) => {
1172
460
  let dispatchResult;
1173
461
  if (statement.op === "BARE") {
462
+ if (bare === undefined) {
463
+ throw new Error(`${origin} turnOps cannot execute BARE without provider acquisition context`);
464
+ }
1174
465
  if (bareResults === null) {
1175
466
  const batch = await this.#runBareBatch({
1176
467
  statements: bareStatements,
1177
- provider: childProvider,
468
+ provider: bare.provider,
1178
469
  turnId,
1179
- modelCallSequenceStart: modelCallSequence + 1,
470
+ modelCallSequenceStart: bare.modelCallSequenceStart,
1180
471
  workspaceId,
1181
472
  workerId,
1182
- primaryWorkerId,
1183
- loopSequence: loopSeq,
1184
- turnSequence: seq,
1185
- signal: providerSignal,
473
+ primaryWorkerId: bare.primaryWorkerId,
474
+ loopSequence: bare.loopSequence,
475
+ turnSequence: bare.turnSequence,
476
+ signal: bare.signal,
1186
477
  });
1187
478
  bareResults = new Map(batch.map((item) => [item.statement, item]));
1188
479
  }
1189
- const bare = bareResults.get(statement);
1190
- if (bare === undefined) {
480
+ const bareResult = bareResults.get(statement);
481
+ if (bareResult === undefined) {
1191
482
  throw new Error("BARE statement reached dispatch without its batch result");
1192
483
  }
1193
484
  dispatchResult = await this.#dispatcher.recordBareResult({
@@ -1196,16 +487,21 @@ class TurnRunner {
1196
487
  workerId,
1197
488
  loopId,
1198
489
  turnId,
1199
- sequence: rowSeq,
490
+ sequence: rowSequence,
1200
491
  origin,
1201
492
  onDispatch,
1202
- }, bare.result, bare.modelCallId);
493
+ }, bareResult.result, bareResult.modelCallId);
1203
494
  }
1204
495
  else {
1205
496
  dispatchResult = await this.#dispatcher.dispatch({
1206
- statement, workspaceId, workerId, loopId, turnId,
1207
- sequence: rowSeq,
1208
- origin, onDispatch,
497
+ statement,
498
+ workspaceId,
499
+ workerId,
500
+ loopId,
501
+ turnId,
502
+ sequence: rowSequence,
503
+ origin,
504
+ onDispatch,
1209
505
  });
1210
506
  }
1211
507
  span.setAttribute("status", dispatchResult.status);
@@ -1213,6 +509,8 @@ class TurnRunner {
1213
509
  return dispatchResult;
1214
510
  });
1215
511
  outcomes.push({ op: statement.op, status: result.status });
512
+ if (failOnOperationError && result.status >= 400)
513
+ throw new OperationFailureError(result);
1216
514
  for (const normalization of result.scopeNormalizations ?? []) {
1217
515
  this.#notices.push(workspaceId, loopId, {
1218
516
  source: "engine:slicer",
@@ -1221,39 +519,20 @@ class TurnRunner {
1221
519
  message: `Scope <${normalization.requested.join(",")}> was normalized to <${normalization.canonical.join(",")}>.`,
1222
520
  });
1223
521
  }
1224
- // {§engine-rails} — a refused final disposition leaves both loop
1225
- // and turn continuing, and its 409 steering ruling strikes once.
1226
522
  if (statement === sendOp && result.status === 409) {
1227
523
  steerStruck = true;
1228
524
  turnStatus = TURN_STATUS_IMPLICIT_CONTINUE;
1229
- await this.#db.engine_reconcile_turn_status.run({ id: turnId, status: turnStatus });
1230
525
  }
1231
- // {§send-300-choices}: a question resolves through the proposal system; whatever the
1232
- // resolution (answer/reject/timeout), the LOOP continues to the turn where the model
1233
- // reads it; the turn record is a continue, never a 300 terminal.
1234
- if (statement === sendOp && sendOp.signal === 300 && result.status !== 409) {
1235
- turnStatus = TURN_STATUS_IMPLICIT_CONTINUE;
1236
- await this.#db.engine_reconcile_turn_status.run({ id: turnId, status: turnStatus });
1237
- }
1238
- // A broadcast [202] is a conditional wait, not an unconditional turn status:
1239
- // live work parks at 202; completed-but-unobserved work continues at 102; an
1240
- // empty join completes at 200. Persist and return the dispatcher's actual ruling.
1241
526
  if (statement === sendOp
1242
527
  && result.status !== 409
1243
528
  && sendOp.target === null
1244
529
  && sendOp.signal === 202
1245
530
  && result.status !== 202) {
1246
531
  turnStatus = result.status;
1247
- await this.#db.engine_reconcile_turn_status.run({ id: turnId, status: turnStatus });
1248
532
  }
1249
- rowSeq += result.rowsWritten ?? 1;
533
+ rowSequence += result.rowsWritten ?? 1;
1250
534
  }
1251
535
  await recordRecoverableParseErrors();
1252
- // Engine rail failures mint as op='error' log rows at the turn's next
1253
- // free sequence. Bounded syntax failures were recorded in their
1254
- // authored turn before its terminal disposition.
1255
- let errSeq = rowSeq;
1256
- // max_commands_exceeded IS model-facing: dropped ops the model emitted that didn't run.
1257
536
  if (droppedCount > 0)
1258
537
  pendingEngineErrors.push("max_commands_exceeded");
1259
538
  for (const kind of pendingEngineErrors) {
@@ -1275,47 +554,887 @@ class TurnRunner {
1275
554
  workerId,
1276
555
  loopId,
1277
556
  turnId,
1278
- sequence: errSeq++,
1279
- origin: "plurnk",
557
+ sequence: rowSequence++,
558
+ origin: "_plurnk",
1280
559
  source: "rail",
1281
560
  result: Results.failure("engine:rail", problem.code, problem.status, problem.detail, {}, extensions),
1282
561
  });
1283
562
  }
1284
- // {§log-row-self-explains} — model-operation failures remain on their
1285
- // own rows; genuine engine faults fail hard and mint no substitute row.
1286
- // {§model-entry} — mirror this turn's verbatim emission back as a `model` row, so the NEXT
1287
- // packet shows the model exactly what it last produced. ALWAYS born FOLDED — the old
1288
- // born-OPEN-on-error auto-trigger was conditional helpfulness that bred its own hazards
1289
- // (a 24k-char ramble mirrored open re-injects itself into the next packet: cost,
1290
- // contamination, pressure feedback).
1291
- // {§encrypted-reasoning-carrier} — core relays every provider-normalized
1292
- // item unchanged.
1293
- const reasoningItems = response.assistant.reasoningEncrypted?.length
1294
- ? response.assistant.reasoningEncrypted
1295
- : undefined;
1296
- if (packetAssistant.content.trim().length > 0 || reasoningItems !== undefined) {
1297
- await this.#dispatcher.writeModelEntry({
1298
- verbatim: packetAssistant.content,
563
+ if (source !== null) {
564
+ await this.#dispatcher.writeTurnOps({
565
+ verbatim: source,
1299
566
  workerId,
1300
567
  loopId,
1301
568
  turnId,
1302
- sequence: errSeq++,
1303
- folded: true,
1304
- modelCallId: emissionModelCallId,
1305
- ...(reasoningItems !== undefined ? { reasoningItems } : {}),
569
+ sequence: rowSequence,
570
+ origin,
571
+ folded: sourceFolded,
572
+ modelCallId: sourceModelCallId,
573
+ ...(sourceReasoningItems !== undefined ? { reasoningItems: sourceReasoningItems } : {}),
1306
574
  });
1307
575
  }
576
+ await Turn.complete(this.#db, turnId, turnStatus);
1308
577
  return {
1309
- turnId,
1310
578
  status: turnStatus,
1311
579
  outcomes,
1312
- fingerprint: StrikeRail.fingerprintTurn(packetAssistant.ops),
1313
- capacityHardStop: false,
580
+ fingerprint: StrikeRail.fingerprintTurn(statements),
1314
581
  steerStruck,
1315
- emissionAttempts,
1316
- emissionExhausted: false,
1317
582
  };
1318
583
  }
584
+ async runTurn({ provider, childProvider = provider, messages, requirements = "", workspaceId, workerId, loopId, signal, onDispatch, turnNumber = 1, maxTurns = 50, invalidEmissionRecoveryEntryId, }) {
585
+ const allowInvalidEmissionRecovery = invalidEmissionRecoveryEntryId === null;
586
+ const transientOpenLogEntryId = typeof invalidEmissionRecoveryEntryId === "number"
587
+ ? invalidEmissionRecoveryEntryId
588
+ : null;
589
+ // === Producer-neutral turn container ===
590
+ //
591
+ // Every producer opens the same durable turn and completes it only
592
+ // after its ordered operations settle. Model packets and provider
593
+ // metadata are optional inference evidence, not turn identity.
594
+ const initialSequence = await this.#db.engine_next_turn_sequence.get({ loop_id: loopId });
595
+ if (initialSequence === undefined)
596
+ throw new Error("Engine.runTurn: next turn sequence is unavailable");
597
+ // loops.sequence is the loop's ordinal within the worker. Turn-0 foists that belong to the
598
+ // WORKER (manifest preview, AGENTS, operator docs) gate on the worker's first loop, not every loop's
599
+ // first turn ({§actor-boundary-catalog-preview}); per-loop foists such as
600
+ // {§prompt-entry} still fires once per loop. Durable publication state,
601
+ // rather than the model-turn ordinal, prevents an overflow diversion
602
+ // from replaying the prompt and its automatic path READs.
603
+ const loopRow = await this.#db.engine_get_loop_prompt.get({ loop_id: loopId });
604
+ const workerFirstLoop = (loopRow?.sequence ?? 0) === 1;
605
+ const createdTurnIds = [];
606
+ try {
607
+ // {§worker-initialization-entry} — turn zero is an ordinary `_plurnk`
608
+ // operation turn. Its model-facing zero is a phase label; durable turn
609
+ // coordinates remain one-based.
610
+ const initializationTurn = initialSequence.next === 1 && workerFirstLoop
611
+ ? await Turn.open(this.#db, { loopId, producer: "_plurnk", kind: "initialization" })
612
+ : null;
613
+ if (initializationTurn !== null)
614
+ createdTurnIds.push(initializationTurn.id);
615
+ let modelTurn = initializationTurn === null
616
+ ? await Turn.open(this.#db, { loopId, producer: "model", kind: "inference" })
617
+ : null;
618
+ if (modelTurn !== null)
619
+ createdTurnIds.push(modelTurn.id);
620
+ // {§env-delta-log-pull} — establish a fresh worker's observation
621
+ // baseline immediately after its first turn opens. A fork already has
622
+ // its parent's cursor, so the NULL-guarded statement leaves it intact.
623
+ // Events committed after this statement belong to this or a later
624
+ // closed pull window; pre-existing state is read through the ordinary
625
+ // shared-world projections in this first packet.
626
+ await this.#db.engine_initialize_ambient_cursor.get({ workspace_id: workspaceId, worker_id: workerId });
627
+ // Threaded per turn, never engine state, so concurrent loops on
628
+ // different providers each read their own honest tokenizer values.
629
+ const systemContext = (contextTurnId) => ({
630
+ db: this.#db, workspaceId, workerId, loopId, turnId: contextTurnId,
631
+ writer: "_plurnk",
632
+ signal: this.#loopSignal(loopId),
633
+ streamEventNotify: this.#streamEventNotify,
634
+ wakeWorkerNotify: this.#wakeWorkerNotify,
635
+ weigh: this.#weighContent,
636
+ mimetypes: this.#mimetypes,
637
+ defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s, workspaceId),
638
+ pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
639
+ requestInteraction: (request) => this.#interactions.request(request, { workspaceId, workerId, loopId, turnId: contextTurnId }, this.#loopSignal(loopId)),
640
+ });
641
+ let systemCtx = systemContext(initializationTurn?.id ?? modelTurn.id);
642
+ const initializationStatements = [];
643
+ // {§worker-initialization-entry} — the worker's first turn is the worked
644
+ // example itself: an ordinary PLAN, the actual orienting operations,
645
+ // and an ordinary terminal SEND.
646
+ if (initializationTurn !== null) {
647
+ const plan = {
648
+ op: "PLAN", delimiter: "", annotation: null,
649
+ signal: null, target: null, lineMarker: null,
650
+ body: "* Discover the tooling available and survey the workspace file root.",
651
+ position: UNKNOWN_POSITION,
652
+ };
653
+ initializationStatements.push(plan);
654
+ // {§turn0-agents-stunt} — the project AGENTS.md (materialized by LoopDocs as
655
+ // worker://plurnk/agents.md) gets one foisted READ on the worker's first
656
+ // loop, so local repo guidance is visible turn-0 content. Global policy
657
+ // stays in the system prompt; nothing else is force-read.
658
+ if (workerFirstLoop) {
659
+ const kernelId = await Owner.kernelId(this.#db, workspaceId);
660
+ const agentsEntry = await this.#db.crud_find_workspace_entry.get({
661
+ workspace_id: workspaceId,
662
+ owner_id: kernelId,
663
+ scheme: "worker",
664
+ pathname: "/agents.md",
665
+ });
666
+ if (agentsEntry !== undefined) {
667
+ const agentsTarget = {
668
+ kind: "url", raw: "worker://plurnk/agents.md", scheme: "worker",
669
+ username: null, password: null, hostname: "plurnk", port: null,
670
+ pathname: "/agents.md", query: null, fragment: null,
671
+ };
672
+ const agentsRead = {
673
+ op: "READ", delimiter: "", annotation: null, signal: ["+_plurnk", "+init", "+policy"], target: agentsTarget,
674
+ lineMarker: null, body: null, position: UNKNOWN_POSITION,
675
+ };
676
+ initializationStatements.push(agentsRead);
677
+ }
678
+ }
679
+ }
680
+ // The persistent search-index pass (_search-index.maintain) attaches
681
+ // every readable entry/log projection to complete graph/FTS/vector derivations.
682
+ // NOT an action: no log entry, no sequence slot,
683
+ // not dispatched. There is no materialized manifest entry — the catalog
684
+ // is served on demand by FIND: recursive when asked, shallow-mapped below.
685
+ // {§semantic-embed-dedup} — one pass-wide semantic plan binds every
686
+ // chunk counter to the derivation identity it produces.
687
+ // SPEC {§membership} D4/D5 — git-ls-files workspace membership, resolved at
688
+ // prompt-composition (EMI is eager + exhaustive — git is the only bound). When the
689
+ // workspace's project_root is a git working tree, tracked files are
690
+ // members without a client `add`; active members are materialized
691
+ // (disk → body channel) so they appear in the catalog. No-ops
692
+ // on headless / non-git workspaces. Runs BEFORE the derivation pump so
693
+ // this turn's packet reflects them.
694
+ // Workspace creation starts this eagerly. Joining here is the correctness
695
+ // boundary: the model never runs against partial graph/vector coverage.
696
+ await this.#warmWorkspace(systemCtx, false);
697
+ // The warm materialized membership before deriving. This second pass is the
698
+ // ordinary cheap change detector and captures any drift that landed meanwhile.
699
+ const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
700
+ // {§packet-git-status} — one post-reconciliation snapshot supplies both
701
+ // the compact packet summary and each causal file event's exact XY state.
702
+ const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopSignal(loopId));
703
+ await this.#logFsFictions(workspaceId, fsDivergences, gitStatus);
704
+ // The refresh above may have changed bodies (including model/client edits
705
+ // since the startup warm). Re-derive to completion before packet/model
706
+ // construction. Membership is already current, so this pass does not
707
+ // consume the filesystem divergences a second time.
708
+ await this.#warmWorkspace(systemCtx, true, false);
709
+ // Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, {§actor-boundary-catalog-preview}):
710
+ // Six bodyless FIND surveys in the worker's packetless initialization turn establish the skills tree
711
+ // (authored and Plurnk-generated families), attached tools tree, project, commons, and private
712
+ // surfaces in that order.
713
+ // Their `init` classification lets the model curate this opening survey as one log set.
714
+ if (initializationTurn !== null) {
715
+ // {§operator-config-workspace-files-items} — workspace filesItems replaces the env default.
716
+ const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
717
+ const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
718
+ if (filesItems !== null && workerFirstLoop) { // {§actor-boundary-catalog-preview} — once per worker
719
+ const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
720
+ const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
721
+ const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
722
+ // {§tools-resource-materialization} — PLURNK_MCP_EXPANDED servers
723
+ // contribute one complete tool-tree survey each, directly after the
724
+ // family survey, so turn 0 names every tool they expose in order.
725
+ const registry = this.#executors();
726
+ const toolExpansions = [];
727
+ for (const tag of registry?.availableRuntimes(workspaceId) ?? []) {
728
+ const entry = registry?.entry(tag, workspaceId);
729
+ if (entry?.resourcesPath !== "/tools" || entry.expandTools !== true)
730
+ continue;
731
+ toolExpansions.push({
732
+ statement: {
733
+ op: "FIND", delimiter: "", annotation: `enabled tools: ${tag}`, signal: ["+_plurnk", "+init", "+tools"],
734
+ target: { kind: "url", raw: `worker://plurnk/tools/${tag}/**`, scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: `/tools/${tag}/**`, query: null, fragment: null },
735
+ body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
736
+ },
737
+ });
738
+ }
739
+ const surveys = [
740
+ {
741
+ statement: {
742
+ op: "FIND", delimiter: "", annotation: null, signal: ["+_plurnk", "+init", "+skills"],
743
+ target: { kind: "url", raw: "worker://plurnk/skills/*.md", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/skills/*.md", query: null, fragment: null },
744
+ body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
745
+ },
746
+ },
747
+ {
748
+ statement: {
749
+ op: "FIND", delimiter: "", annotation: null, signal: ["+_plurnk", "+init", "+skills"],
750
+ target: { kind: "url", raw: "worker://plurnk/skills/plurnk/*.md", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/skills/plurnk/*.md", query: null, fragment: null },
751
+ body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
752
+ },
753
+ },
754
+ {
755
+ // {§tools-resource-materialization} — enabled tool families
756
+ // (MCP servers) survey at family level; each row's summary is
757
+ // the server one-liner or its flagship invocation form, so the
758
+ // discovery row itself orients. Servers named in
759
+ // PLURNK_MCP_EXPANDED add a second survey of their complete
760
+ // tool tree.
761
+ statement: {
762
+ op: "FIND", delimiter: "", annotation: "enabled tools", signal: ["+_plurnk", "+init", "+tools"],
763
+ target: { kind: "url", raw: "worker://plurnk/tools/*.md", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/tools/*.md", query: null, fragment: null },
764
+ body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
765
+ },
766
+ },
767
+ ...toolExpansions,
768
+ {
769
+ statement: {
770
+ op: "FIND", delimiter: "", annotation: "workspace files", signal: ["+_plurnk", "+init"],
771
+ target: { kind: "local", raw: "*" },
772
+ body: null,
773
+ lineMarker: fileCap === null ? null : { marks: [1, fileCap] },
774
+ position: UNKNOWN_POSITION,
775
+ },
776
+ },
777
+ {
778
+ statement: {
779
+ op: "FIND", delimiter: "", annotation: "workspace entries", signal: ["+_plurnk", "+init"],
780
+ target: { kind: "url", raw: "worker:///*", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: "/*", query: null, fragment: null },
781
+ body: null, lineMarker: null, position: UNKNOWN_POSITION,
782
+ },
783
+ },
784
+ {
785
+ statement: {
786
+ op: "FIND", delimiter: "", annotation: "worker entries", signal: ["+_plurnk", "+init"],
787
+ target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
788
+ body: null, lineMarker: null, position: UNKNOWN_POSITION,
789
+ },
790
+ },
791
+ ];
792
+ initializationStatements.push(...surveys.map(({ statement }) => statement));
793
+ }
794
+ const send = {
795
+ op: "SEND", delimiter: "", annotation: null,
796
+ signal: 102, target: null, lineMarker: null,
797
+ body: { raw: "Next: Address the prompt.", json: null },
798
+ position: UNKNOWN_POSITION,
799
+ };
800
+ initializationStatements.push(send);
801
+ const source = TurnOps.renderInternal(initializationStatements);
802
+ const admitted = TurnOps.parseInternal(source);
803
+ const result = await this.#executeAdmittedTurn({
804
+ statements: admitted,
805
+ source,
806
+ sourceFolded: false,
807
+ origin: "_plurnk",
808
+ workspaceId,
809
+ workerId,
810
+ loopId,
811
+ turnId: initializationTurn.id,
812
+ fromSequence: 1,
813
+ failOnOperationError: true,
814
+ signal: this.#loopSignal(loopId),
815
+ onDispatch,
816
+ });
817
+ if (result.status !== TURN_STATUS_IMPLICIT_CONTINUE) {
818
+ throw new Error(`initialization SEND returned ${result.status}; expected ${TURN_STATUS_IMPLICIT_CONTINUE}`);
819
+ }
820
+ }
821
+ // Initialization is a complete preceding turn, not a set of rows
822
+ // interleaved with the model boundary. The engine may now acquire the
823
+ // inference turn inside the same warmed workspace cycle.
824
+ if (modelTurn === null) {
825
+ modelTurn = await Turn.open(this.#db, { loopId, producer: "model", kind: "inference" });
826
+ createdTurnIds.push(modelTurn.id);
827
+ }
828
+ const seq = modelTurn.sequence;
829
+ const turnId = modelTurn.id;
830
+ systemCtx = systemContext(turnId);
831
+ // Pre-model writes. Each prompt the model has not seen yet becomes a
832
+ // `prompt` operation row whose target is its durable prompt:// entry.
833
+ // Model operations continue the same turn sequence after these rows.
834
+ let nextActionIndex = 1;
835
+ const turnOpenPaths = [];
836
+ if (turnNumber === 1 && loopRow?.prompt_published === 0) {
837
+ const promptRow = loopRow; // {§prompt-entry} — one durable publication per loop
838
+ if (promptRow !== undefined && typeof promptRow.prompt === "string" && promptRow.prompt.length > 0) {
839
+ const openPaths = assertOpenPaths(JSON.parse(promptRow.open_paths), `Loop ${loopId} open_paths`);
840
+ const promptLoopSeq = promptRow.sequence;
841
+ const promptPath = promptTarget(promptLoopSeq, 1);
842
+ const entry = {
843
+ channels: { body: { content: promptRow.prompt, mimetype: "text/markdown" } },
844
+ attributes: { openPaths },
845
+ };
846
+ await EntryCrud.writeEntry(promptPath.pathname, entry, systemCtx, "prompt", workerId);
847
+ turnOpenPaths.push(...openPaths);
848
+ const promptLogId = await this.#writePromptLog({
849
+ workerId,
850
+ loopId,
851
+ turnId,
852
+ sequence: nextActionIndex++,
853
+ target: promptPath,
854
+ content: promptRow.prompt,
855
+ });
856
+ onDispatch?.(promptLogId);
857
+ }
858
+ }
859
+ // {§prompt-loop-containment}: the loop contains every prompt that arrived
860
+ // while it ran. Publish each undelivered frame oldest-first exactly once.
861
+ {
862
+ const loopSeqRow = await this.#db.engine_loop_sequence.get({ loop_id: loopId });
863
+ const loopSeq = loopSeqRow?.sequence ?? loopId;
864
+ const prefix = promptLoopPrefix(loopSeq);
865
+ const undelivered = (await this.#db.drain_undelivered_prompts_for_loop.all({
866
+ owner_id: workerId,
867
+ pattern: `${prefix}%`,
868
+ prefix_len: prefix.length,
869
+ loop_id: loopId,
870
+ }))
871
+ .filter((row) => typeof row.content === "string" && row.content.length > 0);
872
+ for (const injectedRow of undelivered) {
873
+ const attributes = parsePromptAttributes(injectedRow.attributes, `Prompt ${injectedRow.pathname} attributes`);
874
+ if (attributes.openPaths !== undefined) {
875
+ turnOpenPaths.push(...assertOpenPaths(attributes.openPaths, `Prompt ${injectedRow.pathname} openPaths`));
876
+ }
877
+ const ordinal = Number(injectedRow.pathname.split("/").filter(Boolean).at(-1));
878
+ const promptLogId = await this.#writePromptLog({
879
+ workerId,
880
+ loopId,
881
+ turnId,
882
+ sequence: nextActionIndex++,
883
+ target: promptTarget(loopSeq, ordinal),
884
+ content: injectedRow.content,
885
+ });
886
+ onDispatch?.(promptLogId);
887
+ }
888
+ }
889
+ // {§methods-loop-run-open-paths}: selected workspace paths belong to
890
+ // the prompt frame. Publish the frame, then dispatch ordinary core READs
891
+ // in that same turn; missing/non-member paths retain their normal 4xx.
892
+ for (const raw of turnOpenPaths) {
893
+ const pathname = raw.startsWith("/") ? raw : `/${raw}`;
894
+ const fileRead = {
895
+ op: "READ", delimiter: "", annotation: null, signal: null, lineMarker: null,
896
+ target: {
897
+ kind: "url", raw: `file://${pathname}`, scheme: "file",
898
+ username: null, password: null, hostname: null, port: null,
899
+ pathname, query: null, fragment: null,
900
+ },
901
+ body: null, position: UNKNOWN_POSITION,
902
+ };
903
+ await this.#dispatch({
904
+ statement: fileRead, workspaceId, workerId, loopId, turnId,
905
+ sequence: nextActionIndex, origin: "_plurnk", onDispatch,
906
+ });
907
+ nextActionIndex++;
908
+ }
909
+ // {§env-delta-log-pull} — materialize ambient observations before packet
910
+ // composition and reserve their action indices. {§exec-stream} owns the
911
+ // distinct byte-cursor path for this worker's streams.
912
+ // {§exec-poll} — EXEC `<0>` is turn-scoped: reap the worker's open turn-scoped streams (necessarily
913
+ // from a prior turn — this runs before the turn's own spawns) so a `<0>` never survives into
914
+ // the subsequent turn. The terminal output then surfaces born-OPEN via the stream-delta path.
915
+ await this.#reapTurnScopedStreams(workerId);
916
+ nextActionIndex += await this.#materializeEnvironmentDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
917
+ nextActionIndex += await this.#materializeStreamDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
918
+ // The post-reconciliation Git snapshot above is threaded into the packet
919
+ // and every budget rebuild; overflow never shells again.
920
+ // Notices are non-terminal observations, never operation-failure truth.
921
+ // Drain once and thread the same set through every overflow rebuild.
922
+ const notices = this.#notices.drain(loopId)
923
+ .filter((event) => event.level !== "info");
924
+ // Build the model request packet ({§packet-stored-shape}). The log build
925
+ // queries log_entries scoped to the worker — the prompt entry just
926
+ // written (if turn 1) is part of that query result.
927
+ let promptProjection = "automatic";
928
+ const buildPacket = () => this.#packets.buildRequestPacket({
929
+ initialMessages: messages,
930
+ requirements,
931
+ workspaceId,
932
+ workerId,
933
+ loopId,
934
+ currentTurnSeq: seq,
935
+ provider,
936
+ gitStatus,
937
+ notices,
938
+ transientOpenLogEntryId,
939
+ promptProjection,
940
+ });
941
+ let requestPacket = await buildPacket();
942
+ // {§overflow-turn} — measured overflow diverts this would-be model
943
+ // turn into an ordinary packetless `_plurnk` operation batch. Every
944
+ // visibility effect goes through Dispatcher FOLD; provider I/O is
945
+ // structurally unreachable on this branch.
946
+ const pressure = this.#packets.curationOverflow(requestPacket, provider);
947
+ if (pressure !== null) {
948
+ const folds = await OverflowTurn.plan(this.#db, loopId, turnId);
949
+ await Turn.becomeOverflow(this.#db, turnId);
950
+ const internalStatements = [
951
+ OverflowTurn.planStatement(),
952
+ ...folds.map(({ statement }) => statement),
953
+ OverflowTurn.sendStatement(),
954
+ ];
955
+ const source = TurnOps.renderInternal(internalStatements);
956
+ const admitted = TurnOps.parseInternal(source);
957
+ const executed = await this.#executeAdmittedTurn({
958
+ statements: admitted,
959
+ source,
960
+ sourceFolded: true,
961
+ origin: "_plurnk",
962
+ workspaceId,
963
+ workerId,
964
+ loopId,
965
+ turnId,
966
+ fromSequence: nextActionIndex,
967
+ failOnOperationError: true,
968
+ signal: this.#loopSignal(loopId),
969
+ onDispatch,
970
+ });
971
+ if (executed.status !== TURN_STATUS_IMPLICIT_CONTINUE) {
972
+ throw new Error(`overflow SEND returned ${executed.status}; expected ${TURN_STATUS_IMPLICIT_CONTINUE}`);
973
+ }
974
+ const rebuilt = await buildPacket();
975
+ const remaining = this.#packets.curationOverflow(rebuilt, provider);
976
+ const curationFailure = folds.length === 0 || remaining !== null
977
+ ? curationOverflowFailure(remaining ?? pressure)
978
+ : undefined;
979
+ return {
980
+ createdTurnIds,
981
+ turnId,
982
+ producer: "_plurnk",
983
+ kind: "overflow",
984
+ status: curationFailure?.status ?? TURN_STATUS_IMPLICIT_CONTINUE,
985
+ outcomes: [],
986
+ fingerprint: "",
987
+ capacityHardStop: false,
988
+ steerStruck: false,
989
+ emissionAttempts: 0,
990
+ emissionExhausted: false,
991
+ ...(curationFailure === undefined ? {} : { curationFailure }),
992
+ };
993
+ }
994
+ let modelMessages = PacketWire.packetToWireMessages(requestPacket);
995
+ // Curation pressure and provider generation are independent. The
996
+ // provider owns its configured total output envelope.
997
+ let response;
998
+ let splitResponse;
999
+ let railGrammar;
1000
+ let railResponseGrammar;
1001
+ let railAllowWithheld = false;
1002
+ let railEvidence;
1003
+ let emissionAttempts = 0;
1004
+ let providerCallInFlight = false;
1005
+ let modelCallSequence = 0;
1006
+ let currentEmissionAttempt = 0;
1007
+ let providerAttemptId = null;
1008
+ let providerModelCall = null;
1009
+ let providerAttemptAttributions = [];
1010
+ const providerSignal = this.#loopSignal(loopId) ?? signal;
1011
+ // {§client-metadata}
1012
+ const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
1013
+ const loopSeq = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
1014
+ const providerIdentity = await this.#resolveWorkerProviderIdentity(workerId);
1015
+ const { workerId: providerWorkerId, primaryWorkerId } = providerIdentity;
1016
+ const classifyProviderAttempt = async (id, attemptSplit, emissionAttempt, accepted) => {
1017
+ const result = await this.#db.engine_classify_turn_attempt_response.run({
1018
+ id,
1019
+ accepted: accepted ? 1 : 0,
1020
+ parse_errors: JSON.stringify(attemptSplit.parseErrors),
1021
+ });
1022
+ if (result.changes !== 1) {
1023
+ throw new Error(`emission attempt ${id} was not awaiting classification`);
1024
+ }
1025
+ emissionAttempts = emissionAttempt;
1026
+ };
1027
+ const recoverCapacityPacket = async () => {
1028
+ let baselineMessages = modelMessages;
1029
+ if (promptProjection === "automatic") {
1030
+ promptProjection = "withheld";
1031
+ const candidate = await buildPacket();
1032
+ const candidateMessages = PacketWire.packetToWireMessages(candidate);
1033
+ if (JSON.stringify(candidateMessages) !== JSON.stringify(baselineMessages)) {
1034
+ requestPacket = candidate;
1035
+ modelMessages = candidateMessages;
1036
+ return true;
1037
+ }
1038
+ baselineMessages = candidateMessages;
1039
+ }
1040
+ return false;
1041
+ };
1042
+ try {
1043
+ // {§turn-lifecycle}: bracket the complete provider-attempt window with liveness notices.
1044
+ if (!signal?.aborted)
1045
+ this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
1046
+ const railConstraint = await this.#grammarConstraint(provider);
1047
+ railGrammar = railConstraint?.transport;
1048
+ railResponseGrammar = railConstraint?.response;
1049
+ railAllowWithheld = railConstraint?.allowWithheld ?? false;
1050
+ const attemptLimit = readEmissionAttempts();
1051
+ const strikeStreak = this.#strikes.streak(loopId);
1052
+ for (let attempt = 1; attempt <= attemptLimit;) {
1053
+ // Capacity recovery may rebuild and resend the request without
1054
+ // consuming a grammar-emission attempt. Every logical provider
1055
+ // call still receives its own durable sequence and accounting.
1056
+ currentEmissionAttempt = attempt;
1057
+ modelCallSequence++;
1058
+ const attributionContext = Object.freeze({
1059
+ workspaceId: String(workspaceId),
1060
+ workerId: providerWorkerId,
1061
+ primaryWorkerId,
1062
+ loop: loopSeq,
1063
+ turn: seq,
1064
+ attempt: modelCallSequence,
1065
+ });
1066
+ providerAttemptAttributions = await this.#attemptAttributions(provider, attributionContext);
1067
+ requestPacket = { ...requestPacket, attributions: providerAttemptAttributions };
1068
+ providerModelCall = await ModelCall.open(this.#db, {
1069
+ turnId,
1070
+ sequence: modelCallSequence,
1071
+ kind: "emission",
1072
+ attributions: providerAttemptAttributions,
1073
+ model: provider.model,
1074
+ });
1075
+ const attemptRow = await this.#db.engine_open_turn_attempt.get({
1076
+ model_call_id: providerModelCall.id,
1077
+ });
1078
+ if (attemptRow === undefined) {
1079
+ throw new Error(`Engine.runTurn: provider call ${modelCallSequence} did not open`);
1080
+ }
1081
+ providerAttemptId = attemptRow.id;
1082
+ providerCallInFlight = true;
1083
+ const currentModelCall = providerModelCall;
1084
+ let completedResponse;
1085
+ try {
1086
+ completedResponse = await observed(// {§observability-boundary}
1087
+ GEN_AI_REQUEST_SPAN, { model: provider.model, attempt: modelCallSequence }, async (span) => {
1088
+ try {
1089
+ const generated = await provider.generate({
1090
+ messages: modelMessages,
1091
+ workerId: providerWorkerId,
1092
+ primaryWorkerId,
1093
+ signal: providerSignal,
1094
+ grammar: railGrammar,
1095
+ strikes: strikeStreak,
1096
+ attributions: providerAttemptAttributions.length > 0
1097
+ ? providerAttemptAttributions
1098
+ : undefined,
1099
+ client: client ?? undefined,
1100
+ workspaceId: String(workspaceId),
1101
+ loop: loopSeq,
1102
+ turn: seq,
1103
+ observeRequest: currentModelCall.observeRequest,
1104
+ callKind: "emission",
1105
+ }); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
1106
+ currentModelCall.assertAccounting(generated.accounting);
1107
+ providerCallInFlight = false;
1108
+ recordCounter(PROVIDER_CALLS, {
1109
+ model: provider.model,
1110
+ attempt: modelCallSequence,
1111
+ status: "resolved",
1112
+ });
1113
+ span.setAttribute("status", "resolved");
1114
+ settleGenAiResponse(span, generated);
1115
+ return generated;
1116
+ }
1117
+ catch (error) {
1118
+ if (error instanceof ProviderError) {
1119
+ currentModelCall.assertAccounting(error.accounting);
1120
+ }
1121
+ throw error;
1122
+ }
1123
+ }, genAiRequestOptions(ProviderInstantiate.aliasOf(provider) ?? "plurnk", provider.model));
1124
+ }
1125
+ catch (error) {
1126
+ if (!(error instanceof ProviderError)
1127
+ || error.kind !== "capacity_exceeded"
1128
+ || providerSignal?.aborted === true
1129
+ || !await recoverCapacityPacket()) {
1130
+ throw error;
1131
+ }
1132
+ const failure = _a.#providerFailure(error, providerSignal);
1133
+ await currentModelCall.fail(failure, error.capacity ?? null);
1134
+ providerCallInFlight = false;
1135
+ await this.#problems.record({
1136
+ workerId,
1137
+ loopId,
1138
+ turnId,
1139
+ sequence: nextActionIndex++,
1140
+ origin: "_plurnk",
1141
+ source: "provider",
1142
+ result: failure,
1143
+ });
1144
+ // Include the durable recovery signal in the replacement
1145
+ // request while preserving the selected recovery posture.
1146
+ requestPacket = await buildPacket();
1147
+ modelMessages = PacketWire.packetToWireMessages(requestPacket);
1148
+ continue;
1149
+ }
1150
+ response = completedResponse;
1151
+ await currentModelCall.observeResponse(completedResponse);
1152
+ railEvidence = railGrammar === undefined
1153
+ ? undefined
1154
+ : _a.#requireGrammarEvidence(completedResponse, railAllowWithheld);
1155
+ splitResponse = this.#splitResponse(completedResponse);
1156
+ await classifyProviderAttempt(attemptRow.id, splitResponse, attempt, splitResponse.emissionValid);
1157
+ if (splitResponse.emissionValid)
1158
+ break;
1159
+ attempt++;
1160
+ }
1161
+ if (!signal?.aborted)
1162
+ this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_generated", level: "info", message: "parsing model response" });
1163
+ }
1164
+ catch (err) {
1165
+ // This handler owns only provider-call failures. Parser, cost, SQL,
1166
+ // and engine-contract failures retain their original source.
1167
+ if (err instanceof ModelCallPersistenceError || err instanceof ProviderAccountingIntegrityError)
1168
+ throw err;
1169
+ if (!providerCallInFlight)
1170
+ throw err;
1171
+ providerCallInFlight = false;
1172
+ if (providerAttemptId === null || providerModelCall === null) {
1173
+ throw new Error("provider call failed without durable model-call and attempt identities", { cause: err });
1174
+ }
1175
+ const failure = _a.#providerFailure(err, providerSignal);
1176
+ const capacityFailure = err instanceof ProviderError && err.kind === "capacity_exceeded";
1177
+ // {§provider-interrupted-attempt} — a provider-declared interruption
1178
+ // carries response evidence without becoming a completed exchange.
1179
+ // Persist it as an unaccepted attempt before settling the failure.
1180
+ if (err instanceof ProviderError && err.attempt !== undefined) {
1181
+ response = err.attempt;
1182
+ await providerModelCall.observeResponse(response, failure);
1183
+ splitResponse = this.#splitResponse(response);
1184
+ await classifyProviderAttempt(providerAttemptId, splitResponse, currentEmissionAttempt, false);
1185
+ }
1186
+ else {
1187
+ await providerModelCall.fail(failure, err instanceof ProviderError ? err.capacity ?? null : null);
1188
+ if (!capacityFailure)
1189
+ emissionAttempts = currentEmissionAttempt;
1190
+ }
1191
+ // {§turn-never-blank} — a ProviderError means no completed exchange exists.
1192
+ // Persist its exact RFC 9457 result before propagating it. Grammar evidence
1193
+ // and its engine-owned verdict exist only on completed responses
1194
+ // ({§rail-truth-engine-verdict}). Cancellation is lifecycle truth, not a
1195
+ // provider failure. Close the
1196
+ // attempted turn without inventing an assistant response, then let
1197
+ // runLoop/Daemon settle the exact 504/499 loop result.
1198
+ if (providerSignal?.aborted) {
1199
+ const status = providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499;
1200
+ await Turn.recordInference(this.#db, turnId, {
1201
+ packet: StoredPacket.stringify(requestPacket),
1202
+ usageCurationBudget: this.#packets.curationBudgetFor(provider),
1203
+ finishReason: splitResponse?.callMetadata.finishReason ?? null,
1204
+ model: splitResponse?.callMetadata.model ?? provider.model,
1205
+ meta: JSON.stringify(response?.meta ?? {}),
1206
+ });
1207
+ await Turn.complete(this.#db, turnId, status);
1208
+ throw err;
1209
+ }
1210
+ const recorded = await this.#problems.record({
1211
+ workerId,
1212
+ loopId,
1213
+ turnId,
1214
+ sequence: nextActionIndex,
1215
+ origin: "_plurnk",
1216
+ source: "provider",
1217
+ result: failure,
1218
+ });
1219
+ // The provider call was attempted, but no completed exchange exists.
1220
+ // Persist the exact request half and failure status; omitting assistant
1221
+ // is materially different from fabricating an empty model turn.
1222
+ await Turn.recordInference(this.#db, turnId, {
1223
+ packet: StoredPacket.stringify(requestPacket),
1224
+ usageCurationBudget: this.#packets.curationBudgetFor(provider),
1225
+ finishReason: splitResponse?.callMetadata.finishReason ?? null,
1226
+ model: splitResponse?.callMetadata.model ?? provider.model,
1227
+ meta: JSON.stringify(response?.meta ?? {}),
1228
+ });
1229
+ await Turn.complete(this.#db, turnId, recorded.result.status);
1230
+ if (capacityFailure) {
1231
+ return {
1232
+ createdTurnIds,
1233
+ turnId,
1234
+ producer: "model",
1235
+ kind: "inference",
1236
+ status: recorded.result.status,
1237
+ outcomes: [],
1238
+ fingerprint: "",
1239
+ capacityHardStop: true,
1240
+ capacityFailure: recorded.result,
1241
+ steerStruck: false,
1242
+ emissionAttempts,
1243
+ emissionExhausted: false,
1244
+ };
1245
+ }
1246
+ throw new OperationFailureError(recorded.result, { cause: err });
1247
+ }
1248
+ if (response === undefined || splitResponse === undefined || providerModelCall === null) {
1249
+ throw new Error("provider attempt loop completed without a response");
1250
+ }
1251
+ const emissionModelCallId = providerModelCall.id;
1252
+ if (!splitResponse.emissionValid) {
1253
+ // {§invalid-emission-attempts} The first consecutive exhaustion
1254
+ // publishes only the raw final response and generic recovery fact.
1255
+ let rejectedModelEntryId;
1256
+ if (allowInvalidEmissionRecovery) {
1257
+ rejectedModelEntryId = await this.#dispatcher.writeEmissionAttempt({
1258
+ verbatim: splitResponse.packetAssistant.content,
1259
+ workerId,
1260
+ loopId,
1261
+ turnId,
1262
+ sequence: nextActionIndex,
1263
+ modelCallId: emissionModelCallId,
1264
+ ...(response.assistant.reasoningEncrypted?.length
1265
+ ? { reasoningItems: response.assistant.reasoningEncrypted }
1266
+ : {}),
1267
+ });
1268
+ this.#notices.push(workspaceId, loopId, {
1269
+ source: "engine:grammar",
1270
+ kind: "invalid_emission",
1271
+ level: "error",
1272
+ message: INVALID_EMISSION_RECOVERY_MESSAGE,
1273
+ });
1274
+ }
1275
+ const status = allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500;
1276
+ await Turn.recordInference(this.#db, turnId, {
1277
+ packet: StoredPacket.stringify(requestPacket),
1278
+ usageCurationBudget: this.#packets.curationBudgetFor(provider),
1279
+ finishReason: splitResponse.callMetadata.finishReason,
1280
+ model: splitResponse.callMetadata.model,
1281
+ meta: JSON.stringify(response.meta ?? {}),
1282
+ });
1283
+ await Turn.complete(this.#db, turnId, status);
1284
+ return {
1285
+ createdTurnIds,
1286
+ turnId,
1287
+ producer: "model",
1288
+ kind: "inference",
1289
+ status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
1290
+ outcomes: [],
1291
+ fingerprint: "",
1292
+ capacityHardStop: false,
1293
+ steerStruck: false,
1294
+ emissionAttempts,
1295
+ emissionExhausted: true,
1296
+ ...(rejectedModelEntryId === undefined ? {} : { rejectedModelEntryId }),
1297
+ };
1298
+ }
1299
+ // {§packet-stored-shape} — admitted emission data extends the packet;
1300
+ // provider-call metadata remains on the Turn row.
1301
+ const { packetAssistant, callMetadata, parseNotices, recoverableParseErrors, } = splitResponse; // raw assistant content is opaque — split, never interpreted — {§provider-guarantees-assistantraw-opaque}
1302
+ for (const notice of parseNotices) {
1303
+ this.#notices.push(workspaceId, loopId, notice);
1304
+ }
1305
+ // Non-fatal provider transport notices on an accepted turn. Forward each
1306
+ // Notice with a content-offset `line:col`;
1307
+ // the model resolves it against its own emission — READ the folded turnOps row at the
1308
+ // cited lines ({§turn-ops-entry}) — not an embedded snippet that would duplicate the emission.
1309
+ for (const notice of response.notices ?? []) {
1310
+ const located = typeof notice.position === "number"
1311
+ ? this.#offsetToLineColumn(packetAssistant.content, notice.position)
1312
+ : null;
1313
+ this.#notices.push(workspaceId, loopId, {
1314
+ source: notice.source,
1315
+ kind: notice.kind,
1316
+ message: notice.message ?? "",
1317
+ level: notice.level,
1318
+ ...(located !== null
1319
+ ? { position: { type: "content-offset", line: located.line, column: located.column } }
1320
+ : {}),
1321
+ });
1322
+ }
1323
+ // Grade configured local evidence independently. Endpoint-owned
1324
+ // constraints remain provider observations. {§rail-truth-engine-verdict}
1325
+ let railKeys;
1326
+ if (railGrammar !== undefined) {
1327
+ if (railEvidence === undefined)
1328
+ throw new Error("configured GBNF response has no final grammar evidence");
1329
+ if (railResponseGrammar === undefined)
1330
+ throw new Error("configured GBNF has no response grammar");
1331
+ let verdict = null;
1332
+ try {
1333
+ verdict = validateGbnf(railResponseGrammar, railEvidence.input);
1334
+ }
1335
+ catch (cause) {
1336
+ _a.#warnRailVerdictGapOnce(cause.message);
1337
+ }
1338
+ railKeys = {
1339
+ railsAttached: railEvidence.transported ? "client" : "withheld",
1340
+ railsVerdict: verdict?.status ?? "unverifiable",
1341
+ };
1342
+ if (verdict !== null && verdict.status !== "accept") {
1343
+ const contentPosition = verdict.pos >= railEvidence.contentStart
1344
+ ? verdict.pos - railEvidence.contentStart
1345
+ : null;
1346
+ const located = contentPosition === null
1347
+ ? null
1348
+ : this.#offsetToLineColumn(packetAssistant.content, contentPosition);
1349
+ this.#notices.push(workspaceId, loopId, {
1350
+ source: "engine:rails",
1351
+ kind: "grammar_unenforced",
1352
+ message: verdict.status === "reject"
1353
+ ? `emission rejects the grammar at raw code point ${verdict.pos}`
1354
+ : `emission is an incomplete grammar sentence (ends at raw code point ${verdict.pos})`,
1355
+ level: "warn",
1356
+ ...(located === null
1357
+ ? {}
1358
+ : { position: { type: "content-offset", line: located.line, column: located.column } }),
1359
+ });
1360
+ }
1361
+ }
1362
+ // Attach the admitted inference evidence. The turn remains open until
1363
+ // the producer-neutral admitted-turn executor settles every operation
1364
+ // and its exact source artifact.
1365
+ const packet = StoredPacket.admit(requestPacket, packetAssistant, response.assistantRaw);
1366
+ await Turn.recordInference(this.#db, turnId, {
1367
+ packet: StoredPacket.stringify(packet),
1368
+ usageCurationBudget: this.#packets.curationBudgetFor(provider), // {§tokenomics-client-gauge}
1369
+ finishReason: callMetadata.finishReason,
1370
+ model: callMetadata.model,
1371
+ // Opaque provider metadata plus engine-authored rail keys.
1372
+ // {§meta-passthrough}, {§rail-truth-engine-verdict}
1373
+ meta: JSON.stringify({ ...(response.meta ?? {}), ...(railKeys ?? {}) }),
1374
+ });
1375
+ // {§operator-config-workspace-max-commands} — workspace maxCommands
1376
+ // narrows the operator ceiling before the admitted program reaches the
1377
+ // shared executor.
1378
+ const maxCommands = Math.min(readMaxCommands(), (await WorkspaceSettings.read(this.#db, workspaceId)).maxCommands ?? Number.POSITIVE_INFINITY);
1379
+ const reasoningItems = response.assistant.reasoningEncrypted?.length
1380
+ ? response.assistant.reasoningEncrypted
1381
+ : undefined;
1382
+ const executed = await this.#executeAdmittedTurn({
1383
+ statements: packetAssistant.ops,
1384
+ source: splitResponse.sourceBacked ? packetAssistant.content : null,
1385
+ sourceFolded: true,
1386
+ sourceModelCallId: emissionModelCallId,
1387
+ ...(reasoningItems !== undefined ? { sourceReasoningItems: reasoningItems } : {}),
1388
+ origin: "model",
1389
+ workspaceId,
1390
+ workerId,
1391
+ loopId,
1392
+ turnId,
1393
+ fromSequence: nextActionIndex,
1394
+ maxCommands,
1395
+ enforceIdle: true,
1396
+ recoverableParseErrors,
1397
+ bare: {
1398
+ provider: childProvider,
1399
+ modelCallSequenceStart: modelCallSequence + 1,
1400
+ primaryWorkerId,
1401
+ loopSequence: loopSeq,
1402
+ turnSequence: seq,
1403
+ signal: providerSignal,
1404
+ },
1405
+ signal: providerSignal,
1406
+ onDispatch,
1407
+ });
1408
+ return {
1409
+ createdTurnIds,
1410
+ turnId,
1411
+ producer: "model",
1412
+ kind: "inference",
1413
+ status: executed.status,
1414
+ outcomes: executed.outcomes,
1415
+ fingerprint: executed.fingerprint,
1416
+ capacityHardStop: false,
1417
+ steerStruck: executed.steerStruck,
1418
+ emissionAttempts,
1419
+ emissionExhausted: false,
1420
+ };
1421
+ }
1422
+ catch (cause) {
1423
+ const completionFailures = [];
1424
+ for (const createdTurnId of createdTurnIds) {
1425
+ try {
1426
+ await Turn.failOpen(this.#db, createdTurnId);
1427
+ }
1428
+ catch (completionCause) {
1429
+ completionFailures.push(completionCause);
1430
+ }
1431
+ }
1432
+ if (completionFailures.length > 0) {
1433
+ throw new AggregateError([cause, ...completionFailures], "turn execution failed and its open turn containers could not all be completed");
1434
+ }
1435
+ throw cause;
1436
+ }
1437
+ }
1319
1438
  // Split the wire-level ProviderResponse into the two destinations:
1320
1439
  // packet.assistant gets the model's emission (content, ops, reasoning);
1321
1440
  // Turn columns get accepted-call metadata (finishReason, model). Physical
@@ -1409,6 +1528,7 @@ class TurnRunner {
1409
1528
  const reasoning = assistant.reasoning ?? null;
1410
1529
  return {
1411
1530
  packetAssistant: { content: assistant.content, ops, reasoning },
1531
+ sourceBacked: preParsedOps === undefined,
1412
1532
  callMetadata: { finishReason: assistant.finishReason, model: assistant.model },
1413
1533
  parseErrors,
1414
1534
  recoverableParseErrors: emissionValid ? recoverableParseErrors : [],
@@ -1421,8 +1541,8 @@ class TurnRunner {
1421
1541
  emissionValid,
1422
1542
  };
1423
1543
  }
1424
- // #note12 — the plugin-provided reference docs (schemes' + execs' `documentation`),
1425
- // materialized at worker://plurnk/docs/<name>.md by LoopDocs (like operator docs).
1544
+ // #note12 — plugin reference docs are materialized beneath
1545
+ // worker://plurnk/skills/plurnk/ by LoopDocs.
1426
1546
  async #materializeEnvironmentDeltas(args) {
1427
1547
  const { workspaceId, workerId, loopId, turnId, fromSequence } = args;
1428
1548
  const rows = await this.#db.engine_pull_ambient_events.all({ workspace_id: workspaceId, worker_id: workerId });
@@ -1484,7 +1604,9 @@ class TurnRunner {
1484
1604
  mimetypeTx: "text/plain",
1485
1605
  mimetypeRx: "application/json",
1486
1606
  }, this.#weighContent),
1487
- expanded: terminal !== null && terminal.status >= 200 && terminal.status < 300 ? 1 : 0,
1607
+ folded: LogVisibility.serialize(terminal !== null && terminal.status >= 200 && terminal.status < 300
1608
+ ? LogVisibility.OPEN
1609
+ : LogVisibility.FOLDED),
1488
1610
  attrs,
1489
1611
  });
1490
1612
  const materialized = inserted ?? await this.#db.engine_ambient_delta_id.get({
@@ -1517,7 +1639,7 @@ class TurnRunner {
1517
1639
  await Promise.all(open.map(({ id }) => this.#liveSubscriptions.cancel(id)));
1518
1640
  }
1519
1641
  async #materializeStreamDeltas(args) {
1520
- const { workerId, loopId, turnId, fromSequence } = args;
1642
+ const { workspaceId, workerId, loopId, turnId, fromSequence } = args;
1521
1643
  const channels = await this.#db.engine_worker_stream_channels.all({ worker_id: workerId });
1522
1644
  let written = 0;
1523
1645
  for (const ch of channels) {
@@ -1526,7 +1648,7 @@ class TurnRunner {
1526
1648
  // ordinary address to the model; only an explicitly non-default
1527
1649
  // channel earns a fragment in the log.
1528
1650
  const visibleFragment = ch.published_channel !== null
1529
- && ch.channel === this.#schemes.defaultChannelFor(ch.runtime)
1651
+ && ch.channel === this.#schemes.defaultChannelFor(ch.runtime, workspaceId)
1530
1652
  ? null
1531
1653
  : ch.channel;
1532
1654
  const prior = await this.#db.engine_stream_cursor.get({
@@ -1564,8 +1686,9 @@ class TurnRunner {
1564
1686
  // content and the auto-OPEN terminal observation never fired — the model was never shown
1565
1687
  // the conclusion of a stream whose result it already holds folded. The same
1566
1688
  // observation is required when the channel produced no publishable content:
1567
- // completion is information independently of payload. Text streams retain their
1568
- // terse marker; structured channels emit a bodyless typed conclusion.
1689
+ // completion is information independently of payload. A text stream that delivered
1690
+ // content retains its terse revisit marker; an empty text stream and every structured
1691
+ // channel emit a bodyless typed conclusion.
1569
1692
  if (closed && priorAttrs.terminal !== true) {
1570
1693
  const streamTarget = renderTarget({
1571
1694
  scheme: ch.runtime,
@@ -1574,12 +1697,9 @@ class TurnRunner {
1574
1697
  });
1575
1698
  if (streamTarget === null)
1576
1699
  throw new Error(`stream ${ch.subscription_id} has no renderable address`);
1577
- const pointer = cursor > 0
1578
- ? `full output already delivered above; READ ${streamTarget} to revisit`
1579
- : "stream produced no output";
1580
1700
  const sequence = fromSequence + written;
1581
- const content = baseMimetype(ch.mimetype).startsWith("text/")
1582
- ? `[ stream closed (${ch.close_status ?? 200}) - ${pointer} ]`
1701
+ const content = cursor > 0 && baseMimetype(ch.mimetype).startsWith("text/")
1702
+ ? `[ stream closed (${ch.close_status ?? 200}) - full output already delivered above; READ ${streamTarget} to revisit ]`
1583
1703
  : "";
1584
1704
  const rx = JSON.stringify(await terminalResult({
1585
1705
  content,
@@ -1599,7 +1719,7 @@ class TurnRunner {
1599
1719
  }, this.#weighContent),
1600
1720
  status: terminal?.status ?? 200,
1601
1721
  attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: true }),
1602
- expanded: 1,
1722
+ folded: LogVisibility.serialize(LogVisibility.OPEN),
1603
1723
  });
1604
1724
  written++;
1605
1725
  }
@@ -1630,55 +1750,85 @@ class TurnRunner {
1630
1750
  }, this.#weighContent),
1631
1751
  status: result.status,
1632
1752
  attrs: JSON.stringify({ streamEnd: publishEnd, terminal: terminalDelivery }),
1633
- expanded: terminalDelivery ? 1 : 0, // {§exec-stream} — terminal observation auto-OPENs; ongoing folds
1753
+ folded: LogVisibility.serialize(terminalDelivery ? LogVisibility.OPEN : LogVisibility.FOLDED), // {§exec-stream} — terminal observation auto-OPENs; ongoing folds
1634
1754
  });
1635
1755
  written++;
1636
1756
  }
1637
1757
  return written;
1638
1758
  }
1639
1759
  // {§env-delta-filesystem-narration} {§membership-emi-divergence-signal}
1640
- // — journal project-file divergence once through the reserved actor.
1760
+ // — record project-file divergence once through the reserved actor.
1641
1761
  async #logFsFictions(workspaceId, divergences, gitStatus) {
1642
1762
  if (divergences.length === 0)
1643
1763
  return;
1644
1764
  const gitByPath = new Map(gitStatus?.files.map(({ path, status }) => [path, status]) ?? []);
1645
1765
  const worker = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name: "plurnk" })
1646
- ?? await this.#db.envelope_insert_worker.get({ workspace_id: workspaceId, name: "plurnk", origin: "plurnk" });
1766
+ ?? await this.#db.envelope_insert_worker.get({ workspace_id: workspaceId, name: "plurnk", origin: "_plurnk" });
1647
1767
  if (worker === undefined)
1648
1768
  throw new Error("logFsFictions: plurnk worker resolution returned no row");
1649
1769
  const loop = await this.#db.envelope_insert_client_loop.get({ worker_id: worker.id });
1650
1770
  if (loop === undefined)
1651
1771
  throw new Error("logFsFictions: loop insert returned no row");
1652
- const turn = await JournalTurn.insert(this.#db, loop.id);
1653
- let sequence = 1;
1654
- for (const d of divergences) {
1655
- const span = editedSpan(d.before, d.after);
1656
- const rx = JSON.stringify({ status: 200, entryId: d.entryId, channel: d.channel, span });
1657
- const attrs = gitByPath.has(d.pathname)
1658
- ? JSON.stringify({ git: gitByPath.get(d.pathname) })
1659
- : "{}";
1660
- await this.#db.engine_insert_log_entry.get({
1661
- worker_id: worker.id, loop_id: loop.id, turn_id: turn.id, sequence: sequence++,
1662
- origin: "plurnk", source: "file", model_call_id: null,
1663
- op: "EDIT", suffix: "", signal: null,
1664
- // Match Dispatcher.#extractTarget: a bare file address has NULL scheme
1665
- // only in log target metadata; its entry identity remains `file`.
1666
- scheme: null, username: null, password: null, hostname: null, port: null,
1667
- pathname: d.pathname, query: null, fragment: null, lineMarker: null,
1668
- tx: "", mimetype_tx: "text/plain",
1669
- rx, mimetype_rx: "application/json",
1670
- status_rx: 200,
1671
- weight: LogBody.weight({
1672
- op: "EDIT",
1772
+ const turn = await Turn.open(this.#db, { loopId: loop.id, producer: "_plurnk", kind: "operation" });
1773
+ let turnOpen = true;
1774
+ try {
1775
+ let sequence = 1;
1776
+ for (const d of divergences) {
1777
+ const span = editedSpan(d.before, d.after);
1778
+ const rx = JSON.stringify({ status: 200, entryId: d.entryId, channel: d.channel, span });
1779
+ const attrs = gitByPath.has(d.pathname)
1780
+ ? JSON.stringify({ git: gitByPath.get(d.pathname) })
1781
+ : "{}";
1782
+ await this.#db.engine_insert_log_entry.get({
1783
+ worker_id: worker.id, loop_id: loop.id, turn_id: turn.id, sequence: sequence++,
1784
+ origin: "_plurnk", source: "file", model_call_id: null,
1785
+ op: "EDIT", delimiter: "", signal: null,
1786
+ // Match Dispatcher.#extractTarget: a bare file address has NULL scheme
1787
+ // only in log target metadata; its entry identity remains `file`.
1788
+ scheme: null, username: null, password: null, hostname: null, port: null,
1789
+ pathname: d.pathname, query: null, fragment: null, lineMarker: null,
1790
+ tx: "", mimetype_tx: "text/plain",
1791
+ rx, mimetype_rx: "application/json",
1792
+ status_rx: 200,
1793
+ weight: LogBody.weight({
1794
+ op: "EDIT",
1795
+ attrs,
1796
+ tx: "",
1797
+ rx,
1798
+ mimetypeTx: "text/plain",
1799
+ mimetypeRx: "application/json",
1800
+ }, this.#weighContent),
1801
+ state: "resolved", outcome: null,
1673
1802
  attrs,
1674
- tx: "",
1675
- rx,
1676
- mimetypeTx: "text/plain",
1677
- mimetypeRx: "application/json",
1678
- }, this.#weighContent),
1679
- state: "resolved", outcome: null,
1680
- attrs,
1681
- });
1803
+ });
1804
+ }
1805
+ await Turn.complete(this.#db, turn.id, 200);
1806
+ turnOpen = false;
1807
+ const closed = await new LoopLifecycle(this.#db).finish(loop.id, { status: 200 });
1808
+ if (closed === null)
1809
+ throw new Error(`logFsFictions: narration loop ${loop.id} was not open at completion`);
1810
+ }
1811
+ catch (cause) {
1812
+ const settlementFailures = [];
1813
+ const failure = Results.failure("engine:filesystem-narration", "narration-failed", 500, "Filesystem divergence narration failed before its operation turn settled.", {}, { stage: "filesystem-narration", retryable: false });
1814
+ if (turnOpen) {
1815
+ try {
1816
+ await Turn.complete(this.#db, turn.id, failure.status);
1817
+ }
1818
+ catch (turnCause) {
1819
+ settlementFailures.push(turnCause);
1820
+ }
1821
+ }
1822
+ try {
1823
+ await new LoopLifecycle(this.#db).finish(loop.id, failure);
1824
+ }
1825
+ catch (loopCause) {
1826
+ settlementFailures.push(loopCause);
1827
+ }
1828
+ if (settlementFailures.length > 0) {
1829
+ throw new AggregateError([cause, ...settlementFailures], `filesystem narration ${turn.id} failed to settle`);
1830
+ }
1831
+ throw cause;
1682
1832
  }
1683
1833
  }
1684
1834
  async #writePromptLog({ workerId, loopId, turnId, sequence, target, content, }) {
@@ -1688,11 +1838,11 @@ class TurnRunner {
1688
1838
  loop_id: loopId,
1689
1839
  turn_id: turnId,
1690
1840
  sequence,
1691
- origin: "plurnk",
1841
+ origin: "_plurnk",
1692
1842
  source: null,
1693
1843
  model_call_id: null,
1694
1844
  op: "prompt",
1695
- suffix: "",
1845
+ delimiter: "",
1696
1846
  signal: null,
1697
1847
  scheme: target.scheme,
1698
1848
  username: target.username,