@plurnk/plurnk-service 2.0.0 → 3.1.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 (333) hide show
  1. package/.env.defaults +27 -19
  2. package/INSTALL.md +21 -2
  3. package/README.md +4 -2
  4. package/SPEC.md +644 -467
  5. package/dist/build-info.json +1 -1
  6. package/dist/completions/_plurnk-service +13 -0
  7. package/dist/completions/plurnk-service.bash +19 -0
  8. package/dist/completions/plurnk-service.fish +93 -0
  9. package/dist/content/edit-receipt.d.ts +1 -1
  10. package/dist/content/edit-receipt.d.ts.map +1 -1
  11. package/dist/content/edit-receipt.js +1 -1
  12. package/dist/content/edit-receipt.js.map +1 -1
  13. package/dist/content/read-projector.d.ts.map +1 -1
  14. package/dist/content/read-projector.js +3 -12
  15. package/dist/content/read-projector.js.map +1 -1
  16. package/dist/core/AdministrativeLoop.d.ts +1 -1
  17. package/dist/core/AdministrativeLoop.d.ts.map +1 -1
  18. package/dist/core/AdministrativeLoop.js +2 -6
  19. package/dist/core/AdministrativeLoop.js.map +1 -1
  20. package/dist/core/AdmittedTurnExecutor.d.ts +2 -2
  21. package/dist/core/AdmittedTurnExecutor.d.ts.map +1 -1
  22. package/dist/core/AdmittedTurnExecutor.js +16 -24
  23. package/dist/core/AdmittedTurnExecutor.js.map +1 -1
  24. package/dist/core/BudgetReadout.d.ts +8 -2
  25. package/dist/core/BudgetReadout.d.ts.map +1 -1
  26. package/dist/core/BudgetReadout.js +48 -5
  27. package/dist/core/BudgetReadout.js.map +1 -1
  28. package/dist/core/CapabilityPolicies.d.ts +2 -4
  29. package/dist/core/CapabilityPolicies.d.ts.map +1 -1
  30. package/dist/core/CapabilityPolicies.js +3 -23
  31. package/dist/core/CapabilityPolicies.js.map +1 -1
  32. package/dist/core/CapabilityResolver.d.ts +9 -2
  33. package/dist/core/CapabilityResolver.d.ts.map +1 -1
  34. package/dist/core/CapabilityResolver.js +34 -5
  35. package/dist/core/CapabilityResolver.js.map +1 -1
  36. package/dist/core/ChannelWrite.d.ts +0 -2
  37. package/dist/core/ChannelWrite.d.ts.map +1 -1
  38. package/dist/core/ChannelWrite.js.map +1 -1
  39. package/dist/core/ClientInteractions.d.ts +3 -2
  40. package/dist/core/ClientInteractions.d.ts.map +1 -1
  41. package/dist/core/ClientInteractions.js +21 -12
  42. package/dist/core/ClientInteractions.js.map +1 -1
  43. package/dist/core/ClientInteractions.sql +8 -2
  44. package/dist/core/CoreSchemeServices.d.ts +1 -0
  45. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  46. package/dist/core/CoreSchemeServices.js +1 -0
  47. package/dist/core/CoreSchemeServices.js.map +1 -1
  48. package/dist/core/DataStatementRunner.js +1 -1
  49. package/dist/core/DataStatementRunner.js.map +1 -1
  50. package/dist/core/Dispatcher.d.ts +3 -2
  51. package/dist/core/Dispatcher.d.ts.map +1 -1
  52. package/dist/core/Dispatcher.js +55 -52
  53. package/dist/core/Dispatcher.js.map +1 -1
  54. package/dist/core/Engine.d.ts +1 -1
  55. package/dist/core/Engine.d.ts.map +1 -1
  56. package/dist/core/Engine.js +4 -6
  57. package/dist/core/Engine.js.map +1 -1
  58. package/dist/core/EnvFlags.d.ts.map +1 -1
  59. package/dist/core/EnvFlags.js +5 -4
  60. package/dist/core/EnvFlags.js.map +1 -1
  61. package/dist/core/FabricatedLog.d.ts +1 -0
  62. package/dist/core/FabricatedLog.d.ts.map +1 -1
  63. package/dist/core/FabricatedLog.js +9 -1
  64. package/dist/core/FabricatedLog.js.map +1 -1
  65. package/dist/core/HostPaths.d.ts +0 -1
  66. package/dist/core/HostPaths.d.ts.map +1 -1
  67. package/dist/core/HostPaths.js +0 -2
  68. package/dist/core/HostPaths.js.map +1 -1
  69. package/dist/core/KillHandler.d.ts.map +1 -1
  70. package/dist/core/KillHandler.js +3 -1
  71. package/dist/core/KillHandler.js.map +1 -1
  72. package/dist/core/LiveSubscription.d.ts.map +1 -1
  73. package/dist/core/LiveSubscription.js +14 -2
  74. package/dist/core/LiveSubscription.js.map +1 -1
  75. package/dist/core/LiveSubscriptions.d.ts +3 -1
  76. package/dist/core/LiveSubscriptions.d.ts.map +1 -1
  77. package/dist/core/LiveSubscriptions.js +16 -1
  78. package/dist/core/LiveSubscriptions.js.map +1 -1
  79. package/dist/core/LogBody.d.ts.map +1 -1
  80. package/dist/core/LogBody.js +3 -1
  81. package/dist/core/LogBody.js.map +1 -1
  82. package/dist/core/LogVisibility.d.ts.map +1 -1
  83. package/dist/core/LogVisibility.js +8 -17
  84. package/dist/core/LogVisibility.js.map +1 -1
  85. package/dist/core/LoopDriver.js +7 -6
  86. package/dist/core/LoopDriver.js.map +1 -1
  87. package/dist/core/LoopLifecycle.d.ts.map +1 -1
  88. package/dist/core/LoopLifecycle.js +4 -9
  89. package/dist/core/LoopLifecycle.js.map +1 -1
  90. package/dist/core/LoopLifecycle.sql +7 -6
  91. package/dist/core/MessageResources.sql +31 -0
  92. package/dist/core/OperatorConfig.d.ts +1 -0
  93. package/dist/core/OperatorConfig.d.ts.map +1 -1
  94. package/dist/core/OperatorConfig.js +10 -2
  95. package/dist/core/OperatorConfig.js.map +1 -1
  96. package/dist/core/PacketBuilder.d.ts +5 -7
  97. package/dist/core/PacketBuilder.d.ts.map +1 -1
  98. package/dist/core/PacketBuilder.js +160 -158
  99. package/dist/core/PacketBuilder.js.map +1 -1
  100. package/dist/core/PacketBuilder.sql +20 -6
  101. package/dist/core/ProposalLifecycle.d.ts +1 -1
  102. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  103. package/dist/core/ProposalLifecycle.js +18 -20
  104. package/dist/core/ProposalLifecycle.js.map +1 -1
  105. package/dist/core/ProposalPolicies.d.ts +6 -0
  106. package/dist/core/ProposalPolicies.d.ts.map +1 -0
  107. package/dist/core/ProposalPolicies.js +28 -0
  108. package/dist/core/ProposalPolicies.js.map +1 -0
  109. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  110. package/dist/core/ProviderInstantiate.js +3 -4
  111. package/dist/core/ProviderInstantiate.js.map +1 -1
  112. package/dist/core/ReplyMetadata.d.ts +10 -0
  113. package/dist/core/ReplyMetadata.d.ts.map +1 -0
  114. package/dist/core/ReplyMetadata.js +20 -0
  115. package/dist/core/ReplyMetadata.js.map +1 -0
  116. package/dist/core/ResourceSelector.js +2 -2
  117. package/dist/core/ResourceSelector.js.map +1 -1
  118. package/dist/core/ResourceTransfers.js +3 -3
  119. package/dist/core/ResourceTransfers.js.map +1 -1
  120. package/dist/core/RuntimeWorker.sql +1 -2
  121. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  122. package/dist/core/SchemeRegistry.js +4 -5
  123. package/dist/core/SchemeRegistry.js.map +1 -1
  124. package/dist/core/ServiceHelp.d.ts +6 -0
  125. package/dist/core/ServiceHelp.d.ts.map +1 -0
  126. package/dist/core/ServiceHelp.js +35 -0
  127. package/dist/core/ServiceHelp.js.map +1 -0
  128. package/dist/core/StoredPacket.d.ts +1 -0
  129. package/dist/core/StoredPacket.d.ts.map +1 -1
  130. package/dist/core/StoredPacket.js +4 -1
  131. package/dist/core/StoredPacket.js.map +1 -1
  132. package/dist/core/StrikeRail.d.ts.map +1 -1
  133. package/dist/core/StrikeRail.js +2 -2
  134. package/dist/core/StrikeRail.js.map +1 -1
  135. package/dist/core/TokenCalibration.d.ts +2 -2
  136. package/dist/core/TokenCalibration.d.ts.map +1 -1
  137. package/dist/core/TokenCalibration.js +12 -16
  138. package/dist/core/TokenCalibration.js.map +1 -1
  139. package/dist/core/TokenCalibration.sql +29 -6
  140. package/dist/core/ToolInputSchema.d.ts +1 -0
  141. package/dist/core/ToolInputSchema.d.ts.map +1 -1
  142. package/dist/core/ToolInputSchema.js +8 -2
  143. package/dist/core/ToolInputSchema.js.map +1 -1
  144. package/dist/core/ToolResources.d.ts +1 -0
  145. package/dist/core/ToolResources.d.ts.map +1 -1
  146. package/dist/core/ToolResources.js +7 -1
  147. package/dist/core/ToolResources.js.map +1 -1
  148. package/dist/core/TurnDispositionHandler.d.ts +1 -2
  149. package/dist/core/TurnDispositionHandler.d.ts.map +1 -1
  150. package/dist/core/TurnDispositionHandler.js +31 -33
  151. package/dist/core/TurnDispositionHandler.js.map +1 -1
  152. package/dist/core/TurnMaterialization.d.ts.map +1 -1
  153. package/dist/core/TurnMaterialization.js +12 -2
  154. package/dist/core/TurnMaterialization.js.map +1 -1
  155. package/dist/core/TurnOps.d.ts +5 -1
  156. package/dist/core/TurnOps.d.ts.map +1 -1
  157. package/dist/core/TurnOps.js +29 -2
  158. package/dist/core/TurnOps.js.map +1 -1
  159. package/dist/core/TurnRunner.d.ts.map +1 -1
  160. package/dist/core/TurnRunner.js +184 -103
  161. package/dist/core/TurnRunner.js.map +1 -1
  162. package/dist/core/TurnSources.sql +2 -2
  163. package/dist/core/WorkerControlHandler.d.ts.map +1 -1
  164. package/dist/core/WorkerControlHandler.js +1 -8
  165. package/dist/core/WorkerControlHandler.js.map +1 -1
  166. package/dist/core/WorkerName.d.ts +1 -0
  167. package/dist/core/WorkerName.d.ts.map +1 -1
  168. package/dist/core/WorkerName.js +2 -0
  169. package/dist/core/WorkerName.js.map +1 -1
  170. package/dist/core/WorkerName.sql +2 -2
  171. package/dist/core/WorkerOwners.d.ts +13 -0
  172. package/dist/core/WorkerOwners.d.ts.map +1 -0
  173. package/dist/core/WorkerOwners.js +44 -0
  174. package/dist/core/WorkerOwners.js.map +1 -0
  175. package/dist/core/WorkerOwners.sql +55 -0
  176. package/dist/core/env-catalog.d.ts +3 -0
  177. package/dist/core/env-catalog.d.ts.map +1 -1
  178. package/dist/core/env-catalog.js +44 -9
  179. package/dist/core/env-catalog.js.map +1 -1
  180. package/dist/core/fork.sql +3 -4
  181. package/dist/core/packet-inject.js +0 -2
  182. package/dist/core/packet-inject.js.map +1 -1
  183. package/dist/core/packet-wire.d.ts +9 -11
  184. package/dist/core/packet-wire.d.ts.map +1 -1
  185. package/dist/core/packet-wire.js +103 -126
  186. package/dist/core/packet-wire.js.map +1 -1
  187. package/dist/core/plurnk-uri.d.ts +1 -1
  188. package/dist/core/plurnk-uri.d.ts.map +1 -1
  189. package/dist/core/plurnk-uri.js +4 -4
  190. package/dist/core/plurnk-uri.js.map +1 -1
  191. package/dist/core/recap-lines.d.ts +4 -0
  192. package/dist/core/recap-lines.d.ts.map +1 -0
  193. package/dist/core/recap-lines.js +11 -0
  194. package/dist/core/recap-lines.js.map +1 -0
  195. package/dist/core/scheme-types.d.ts +2 -1
  196. package/dist/core/scheme-types.d.ts.map +1 -1
  197. package/dist/core/sql-functions.d.ts +2 -0
  198. package/dist/core/sql-functions.d.ts.map +1 -0
  199. package/dist/core/sql-functions.js +5 -0
  200. package/dist/core/sql-functions.js.map +1 -0
  201. package/dist/core/turn-scheduler.d.ts.map +1 -1
  202. package/dist/core/turn-scheduler.js +1 -1
  203. package/dist/core/turn-scheduler.js.map +1 -1
  204. package/dist/core/types.d.ts +0 -1
  205. package/dist/core/types.d.ts.map +1 -1
  206. package/dist/core/unregistered-scheme.d.ts +1 -3
  207. package/dist/core/unregistered-scheme.d.ts.map +1 -1
  208. package/dist/core/unregistered-scheme.js +3 -11
  209. package/dist/core/unregistered-scheme.js.map +1 -1
  210. package/dist/evidence/EvidenceReader.d.ts.map +1 -1
  211. package/dist/evidence/EvidenceReader.js +18 -2
  212. package/dist/evidence/EvidenceReader.js.map +1 -1
  213. package/dist/evidence/digest.sql +2 -2
  214. package/dist/man/plurnk-service.1 +488 -0
  215. package/dist/schemes/EffectPolicy.d.ts.map +1 -1
  216. package/dist/schemes/EffectPolicy.js +1 -10
  217. package/dist/schemes/EffectPolicy.js.map +1 -1
  218. package/dist/schemes/Env.d.ts +26 -0
  219. package/dist/schemes/Env.d.ts.map +1 -0
  220. package/dist/schemes/Env.js +133 -0
  221. package/dist/schemes/Env.js.map +1 -0
  222. package/dist/schemes/Exec.d.ts.map +1 -1
  223. package/dist/schemes/Exec.js +30 -37
  224. package/dist/schemes/Exec.js.map +1 -1
  225. package/dist/schemes/ExecOutputScheme.d.ts +3 -2
  226. package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
  227. package/dist/schemes/ExecOutputScheme.js +9 -0
  228. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  229. package/dist/schemes/File.d.ts.map +1 -1
  230. package/dist/schemes/File.js +10 -16
  231. package/dist/schemes/File.js.map +1 -1
  232. package/dist/schemes/Log.d.ts.map +1 -1
  233. package/dist/schemes/Log.js +33 -10
  234. package/dist/schemes/Log.js.map +1 -1
  235. package/dist/schemes/Log.sql +9 -0
  236. package/dist/schemes/QuestionTool.d.ts.map +1 -1
  237. package/dist/schemes/QuestionTool.js +1 -3
  238. package/dist/schemes/QuestionTool.js.map +1 -1
  239. package/dist/schemes/Worker.d.ts.map +1 -1
  240. package/dist/schemes/Worker.js +6 -8
  241. package/dist/schemes/Worker.js.map +1 -1
  242. package/dist/schemes/_entry-find.d.ts +1 -1
  243. package/dist/schemes/_entry-find.d.ts.map +1 -1
  244. package/dist/schemes/_entry-find.js +16 -9
  245. package/dist/schemes/_entry-find.js.map +1 -1
  246. package/dist/schemes/_entry-fts.d.ts.map +1 -1
  247. package/dist/schemes/_entry-fts.js +3 -9
  248. package/dist/schemes/_entry-fts.js.map +1 -1
  249. package/dist/schemes/_entry-manifest.js +3 -3
  250. package/dist/schemes/_entry-manifest.js.map +1 -1
  251. package/dist/schemes/_entry-ops.js +1 -1
  252. package/dist/schemes/_entry-ops.js.map +1 -1
  253. package/dist/schemes/exec-abort.js +2 -2
  254. package/dist/schemes/exec-abort.js.map +1 -1
  255. package/dist/server/ConfigurationDiagnostics.d.ts +1 -0
  256. package/dist/server/ConfigurationDiagnostics.d.ts.map +1 -1
  257. package/dist/server/ConfigurationDiagnostics.js +9 -0
  258. package/dist/server/ConfigurationDiagnostics.js.map +1 -1
  259. package/dist/server/Daemon.d.ts +13 -8
  260. package/dist/server/Daemon.d.ts.map +1 -1
  261. package/dist/server/Daemon.js +57 -53
  262. package/dist/server/Daemon.js.map +1 -1
  263. package/dist/server/DrainSupervisor.d.ts +2 -4
  264. package/dist/server/DrainSupervisor.d.ts.map +1 -1
  265. package/dist/server/DrainSupervisor.js +5 -12
  266. package/dist/server/DrainSupervisor.js.map +1 -1
  267. package/dist/server/EnvFunctionality.d.ts +6 -6
  268. package/dist/server/EnvFunctionality.d.ts.map +1 -1
  269. package/dist/server/EnvFunctionality.js +7 -6
  270. package/dist/server/EnvFunctionality.js.map +1 -1
  271. package/dist/server/Functionality.d.ts.map +1 -1
  272. package/dist/server/Functionality.js +42 -13
  273. package/dist/server/Functionality.js.map +1 -1
  274. package/dist/server/FunctionalityManager.d.ts +3 -4
  275. package/dist/server/FunctionalityManager.d.ts.map +1 -1
  276. package/dist/server/FunctionalityManager.js +17 -9
  277. package/dist/server/FunctionalityManager.js.map +1 -1
  278. package/dist/server/HttpListener.d.ts.map +1 -1
  279. package/dist/server/HttpListener.js +9 -3
  280. package/dist/server/HttpListener.js.map +1 -1
  281. package/dist/server/MembersFunctionality.d.ts +2 -5
  282. package/dist/server/MembersFunctionality.d.ts.map +1 -1
  283. package/dist/server/MembersFunctionality.js +5 -3
  284. package/dist/server/MembersFunctionality.js.map +1 -1
  285. package/dist/server/WorkspaceStorage.d.ts +0 -1
  286. package/dist/server/WorkspaceStorage.d.ts.map +1 -1
  287. package/dist/server/WorkspaceStorage.js +1 -24
  288. package/dist/server/WorkspaceStorage.js.map +1 -1
  289. package/dist/server/client-input.d.ts +1 -2
  290. package/dist/server/client-input.d.ts.map +1 -1
  291. package/dist/server/client-input.js +13 -15
  292. package/dist/server/client-input.js.map +1 -1
  293. package/dist/server/dispatch-as-plurnk.js +1 -1
  294. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  295. package/dist/server/drain.sql +16 -15
  296. package/dist/server/envelope.d.ts +2 -1
  297. package/dist/server/envelope.d.ts.map +1 -1
  298. package/dist/server/envelope.js +7 -17
  299. package/dist/server/envelope.js.map +1 -1
  300. package/dist/server/envelope.sql +5 -5
  301. package/dist/server/seam-proposal-list.sql +6 -4
  302. package/dist/server/workspace-storage.sql +0 -5
  303. package/dist/service.d.ts.map +1 -1
  304. package/dist/service.js +28 -64
  305. package/dist/service.js.map +1 -1
  306. package/docs/env.md +22 -0
  307. package/migrations/003_loops.sql +2 -2
  308. package/migrations/006_log.sql +1 -1
  309. package/migrations/016_selection_statements.sql +1 -0
  310. package/migrations/017_skills_module.sql +1 -0
  311. package/migrations/018_worker_owners.sql +103 -0
  312. package/migrations/019_interactive_owners.sql +7 -0
  313. package/migrations/020_log_projection.sql +144 -0
  314. package/migrations/021_packet_roles.sql +86 -0
  315. package/migrations/022_message_completion.sql +24 -0
  316. package/package.json +41 -43
  317. package/dist/core/LegacyHome.d.ts +0 -10
  318. package/dist/core/LegacyHome.d.ts.map +0 -1
  319. package/dist/core/LegacyHome.js +0 -170
  320. package/dist/core/LegacyHome.js.map +0 -1
  321. package/dist/core/LoopPolicies.d.ts +0 -7
  322. package/dist/core/LoopPolicies.d.ts.map +0 -1
  323. package/dist/core/LoopPolicies.js +0 -42
  324. package/dist/core/LoopPolicies.js.map +0 -1
  325. package/dist/core/LoopPolicyReader.d.ts +0 -7
  326. package/dist/core/LoopPolicyReader.d.ts.map +0 -1
  327. package/dist/core/LoopPolicyReader.js +0 -33
  328. package/dist/core/LoopPolicyReader.js.map +0 -1
  329. package/dist/core/LoopPolicyReader.sql +0 -6
  330. package/dist/index.d.ts +0 -14
  331. package/dist/index.d.ts.map +0 -1
  332. package/dist/index.js +0 -21
  333. package/dist/index.js.map +0 -1
@@ -1,8 +1,8 @@
1
1
  var _a;
2
2
  import NativeContent from "./NativeContent.js";
3
- import { PlurnkParser } from "@plurnk/plurnk-parser";
4
- import { PathSyntax, PlurnkParseError, TurnDisposition, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
5
- import LoopPolicyReader from "./LoopPolicyReader.js";
3
+ import { PlurnkParser, parsePath } from "@plurnk/plurnk-parser";
4
+ import { PathSyntax, PlurnkParseError, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
5
+ import WorkerOwners from "./WorkerOwners.js";
6
6
  import { setTimeout as delay } from "node:timers/promises";
7
7
  import { aggregateProviderAccounting } from "@plurnk/plurnk-providers";
8
8
  const comparePosition = (a, b) => a.line - b.line || a.column - b.column;
@@ -12,20 +12,16 @@ import GitMembership from "./git-membership.js";
12
12
  import { acceptedKinds } from "./attachments.js";
13
13
  import GitState from "./git-state.js";
14
14
  import WorkspaceSettings from "./workspace-settings.js";
15
- import { contentWeight } from "./content-weight.js";
16
15
  import { generatedPathname } from "./plurnk-uri.js";
16
+ import { contentWeight } from "./content-weight.js";
17
17
  import { readFile } from "node:fs/promises";
18
18
  import { resolve as resolvePath } from "node:path";
19
19
  import { homedir } from "node:os";
20
- // {§operator-grammar} — an operator's own GBNF is a file path: absolute, `~`-relative, or
21
- // relative to the daemon's working directory. The service ships no grammar profile, so a bare
22
- // name (no separator) is refused by name rather than resolved against anything.
20
+ // {§operator-grammar}
23
21
  export const resolveOperatorGrammarPath = (value) => {
24
22
  if (value === "~" || value.startsWith("~/"))
25
23
  return resolvePath(homedir(), value.slice(2));
26
- if (value.startsWith("/") || value.startsWith(".") || value.includes("/"))
27
- return resolvePath(value);
28
- throw new Error(`PLURNK_PROVIDERS_GBNF=${value} names a bundled grammar profile; the service ships none (#588). Give the path of a grammar file you wrote.`);
24
+ return resolvePath(value);
29
25
  };
30
26
  // Shared module imported by both Engine and the digest, so wire
31
27
  // projection and digest projection are structurally one function — no
@@ -35,7 +31,6 @@ import { RECEIPT_RESERVE, reserved } from "./ContextFit.js";
35
31
  import Results, { OperationFailureError } from "./results.js";
36
32
  import Turn from "./Turn.js";
37
33
  import ProviderRecovery from "./ProviderRecovery.js";
38
- import PacketBuilder from "./PacketBuilder.js";
39
34
  import StoredPacket from "./StoredPacket.js";
40
35
  import { observed, observedSync } from "../observe/spans.js";
41
36
  import { genAiRequestName, genAiRequestOptions, settleGenAiResponse } from "../observe/genai.js";
@@ -43,7 +38,6 @@ import { PROVIDER_CALLS, recordCounter } from "../observe/metrics.js";
43
38
  import ModelCall, { ModelCallPersistenceError, ProviderAccountingIntegrityError } from "./ModelCall.js";
44
39
  import WorkerName from "./WorkerName.js";
45
40
  import TurnOps from "./TurnOps.js";
46
- import CapabilityPolicies from "./CapabilityPolicies.js";
47
41
  import CapabilityResolver from "./CapabilityResolver.js";
48
42
  const regexLiteral = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
49
43
  const workerCatalogTarget = (namespace) => {
@@ -103,6 +97,24 @@ const windowOverflowFailure = (overflow) => Results.failure("engine:context", "w
103
97
  // {§context-over-budget-row} — the row is the mandate; the gauge is the one home for the numbers
104
98
  // ({§context-gauge}), which the row's own charge would otherwise put out of agreement.
105
99
  const OVER_BUDGET_DETAIL = "Context exceeds budget. YOU MUST ONLY KILL, MOVE or NOTE this turn.";
100
+ // {§worker-initialization-entry} — the one NOTE that leads the initialization surveys.
101
+ const INITIALIZATION_NOTE = {
102
+ op: "NOTE", aside: null, metadata: null, target: null, lineMarker: null,
103
+ body: "Surveyed tooling, documentation, extended context, and project root.", position: UNKNOWN_POSITION,
104
+ };
105
+ // {§continued-without-note} — said once, in the next packet, of a continuing turn that carried no NOTE.
106
+ const CONTINUED_WITHOUT_NOTE = "The turn continued without a NOTE.";
107
+ // Trial (#1073): a continuing turn that carried NOTEs and no other operation is told so under the same
108
+ // per-turn rule; it is a notice, never a strike.
109
+ const NOTED_WITHOUT_OPERATION = "The turn carried NOTEs and no other operation.";
110
+ // {§notice-directive} — what each notice of the model's own output asks of it: MUST where the card says MUST
111
+ // or the output was refused, SHOULD where a near-miss was accepted.
112
+ const DIRECTIVE = Object.freeze({
113
+ note: "YOU MUST emit at least one NOTE operation with a complete status summary and at least one other operation per turn.",
114
+ emission: "YOU MUST ONLY emit fenced operations; with the parameters, pattern, and terse aside on the fenced Operation line.",
115
+ advisory: "YOU SHOULD write the canonical form.",
116
+ output: "YOU SHOULD emit fewer or shorter operations per turn.",
117
+ });
106
118
  const overBudgetFailure = () => Results.failure("engine:context", "packet-exceeds-budget", 413, OVER_BUDGET_DETAIL);
107
119
  const INVALID_EMISSION_RECOVERY_MESSAGE = "Response rejected before dispatch; no operations were performed.";
108
120
  // {§output-allowance-notice} — THE single derivation of a ceiling cut: a
@@ -111,7 +123,7 @@ const INVALID_EMISSION_RECOVERY_MESSAGE = "Response rejected before dispatch; no
111
123
  // path. Null means the emission was not cut by the allowance.
112
124
  const allowanceCutMessage = (finishReason, grant) => finishReason !== "length"
113
125
  ? null
114
- : `emission truncated at the output allowance${grant === null ? "" : ` (${grant} tokens)`}`;
126
+ : `Emission truncated at the output allowance${grant === null ? "" : ` (${grant} tokens)`}`;
115
127
  const readEmissionAttempts = () => Knob.integer("PLURNK_SERVICE_EMISSION_ATTEMPTS", 1);
116
128
  // The wall's abort reason — runLoop branches a mid-turn teardown to the 504 terminal on it.
117
129
  export const LOOP_TIMEOUT_REASON = "loop_timeout";
@@ -270,18 +282,15 @@ class TurnRunner {
270
282
  // {§packet-attachment-parts}: native parts come from the READ's immutable snapshot,
271
283
  // never from a source that may have changed since the observation.
272
284
  async #wireMessages(packet, ctx, provider) {
273
- // {§packet-wire-envelope} — each emission row the packet placed carries its emission as the
274
- // worker's own assistant message.
275
- const emissions = this.#packets.emissionsFor(packet);
276
285
  const accepted = acceptedKinds(provider.inputModalities);
277
286
  if (accepted.length === 0 || !(packet.attachments ?? []).some((attachment) => accepted.includes(attachment.kind))) {
278
287
  return {
279
- messages: PacketWire.packetToWireMessages(packet, emissions),
288
+ messages: PacketWire.packetToWireMessages(packet),
280
289
  nativeInputs: [],
281
290
  };
282
291
  }
283
292
  const nativeInputs = new Set();
284
- const messages = await PacketWire.wireMessages(packet, emissions, async (attachment) => {
293
+ const messages = await PacketWire.wireMessages(packet, async (attachment) => {
285
294
  const bytes = await NativeContent.read(ctx.db, attachment.contentHash);
286
295
  nativeInputs.add(attachment.coordinate);
287
296
  return bytes;
@@ -300,8 +309,6 @@ class TurnRunner {
300
309
  const createdTurnIds = [];
301
310
  const turnSignal = this.#loopSignal(loopId) ?? signal;
302
311
  try {
303
- // {§configuration-repair-path} — a retired packet knob refuses inference before any turn opens.
304
- PacketBuilder.assertConfiguration();
305
312
  const container = await this.#openTurnContainer(args, createdTurnIds);
306
313
  const gitStatus = await this.#deriveWorkspace(args, container.systemCtx);
307
314
  if (container.initializationTurn !== null)
@@ -312,7 +319,7 @@ class TurnRunner {
312
319
  if (admitted.overflow !== null)
313
320
  return await this.#failCuration(admitted.request, admitted.overflow);
314
321
  const request = admitted.request;
315
- const attempts = await this.#prepareProviderAttempts(args, request);
322
+ const attempts = await this.#prepareProviderAttempts(args, request, admitted.wire);
316
323
  let emission;
317
324
  try {
318
325
  emission = await this.#attemptProvider(args, request, attempts);
@@ -376,7 +383,7 @@ class TurnRunner {
376
383
  createdTurnIds.push(initializationTurn.id);
377
384
  const initializationPolicies = initializationTurn === null
378
385
  ? []
379
- : (await CapabilityPolicies.layers(this.#db, workspaceId, loopId)).map((layer) => layer.policy);
386
+ : (await this.#capabilities.layers(workspaceId, workerId)).map((layer) => layer.policy);
380
387
  const modelTurn = initializationTurn === null
381
388
  ? await Turn.open(this.#db, { loopId, producer: "model", kind: "inference" })
382
389
  : null;
@@ -495,9 +502,14 @@ class TurnRunner {
495
502
  await Turn.complete(this.#db, initializationTurn.id, 200);
496
503
  return;
497
504
  }
498
- const program = TurnOps.renderInternal(admittedInitializationStatements);
505
+ // {§worker-initialization-entry} — the example turn keeps the NOTE contract the card teaches:
506
+ // when the surveys run, one NOTE leads them.
507
+ const programStatements = admittedInitializationStatements.some((statement) => statement.op === "FIND")
508
+ ? [INITIALIZATION_NOTE, ...admittedInitializationStatements]
509
+ : admittedInitializationStatements;
510
+ const program = TurnOps.renderInternal(programStatements);
499
511
  const admitted = PlurnkParser.parseStatements(program).items.flatMap((item) => item.kind === "statement" ? [item.statement] : []);
500
- if (admitted.length !== admittedInitializationStatements.length) {
512
+ if (admitted.length !== programStatements.length) {
501
513
  throw new Error("initialization program did not preserve every authored operation");
502
514
  }
503
515
  await Turn.recordSource(this.#db, initializationTurn.id, "ops", program);
@@ -512,7 +524,7 @@ class TurnRunner {
512
524
  turnId: initializationTurn.id,
513
525
  fromSequence: 1,
514
526
  failOnOperationError: true,
515
- fit: this.#fitFor(args, { turnId: initializationTurn.id, seq: initializationTurn.sequence, gitStatus, notices: [], transientOpenLogEntryId: container.transientOpenLogEntryId, bodiless: new Set() }),
527
+ fit: this.#fitFor(args, { turnId: initializationTurn.id, seq: initializationTurn.sequence, gitStatus, notices: [], transientOpenLogEntryId: container.transientOpenLogEntryId, projection: { bodiless: new Set(), omitEmissionHistory: false } }),
516
528
  signal: this.#loopSignal(loopId),
517
529
  onDispatch,
518
530
  onSettled,
@@ -524,11 +536,18 @@ class TurnRunner {
524
536
  // {§actor-boundary-catalog-preview} — the opening surveys, each admitted only when
525
537
  // its scheme is registered for the workspace. A positive filesItems caps the
526
538
  // project rows.
527
- async #catalogSurveys({ workspaceId }, { workerName, initializationPolicies }, filesItems) {
539
+ async #catalogSurveys({ workspaceId }, { workerName, initializationPolicies, loopSequence }, filesItems) {
528
540
  const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
529
541
  const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
530
542
  const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
531
543
  const surveys = [
544
+ {
545
+ // Trial (#1073): the survey reads its own program, so the log carries one program verbatim
546
+ // and shows that every ops:// turn is readable.
547
+ op: "READ", aside: "this program",
548
+ target: { kind: "url", raw: `ops://${workerName}/${loopSequence}/1`, scheme: "ops", username: null, password: null, hostname: workerName, port: null, pathname: `/${loopSequence}/1`, query: null, fragment: null },
549
+ metadata: null, lineMarker: null, matcher: null, body: null, position: UNKNOWN_POSITION,
550
+ },
532
551
  {
533
552
  op: "FIND", aside: null,
534
553
  target: { kind: "url", raw: "skill://*/SKILL.md", scheme: "skill", username: null, password: null, hostname: "*", port: null, pathname: "/SKILL.md", query: null, fragment: null },
@@ -638,7 +657,7 @@ class TurnRunner {
638
657
  const systemCtx = this.#schemeContext(args, turnId);
639
658
  // {§context-fit} — one measure for everything this turn lands: the budget less the packet as it
640
659
  // would render now. The drained notices join the same facts below.
641
- const facts = { turnId, seq, gitStatus, notices: [], transientOpenLogEntryId: container.transientOpenLogEntryId, bodiless: new Set() };
660
+ const facts = { turnId, seq, gitStatus, notices: [], transientOpenLogEntryId: container.transientOpenLogEntryId, projection: { bodiless: new Set(), omitEmissionHistory: false } };
642
661
  const fit = this.#fitFor(args, facts);
643
662
  const messages = await this.#publishMessages(args, turnId, fit);
644
663
  let nextActionIndex = await this.#readOpenPaths(args, turnId, messages.openPaths, messages.nextActionIndex, fit);
@@ -699,20 +718,20 @@ class TurnRunner {
699
718
  }
700
719
  return { nextActionIndex, openPaths };
701
720
  }
702
- // {§methods-loop-run-open-paths}: selected workspace paths belong to the message.
721
+ // {§methods-loop-run-open-paths}: selected resources belong to the message.
703
722
  // Publish it, then dispatch ordinary core READs in that same turn;
704
723
  // missing/non-member paths retain their normal 4xx. Returns the next action index.
705
724
  async #readOpenPaths({ workspaceId, workerId, loopId, onDispatch, onSettled }, turnId, openPaths, fromSequence, fit) {
706
725
  let nextActionIndex = fromSequence;
707
726
  for (const [index, raw] of openPaths.entries()) {
708
- const fileRead = {
727
+ const resourceRead = {
709
728
  op: "READ", aside: null, lineMarker: null, matcher: null,
710
- target: { kind: "local", raw },
729
+ target: parsePath(PathSyntax.escapeTarget(raw)),
711
730
  metadata: null,
712
731
  body: null, position: UNKNOWN_POSITION,
713
732
  };
714
733
  await this.#dispatch({
715
- statement: fileRead, workspaceId, workerId, loopId, turnId,
734
+ statement: resourceRead, workspaceId, workerId, loopId, turnId,
716
735
  sequence: nextActionIndex, origin: "_plurnk", onDispatch, onSettled,
717
736
  fit: reserved(fit, openPaths.length - index - 1),
718
737
  });
@@ -720,8 +739,9 @@ class TurnRunner {
720
739
  }
721
740
  return nextActionIndex;
722
741
  }
723
- #buildPacket({ messages, recap, workspaceId, workerId, loopId, provider }, facts) {
724
- return this.#packets.buildRequestPacket({
742
+ async #buildPacket(args, facts) {
743
+ const { messages, recap, workspaceId, workerId, loopId, provider } = args;
744
+ const packet = await this.#packets.buildRequestPacket({
725
745
  initialMessages: messages,
726
746
  recap,
727
747
  workspaceId,
@@ -733,8 +753,16 @@ class TurnRunner {
733
753
  notices: facts.notices,
734
754
  transientOpenLogEntryId: facts.transientOpenLogEntryId,
735
755
  turnId: facts.turnId,
736
- bodiless: facts.bodiless,
756
+ bodiless: facts.projection.bodiless,
757
+ omitEmissionHistory: facts.projection.omitEmissionHistory,
737
758
  });
759
+ // {§emission-history}: all builds, including result-admission probes, shed the optional
760
+ // replay before taking result bodies. Share the decision across this request's rebuilds.
761
+ if (PacketWire.sectionContent(packet, "emission-history").length > 0 && this.#packets.windowOverflow(packet, provider) !== null) {
762
+ facts.projection.omitEmissionHistory = true;
763
+ return this.#buildPacket(args, facts);
764
+ }
765
+ return packet;
738
766
  }
739
767
  // {§context-fit} — the budget left for one more row, measured by building the packet as it would
740
768
  // render now: every row landed so far, every KILL already honoured. Null without a budget.
@@ -760,41 +788,61 @@ class TurnRunner {
760
788
  const packet = await this.#buildPacket(args, request);
761
789
  return { ...request, nextActionIndex: request.nextActionIndex + 1, packet };
762
790
  }
763
- // The packet against the wall and the budget: the newest rows go bodiless until it fits the window
764
- // ({§context-own-rows-fit}); a packet still over budget then says so in a row ({§context-over-budget-row}),
765
- // whose own weight takes the same wall. The overflow is returned only when the packet cannot fit even
766
- // as receipts.
791
+ // {§context-admission} — one request through the room: over the wall by the estimate, it sheds; over
792
+ // budget, it carries one row whose own weight takes the same wall ({§context-over-budget-row}); then the
793
+ // provider assesses the exact wire request, and an exact refusal sheds by its excess
794
+ // ({§context-wall-measure}). Only a packet that cannot fit even as receipts returns its overflow.
767
795
  async #admitPacket(args, request) {
768
- const fitted = await this.#fitOwnRows(args, request);
769
- if (fitted.overflow !== null)
770
- return fitted;
771
- if (this.#packets.curationOverflow(fitted.request.packet) === null)
772
- return fitted;
773
- return await this.#fitOwnRows(args, await this.#noteOverBudget(args, fitted.request));
774
- }
775
- // {§context-own-rows-fit} — while the packet is over the wall ({§context-wall}), the newest rows still
776
- // carrying a body are taken, enough of them by their rendered tokens to shed the excess, and the packet
777
- // is rebuilt with them bodiless; every body stays stored. The rebuilt packet is measured again until
778
- // it fits, or until no row is left to take.
779
- async #fitOwnRows(args, request) {
796
+ const { provider, loopId, signal } = args;
780
797
  let current = request;
798
+ let noted = false;
781
799
  for (;;) {
782
- const overflow = this.#packets.windowOverflow(current.packet, args.provider);
783
- if (overflow === null)
784
- return { request: current, overflow: null };
800
+ const estimated = this.#packets.windowOverflow(current.packet, provider);
801
+ if (estimated !== null) {
802
+ const shed = await this.#shed(args, current, estimated);
803
+ if (shed === null)
804
+ return { request: current, wire: null, overflow: estimated };
805
+ current = shed;
806
+ continue;
807
+ }
808
+ if (!noted && this.#packets.curationOverflow(current.packet) !== null) {
809
+ current = await this.#noteOverBudget(args, current);
810
+ noted = true;
811
+ continue;
812
+ }
813
+ const wire = await this.#wireMessages(current.packet, current.systemCtx, provider);
814
+ const capacity = await provider.assessRequestCapacity(wire.messages, undefined, this.#loopSignal(loopId) ?? signal);
815
+ const refused = this.#packets.exactOverflow(current.packet, capacity);
816
+ if (refused === null)
817
+ return { request: current, wire, overflow: null };
818
+ const shed = await this.#shed(args, current, refused);
819
+ if (shed === null)
820
+ return { request: current, wire: null, overflow: refused };
821
+ current = shed;
822
+ }
823
+ }
824
+ // {§context-own-rows-fit} — one shedding step for whichever measure found the request over the wall: the
825
+ // previous program goes whole first ({§emission-history}), then the newest rows still carrying a body,
826
+ // enough of them by their rendered tokens to shed the excess; every body stays stored. Null when nothing
827
+ // is left to take.
828
+ async #shed(args, request, overflow) {
829
+ if (!request.projection.omitEmissionHistory && PacketWire.sectionContent(request.packet, "emission-history").length > 0) {
830
+ request.projection.omitEmissionHistory = true;
831
+ }
832
+ else {
785
833
  let shed = 0;
786
834
  let taken = 0;
787
- for (const { id, tokens } of this.#packets.bodiedRowsOf(current.packet).toReversed()) {
835
+ for (const { id, tokens } of this.#packets.bodiedRowsOf(request.packet).toReversed()) {
788
836
  if (shed >= overflow.excessWeight)
789
837
  break;
790
- current.bodiless.add(id);
838
+ request.projection.bodiless.add(id);
791
839
  shed += tokens;
792
840
  taken += 1;
793
841
  }
794
842
  if (taken === 0)
795
- return { request: current, overflow };
796
- current = { ...current, packet: await this.#buildPacket(args, current) };
843
+ return null;
797
844
  }
845
+ return { ...request, packet: await this.#buildPacket(args, request) };
798
846
  }
799
847
  // {§context-wall} — the packet cannot fit the window even as receipts: the turn completes on the
800
848
  // overflow and the loop rules the terminal; no provider I/O.
@@ -805,8 +853,7 @@ class TurnRunner {
805
853
  }
806
854
  // Phase 4's bookkeeping: the wire request, the recovery knobs, the signal the
807
855
  // provider sees, the client id and the worker's provider identity.
808
- async #prepareProviderAttempts({ provider, workspaceId, workerId, loopId, signal }, request) {
809
- const wire = await this.#wireMessages(request.packet, request.systemCtx, provider);
856
+ async #prepareProviderAttempts({ workerId, loopId, signal }, request, wire) {
810
857
  // {§provider-recovery} — this turn's recovery clock: the first recoverable provider
811
858
  // failure starts it; the budget and backoff are the operator's.
812
859
  const recoveryBudget = ProviderRecovery.budget();
@@ -838,9 +885,8 @@ class TurnRunner {
838
885
  };
839
886
  }
840
887
  // Phase 4 — the provider attempt loop. Each iteration is one logical call: a
841
- // recoverable failure or a capacity rebuild re-issues the same emission attempt,
842
- // an invalid emission spends one, and a valid emission ends the loop
843
- // ({§invalid-emission-attempts}).
888
+ // recoverable failure re-issues the same emission attempt, an invalid emission
889
+ // spends one, and a valid emission ends the loop ({§invalid-emission-attempts}).
844
890
  async #attemptProvider(args, request, attempts) {
845
891
  const { provider, workspaceId, workerId, loopId, signal } = args;
846
892
  // {§turn-lifecycle}: bracket the complete provider-attempt window with liveness notices.
@@ -899,14 +945,13 @@ class TurnRunner {
899
945
  return emission;
900
946
  }
901
947
  // One logical provider call: its durable model call and attempt row, the exchange
902
- // under the reasoning observer, and its classification. Capacity recovery and
903
- // {§provider-recovery} re-issue the same emission attempt; the parser's admission
904
- // is the one verdict on a completed exchange.
948
+ // under the reasoning observer, and its classification. {§provider-recovery}
949
+ // re-issues the same emission attempt; the parser's admission is the one verdict
950
+ // on a completed exchange.
905
951
  async #issueProviderCall(args, request, attempts, attempt, strikeStreak) {
906
952
  const { provider, workspaceId, workerId, loopId } = args;
907
- // Capacity recovery may rebuild and resend the request without
908
- // consuming a grammar-emission attempt. Every logical provider
909
- // call still receives its own durable sequence and accounting.
953
+ // A recovery re-issue does not consume a grammar-emission attempt. Every
954
+ // logical provider call still receives its own durable sequence and accounting.
910
955
  attempts.currentEmissionAttempt = attempt;
911
956
  attempts.modelCallSequence++;
912
957
  const attributionContext = Object.freeze({
@@ -945,8 +990,8 @@ class TurnRunner {
945
990
  await this.#recoverProviderFailure(args, request, attempts, modelCall, attemptRow.id, error);
946
991
  return "reissued";
947
992
  }
948
- // {§context-wall} — a provider capacity rejection after the provider's own retries is this
949
- // turn's failure; nothing is withheld to make the request smaller.
993
+ // {§context-wall-measure} — admission already shed for an exact refusal of this wire request;
994
+ // an upstream refusal is the provider's own 413.
950
995
  throw error;
951
996
  }
952
997
  finally {
@@ -1181,14 +1226,12 @@ class TurnRunner {
1181
1226
  if (attempts.parked) {
1182
1227
  // {§provider-recovery} — the recovery budget is spent: the loop parks exactly like a
1183
1228
  // [202] wait and resumes on the next prompt or wake; the failure stays durable.
1184
- // {§loop-attendance} — unattended, nothing will wake it, so LoopDriver concludes instead
1185
- // and the notice says that rather than promising a resumption nobody can deliver.
1186
- const { attended } = await LoopPolicyReader.read(this.#db, loopId);
1229
+ const recoverable = await WorkerOwners.interactive(this.#db, loopId);
1187
1230
  this.#notices.push(workspaceId, workerId, loopId, {
1188
1231
  source: "engine:provider",
1189
1232
  kind: "provider_unavailable",
1190
1233
  level: "error",
1191
- message: `${recorded.result.problem?.title ?? "Provider failure"}: the ${Math.round(attempts.recoveryBudget / 1000)}s recovery budget is spent; ${attended ? "the loop is parked and resumes on the next prompt or wake" : "this run is unattended, so the loop ends here"}.`,
1234
+ message: `${recorded.result.problem?.title ?? "Provider failure"}: the ${Math.round(attempts.recoveryBudget / 1000)}s recovery budget is spent; ${recoverable ? "the loop is parked and resumes on the next prompt or wake" : "nobody attends the worker's owner, so the loop ends here"}.`,
1192
1235
  });
1193
1236
  return turnResult(request, 202, { providerParked: true, providerFailure: recorded.result, emissionAttempts });
1194
1237
  }
@@ -1243,7 +1286,8 @@ class TurnRunner {
1243
1286
  source: "engine:capacity",
1244
1287
  kind: "output_truncated",
1245
1288
  level: "error",
1246
- message: `${cut}; no operations were performed`,
1289
+ directive: DIRECTIVE.output,
1290
+ message: `${cut}; no operations were performed.`,
1247
1291
  });
1248
1292
  }
1249
1293
  else {
@@ -1251,6 +1295,7 @@ class TurnRunner {
1251
1295
  source: "engine:grammar",
1252
1296
  kind: "invalid_emission",
1253
1297
  level: "error",
1298
+ directive: DIRECTIVE.emission,
1254
1299
  message: diagnostic === undefined
1255
1300
  ? INVALID_EMISSION_RECOVERY_MESSAGE
1256
1301
  : `${INVALID_EMISSION_RECOVERY_MESSAGE} Parser: ${diagnostic.message}`,
@@ -1286,7 +1331,8 @@ class TurnRunner {
1286
1331
  source: "engine:turn",
1287
1332
  kind: "output_truncated",
1288
1333
  level: "warn",
1289
- message: `${allowanceCut}; no authored operations were performed`,
1334
+ directive: DIRECTIVE.output,
1335
+ message: `${allowanceCut}; no authored operations were performed.`,
1290
1336
  });
1291
1337
  }
1292
1338
  // Non-fatal provider transport notices on an accepted turn. Forward each
@@ -1300,6 +1346,7 @@ class TurnRunner {
1300
1346
  this.#notices.push(workspaceId, workerId, loopId, {
1301
1347
  source: notice.source,
1302
1348
  kind: notice.kind,
1349
+ ...(notice.directive === undefined ? {} : { directive: notice.directive }),
1303
1350
  message: notice.message ?? "",
1304
1351
  level: notice.level,
1305
1352
  ...(located !== null
@@ -1311,27 +1358,19 @@ class TurnRunner {
1311
1358
  this.#notices.push(workspaceId, workerId, loopId, {
1312
1359
  source: "engine:capacity",
1313
1360
  kind: "output_truncated",
1314
- message: allowanceCut,
1361
+ directive: DIRECTIVE.output,
1362
+ message: `${allowanceCut}.`,
1315
1363
  level: "warn",
1316
1364
  });
1317
1365
  }
1318
- // {§outside-text} — text outside the operations is the turn's own source, never a log row
1319
- // or a packet field; the model reads only its weight, the client reads its text once.
1366
+ // {§outside-text} — a turn that fenced no operation keeps its text as its outside source: the
1367
+ // evidence behind the empty turn's strike and a terminal's citation; no row, notice or NOTE.
1320
1368
  if (split.outside !== null) {
1321
- await Turn.recordSource(this.#db, request.turnId, "outside", split.outside.text, { modelCallId: emission.modelCallId });
1322
- this.#notices.push(workspaceId, workerId, loopId, {
1323
- source: "engine:turn",
1324
- kind: "outside_text",
1325
- level: "warn",
1326
- message: `${split.outside.tokens} tokens emitted outside OPs. Discarded.`,
1327
- });
1369
+ await Turn.recordSource(this.#db, request.turnId, "outside", split.outside, { modelCallId: emission.modelCallId });
1328
1370
  this.#notify.outsideEventNotify?.(workspaceId, {
1329
- workerId,
1330
- loopId,
1331
- turnId: request.turnId,
1371
+ workerId, loopId, turnId: request.turnId,
1332
1372
  coordinate: `${request.workerName}-${request.loopSeq}-${request.seq}`,
1333
- text: split.outside.text,
1334
- tokens: split.outside.tokens,
1373
+ text: split.outside, tokens: contentWeight(split.outside),
1335
1374
  });
1336
1375
  }
1337
1376
  // {§operator-grammar} — transport evidence only: the turn records whether the operator's
@@ -1387,7 +1426,7 @@ class TurnRunner {
1387
1426
  fit: request.fit,
1388
1427
  recoverableParseErrors: split.recoverableParseErrors,
1389
1428
  emptyTurn: split.emptyTurn,
1390
- finalResponse: split.finalResponse,
1429
+ completionAllowed: split.completionAllowed,
1391
1430
  bare: {
1392
1431
  provider: childProvider,
1393
1432
  loopSequence: request.loopSeq,
@@ -1398,6 +1437,22 @@ class TurnRunner {
1398
1437
  onDispatch,
1399
1438
  onSettled,
1400
1439
  });
1440
+ // {§continued-without-note} — a turn the loop continues past or parks on, having carried no NOTE
1441
+ // forward in its program or from its reasoning, is told so in the next packet. An empty turn has
1442
+ // its own no-operation row instead.
1443
+ const ops = split.packetAssistant.ops;
1444
+ if ((executed.status === 102 || executed.status === 202) && ops.length > 0
1445
+ && !ops.some((statement) => "op" in statement && statement.op === "NOTE")) {
1446
+ this.#notices.push(workspaceId, workerId, loopId, {
1447
+ source: "engine:turn", kind: "continued_without_note", level: "warn", directive: DIRECTIVE.note, message: CONTINUED_WITHOUT_NOTE,
1448
+ });
1449
+ }
1450
+ const annotationOnly = ops.length > 0 && ops.every((statement) => "op" in statement && statement.op === "NOTE");
1451
+ if ((executed.status === 102 || executed.status === 202) && annotationOnly) {
1452
+ this.#notices.push(workspaceId, workerId, loopId, {
1453
+ source: "engine:turn", kind: "noted_without_operation", level: "warn", directive: DIRECTIVE.note, message: NOTED_WITHOUT_OPERATION,
1454
+ });
1455
+ }
1401
1456
  return turnResult(request, executed.status, {
1402
1457
  outcomes: executed.outcomes,
1403
1458
  progressed: executed.progressed,
@@ -1460,10 +1515,9 @@ class TurnRunner {
1460
1515
  const parseErrors = [];
1461
1516
  let contentStatementCount = 0;
1462
1517
  let hasUnparsedTail = false;
1518
+ let operationOmitted = false;
1463
1519
  const parseNotices = [];
1464
1520
  const fabrications = [];
1465
- // {§outside-text}: every span outside an operation, verbatim and in source order; never an
1466
- // operation of its own, so a repetitive or length-cut response stays one source.
1467
1521
  const outside = [];
1468
1522
  if (preParsedOps !== undefined) {
1469
1523
  ops.push(...preParsedOps);
@@ -1479,13 +1533,33 @@ class TurnRunner {
1479
1533
  span.setAttribute("statements", result.items.filter((item) => item.kind === "statement").length);
1480
1534
  return result;
1481
1535
  });
1536
+ // {§outside-text}: beside a fenced operation, the text between two operations is one NOTE at its
1537
+ // position, annotation the model wrote without its fence; a turn that fenced no operation keeps
1538
+ // its text as its outside source and stays an empty turn ({§empty-turn}). Echoed emission
1539
+ // headings are announcements, not annotation, and are dropped ({§fabricated-log-entry}).
1540
+ const fenced = parsed.items.filter((item) => item.kind === "statement").length;
1541
+ let span = null;
1542
+ const flushSpan = () => {
1543
+ if (span === null)
1544
+ return;
1545
+ const body = span.body.join("\n\n");
1546
+ if (body.length > 0) {
1547
+ if (fenced > 0) {
1548
+ ops.push({ op: "NOTE", aside: null, metadata: null, target: null, lineMarker: null, body, position: span.position });
1549
+ contentStatementCount += 1;
1550
+ }
1551
+ else
1552
+ outside.push(body);
1553
+ }
1554
+ span = null;
1555
+ };
1482
1556
  for (const item of parsed.items) {
1557
+ if (item.kind !== "text")
1558
+ flushSpan();
1483
1559
  if (item.kind === "statement") {
1484
1560
  ops.push(item.statement);
1485
1561
  contentStatementCount += 1;
1486
1562
  }
1487
- // Operation boundaries stay the parser's responsibility; what falls outside them is
1488
- // stored as the turn's outside source and counted as nothing.
1489
1563
  else if (item.kind === "text") {
1490
1564
  // {§fabricated-log-entry}: a log-entry heading in outside text rejects the attempt.
1491
1565
  const fabricated = FabricatedLog.find(item.content);
@@ -1493,16 +1567,22 @@ class TurnRunner {
1493
1567
  fabrications.push({ message: FabricatedLog.message(fabricated.heading), line: item.position.line + fabricated.line, column: 1, source: "harness" });
1494
1568
  continue;
1495
1569
  }
1496
- outside.push(item.content.trim());
1570
+ const body = FabricatedLog.stripEchoes(item.content);
1571
+ if (body.length === 0)
1572
+ continue;
1573
+ span ??= { body: [], position: item.position };
1574
+ span.body.push(body);
1497
1575
  }
1498
1576
  else if (item.kind === "error") {
1499
1577
  const err = item.error;
1500
1578
  if (err instanceof PlurnkParseError) {
1579
+ operationOmitted ||= err.operationOmitted;
1501
1580
  if (err.severity === "warning") {
1502
1581
  parseNotices.push({
1503
1582
  source: "grammar",
1504
1583
  kind: "parse_advisory",
1505
1584
  level: "warn",
1585
+ directive: DIRECTIVE.advisory,
1506
1586
  message: err.message,
1507
1587
  position: {
1508
1588
  type: "content-offset",
@@ -1522,6 +1602,7 @@ class TurnRunner {
1522
1602
  }
1523
1603
  }
1524
1604
  }
1605
+ flushSpan();
1525
1606
  // {§unparsed-tail-boundary} — the lexer's one boundary fact: from `unparsedTail.from` on,
1526
1607
  // nothing was read. The statements that closed before it are ordinary facts and run; the
1527
1608
  // loss itself is one more hard diagnostic, a failed row the model sees. An emission that
@@ -1538,6 +1619,7 @@ class TurnRunner {
1538
1619
  source: "grammar:reasoning",
1539
1620
  kind: "parse_advisory",
1540
1621
  level: "warn",
1622
+ directive: DIRECTIVE.advisory,
1541
1623
  message: warning.message,
1542
1624
  parserSource: warning.source,
1543
1625
  });
@@ -1554,9 +1636,9 @@ class TurnRunner {
1554
1636
  parseErrors.unshift(...fabrications.slice(0, 1));
1555
1637
  const emissionStatements = preParsedOps === undefined && contentStatementCount > 0 ? [...ops] : null;
1556
1638
  ops.unshift(...reasoningOps);
1557
- const finalResponse = TurnDisposition.requestsCompletion(ops)
1558
- && !hasUnparsedTail && parseErrors.length === 0
1559
- && assistant.finishReason !== "length";
1639
+ const completionAllowed = !hasUnparsedTail && !operationOmitted && !assistant.nativeToolCalls
1640
+ && parseErrors.every((error) => error.message === PlurnkParser.NO_VALID_OPERATION)
1641
+ && !["length", "content_filter", "tool_calls"].includes(assistant.finishReason ?? "");
1560
1642
  const emptyTurn = preParsedOps === undefined && operationCount === 0 && !hasUnparsedTail;
1561
1643
  // {§unparsed-tail-boundary}: outside text is not operational work.
1562
1644
  const emissionValid = fabrications.length === 0 && (preParsedOps !== undefined
@@ -1565,7 +1647,6 @@ class TurnRunner {
1565
1647
  const recoverableParseErrors = parseErrors
1566
1648
  .filter((error) => error.message !== PlurnkParser.NO_VALID_OPERATION)
1567
1649
  .toSorted(comparePosition);
1568
- const outsideText = outside.join("\n\n");
1569
1650
  return {
1570
1651
  packetAssistant: { content: assistant.content, ops, reasoning },
1571
1652
  sourceBacked: preParsedOps === undefined,
@@ -1573,10 +1654,10 @@ class TurnRunner {
1573
1654
  parseErrors,
1574
1655
  recoverableParseErrors: emissionValid ? recoverableParseErrors : [],
1575
1656
  emptyTurn,
1576
- finalResponse,
1657
+ completionAllowed,
1577
1658
  parseNotices,
1578
1659
  emissionValid,
1579
- outside: outside.length === 0 ? null : { text: outsideText, tokens: contentWeight(outsideText) },
1660
+ outside: outside.length === 0 ? null : outside.join("\n\n"),
1580
1661
  emissionStatements,
1581
1662
  };
1582
1663
  }