@plurnk/plurnk-service 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/.env.defaults +21 -12
  2. package/INSTALL.md +11 -7
  3. package/SPEC.md +437 -304
  4. package/dist/build-info.json +1 -1
  5. package/dist/content/edit-collision.d.ts +5 -0
  6. package/dist/content/edit-collision.d.ts.map +1 -0
  7. package/dist/content/edit-collision.js +14 -0
  8. package/dist/content/edit-collision.js.map +1 -0
  9. package/dist/content/edit-receipt.d.ts +3 -2
  10. package/dist/content/edit-receipt.d.ts.map +1 -1
  11. package/dist/content/edit-receipt.js +58 -13
  12. package/dist/content/edit-receipt.js.map +1 -1
  13. package/dist/content/index.d.ts +4 -1
  14. package/dist/content/index.d.ts.map +1 -1
  15. package/dist/content/index.js +3 -1
  16. package/dist/content/index.js.map +1 -1
  17. package/dist/content/line-anchors.d.ts +41 -0
  18. package/dist/content/line-anchors.d.ts.map +1 -0
  19. package/dist/content/line-anchors.js +191 -0
  20. package/dist/content/line-anchors.js.map +1 -0
  21. package/dist/content/read-projector.d.ts +7 -1
  22. package/dist/content/read-projector.d.ts.map +1 -1
  23. package/dist/content/read-projector.js +46 -10
  24. package/dist/content/read-projector.js.map +1 -1
  25. package/dist/content/read-resolve.js +3 -3
  26. package/dist/content/read-resolve.js.map +1 -1
  27. package/dist/core/BudgetReadout.d.ts.map +1 -1
  28. package/dist/core/BudgetReadout.js +9 -2
  29. package/dist/core/BudgetReadout.js.map +1 -1
  30. package/dist/core/ChannelWrite.js +1 -1
  31. package/dist/core/ChannelWrite.js.map +1 -1
  32. package/dist/core/Dispatcher.d.ts +13 -3
  33. package/dist/core/Dispatcher.d.ts.map +1 -1
  34. package/dist/core/Dispatcher.js +391 -113
  35. package/dist/core/Dispatcher.js.map +1 -1
  36. package/dist/core/DurableStatement.d.ts.map +1 -1
  37. package/dist/core/DurableStatement.js +2 -0
  38. package/dist/core/DurableStatement.js.map +1 -1
  39. package/dist/core/Engine.d.ts +13 -14
  40. package/dist/core/Engine.d.ts.map +1 -1
  41. package/dist/core/Engine.js +434 -300
  42. package/dist/core/Engine.js.map +1 -1
  43. package/dist/core/Engine.sql +156 -90
  44. package/dist/core/ExecutableTools.d.ts +10 -0
  45. package/dist/core/ExecutableTools.d.ts.map +1 -0
  46. package/dist/core/ExecutableTools.js +41 -0
  47. package/dist/core/ExecutableTools.js.map +1 -0
  48. package/dist/core/ExecutorRegistry.d.ts +3 -3
  49. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  50. package/dist/core/ExecutorRegistry.js +2 -2
  51. package/dist/core/ExecutorRegistry.js.map +1 -1
  52. package/dist/core/LogBody.d.ts +2 -0
  53. package/dist/core/LogBody.d.ts.map +1 -1
  54. package/dist/core/LogBody.js +17 -5
  55. package/dist/core/LogBody.js.map +1 -1
  56. package/dist/core/LogEntryProjection.d.ts +14 -0
  57. package/dist/core/LogEntryProjection.d.ts.map +1 -0
  58. package/dist/core/LogEntryProjection.js +38 -0
  59. package/dist/core/LogEntryProjection.js.map +1 -0
  60. package/dist/core/ModelCall.d.ts +27 -0
  61. package/dist/core/ModelCall.d.ts.map +1 -0
  62. package/dist/core/ModelCall.js +125 -0
  63. package/dist/core/ModelCall.js.map +1 -0
  64. package/dist/core/PacketBuilder.d.ts +10 -10
  65. package/dist/core/PacketBuilder.d.ts.map +1 -1
  66. package/dist/core/PacketBuilder.js +131 -103
  67. package/dist/core/PacketBuilder.js.map +1 -1
  68. package/dist/core/ProblemLog.d.ts.map +1 -1
  69. package/dist/core/ProblemLog.js +1 -0
  70. package/dist/core/ProblemLog.js.map +1 -1
  71. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  72. package/dist/core/ProposalLifecycle.js +3 -4
  73. package/dist/core/ProposalLifecycle.js.map +1 -1
  74. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  75. package/dist/core/ProviderInstantiate.js +23 -6
  76. package/dist/core/ProviderInstantiate.js.map +1 -1
  77. package/dist/core/SchemeRegistry.js +7 -7
  78. package/dist/core/SchemeRegistry.js.map +1 -1
  79. package/dist/core/StrikeRail.d.ts +0 -1
  80. package/dist/core/StrikeRail.d.ts.map +1 -1
  81. package/dist/core/StrikeRail.js +6 -4
  82. package/dist/core/StrikeRail.js.map +1 -1
  83. package/dist/core/caps/CapsResolve.js +1 -1
  84. package/dist/core/caps/CapsResolve.js.map +1 -1
  85. package/dist/core/caps/DbEntryCaps.d.ts +2 -1
  86. package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
  87. package/dist/core/caps/DbEntryCaps.js +8 -5
  88. package/dist/core/caps/DbEntryCaps.js.map +1 -1
  89. package/dist/core/caps/DbProjectionCaps.d.ts +1 -0
  90. package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
  91. package/dist/core/caps/DbProjectionCaps.js +29 -0
  92. package/dist/core/caps/DbProjectionCaps.js.map +1 -1
  93. package/dist/core/caps/SchemeCtxImpl.d.ts +5 -2
  94. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  95. package/dist/core/caps/SchemeCtxImpl.js +6 -4
  96. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  97. package/dist/core/fork.d.ts.map +1 -1
  98. package/dist/core/fork.js +23 -7
  99. package/dist/core/fork.js.map +1 -1
  100. package/dist/core/fork.sql +33 -12
  101. package/dist/core/git-membership.d.ts.map +1 -1
  102. package/dist/core/git-membership.js +1 -2
  103. package/dist/core/git-membership.js.map +1 -1
  104. package/dist/core/git-state.js +1 -1
  105. package/dist/core/git-state.js.map +1 -1
  106. package/dist/core/packet-wire.d.ts +4 -1
  107. package/dist/core/packet-wire.d.ts.map +1 -1
  108. package/dist/core/packet-wire.js +248 -90
  109. package/dist/core/packet-wire.js.map +1 -1
  110. package/dist/core/provider-accounting.d.ts +24 -0
  111. package/dist/core/provider-accounting.d.ts.map +1 -0
  112. package/dist/core/provider-accounting.js +76 -0
  113. package/dist/core/provider-accounting.js.map +1 -0
  114. package/dist/core/search-gate.d.ts +2 -2
  115. package/dist/core/search-gate.d.ts.map +1 -1
  116. package/dist/core/search-gate.js +8 -8
  117. package/dist/core/search-gate.js.map +1 -1
  118. package/dist/core/token-ruler.js +1 -1
  119. package/dist/core/token-ruler.js.map +1 -1
  120. package/dist/core/turn-scheduler.js +1 -1
  121. package/dist/core/turn-scheduler.js.map +1 -1
  122. package/dist/digest/Digest.d.ts.map +1 -1
  123. package/dist/digest/Digest.js +220 -109
  124. package/dist/digest/Digest.js.map +1 -1
  125. package/dist/digest/digest.sql +40 -18
  126. package/dist/schemes/Exec.d.ts.map +1 -1
  127. package/dist/schemes/Exec.js +111 -101
  128. package/dist/schemes/Exec.js.map +1 -1
  129. package/dist/schemes/File.d.ts +5 -4
  130. package/dist/schemes/File.d.ts.map +1 -1
  131. package/dist/schemes/File.js +24 -22
  132. package/dist/schemes/File.js.map +1 -1
  133. package/dist/schemes/Log.d.ts +11 -0
  134. package/dist/schemes/Log.d.ts.map +1 -1
  135. package/dist/schemes/Log.js +171 -106
  136. package/dist/schemes/Log.js.map +1 -1
  137. package/dist/schemes/Log.sql +48 -32
  138. package/dist/schemes/Prompt.d.ts +3 -3
  139. package/dist/schemes/Prompt.d.ts.map +1 -1
  140. package/dist/schemes/Prompt.js +4 -1
  141. package/dist/schemes/Prompt.js.map +1 -1
  142. package/dist/schemes/Skill.d.ts +4 -4
  143. package/dist/schemes/Skill.d.ts.map +1 -1
  144. package/dist/schemes/Skill.js +3 -0
  145. package/dist/schemes/Skill.js.map +1 -1
  146. package/dist/schemes/Worker.d.ts +4 -4
  147. package/dist/schemes/Worker.d.ts.map +1 -1
  148. package/dist/schemes/Worker.js +9 -5
  149. package/dist/schemes/Worker.js.map +1 -1
  150. package/dist/schemes/_entry-crud.d.ts +0 -1
  151. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  152. package/dist/schemes/_entry-crud.js +15 -9
  153. package/dist/schemes/_entry-crud.js.map +1 -1
  154. package/dist/schemes/_entry-crud.sql +2 -14
  155. package/dist/schemes/_entry-find.d.ts +13 -12
  156. package/dist/schemes/_entry-find.d.ts.map +1 -1
  157. package/dist/schemes/_entry-find.js +33 -28
  158. package/dist/schemes/_entry-find.js.map +1 -1
  159. package/dist/schemes/_entry-find.sql +2 -21
  160. package/dist/schemes/_entry-graph.sql +1 -1
  161. package/dist/schemes/_entry-manifest.d.ts +5 -17
  162. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  163. package/dist/schemes/_entry-manifest.js +28 -24
  164. package/dist/schemes/_entry-manifest.js.map +1 -1
  165. package/dist/schemes/_entry-ops.d.ts +5 -4
  166. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  167. package/dist/schemes/_entry-ops.js +87 -47
  168. package/dist/schemes/_entry-ops.js.map +1 -1
  169. package/dist/schemes/_entry-ops.sql +30 -4
  170. package/dist/schemes/_entry-send.js +4 -4
  171. package/dist/schemes/_entry-send.js.map +1 -1
  172. package/dist/schemes/_search-index.d.ts.map +1 -1
  173. package/dist/schemes/_search-index.js +10 -2
  174. package/dist/schemes/_search-index.js.map +1 -1
  175. package/dist/schemes/exec-abort.js +2 -2
  176. package/dist/schemes/exec-abort.js.map +1 -1
  177. package/dist/server/Daemon.d.ts +1 -1
  178. package/dist/server/Daemon.d.ts.map +1 -1
  179. package/dist/server/Daemon.js +25 -23
  180. package/dist/server/Daemon.js.map +1 -1
  181. package/dist/server/drain.sql +2 -2
  182. package/dist/server/envelope.d.ts +0 -2
  183. package/dist/server/envelope.d.ts.map +1 -1
  184. package/dist/server/envelope.js.map +1 -1
  185. package/dist/server/envelope.sql +2 -2
  186. package/dist/server/lifecycle-recovery.sql +25 -15
  187. package/dist/server/logEntry.d.ts +1 -0
  188. package/dist/server/logEntry.d.ts.map +1 -1
  189. package/dist/server/logEntry.js +13 -1
  190. package/dist/server/logEntry.js.map +1 -1
  191. package/dist/server/logEntry.sql +5 -1
  192. package/migrations/001_schema.sql +603 -207
  193. package/package.json +39 -34
  194. package/dist/core/BudgetOverflow.d.ts +0 -20
  195. package/dist/core/BudgetOverflow.d.ts.map +0 -1
  196. package/dist/core/BudgetOverflow.js +0 -49
  197. package/dist/core/BudgetOverflow.js.map +0 -1
  198. package/dist/core/caps/DbTagCaps.d.ts +0 -10
  199. package/dist/core/caps/DbTagCaps.d.ts.map +0 -1
  200. package/dist/core/caps/DbTagCaps.js +0 -46
  201. package/dist/core/caps/DbTagCaps.js.map +0 -1
@@ -1,5 +1,6 @@
1
+ var _a;
1
2
  import { PlurnkParser, PlurnkParseError, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
2
- import { RuntimeTag } from "@plurnk/plurnk-execs";
3
+ import { RuntimeInvocation, RuntimeTag } from "@plurnk/plurnk-execs";
3
4
  import Owner from "./Owner.js";
4
5
  const TERMINAL_SEND_SIGNALS = new Set([102, 200, 202, 300, 499]);
5
6
  const comparePosition = (a, b) => a.line - b.line || a.column - b.column;
@@ -19,6 +20,7 @@ import LoopLifecycle from "./LoopLifecycle.js";
19
20
  import { readFile } from "node:fs/promises";
20
21
  import { fileURLToPath } from "node:url";
21
22
  import { setTimeout as delay } from "node:timers/promises";
23
+ import { randomUUID } from "node:crypto";
22
24
  // Shared module imported by both Engine and bin/digest.ts, so wire
23
25
  // projection and digest projection are structurally one function — no
24
26
  // drift between wire and digest possible.
@@ -26,7 +28,6 @@ import PacketWire from "./packet-wire.js";
26
28
  import Results, { OperationFailureError } from "./results.js";
27
29
  import BranchReceipt from "./BranchReceipt.js";
28
30
  import TerminalResult from "./TerminalResult.js";
29
- import BudgetOverflow from "./BudgetOverflow.js";
30
31
  import WorkerControlAddress from "./WorkerControlAddress.js";
31
32
  import JournalTurn from "./JournalTurn.js";
32
33
  // The engine's collaborators — each owns one machine; Engine owns the loop/turn
@@ -42,7 +43,9 @@ import { observed, observedSync } from "../observe/spans.js";
42
43
  import { OPS_DISPATCHED, PROVIDER_CALLS, recordCounter } from "../observe/metrics.js";
43
44
  import { scheduleTurnOps } from "./turn-scheduler.js";
44
45
  import { readOptimisticSettlementMs } from "./optimistic-settlement.js";
45
- const DEFAULT_MAX_STRIKES = 3;
46
+ import { providerRequestFromStorageRow, } from "./provider-accounting.js";
47
+ import ModelCall, { ModelCallPersistenceError, ProviderAccountingIntegrityError, } from "./ModelCall.js";
48
+ const DEFAULT_MAX_STRIKES = 6;
46
49
  const RECORD_STREAM_MIMETYPES = new Set(["application/jsonl", "application/x-ndjson"]);
47
50
  const baseMimetype = (mimetype) => mimetype.split(";", 1)[0].trim().toLowerCase();
48
51
  // {§exec-stream} Active streams publish only independently meaningful units.
@@ -64,7 +67,7 @@ const ENGINE_PROBLEMS = Object.freeze({
64
67
  idle_turn: {
65
68
  status: 409,
66
69
  code: "idle-turn",
67
- detail: "SEND[102] was emitted without an operation to continue from.",
70
+ detail: "`## SEND0 [102]` was emitted without an operation to continue from.",
68
71
  },
69
72
  });
70
73
  // The prompt entry target - prompt:///<loop>/<N>, self-only ({§prompt-self-only}):
@@ -123,9 +126,50 @@ const readFilesItems = () => {
123
126
  return null;
124
127
  return normalizeFilesItems(Number.parseInt(raw, 10));
125
128
  };
126
- import { ProviderError, providerCostFor, providerCostUsd, validateProviderCost, scopeEnvToAlias, resolveActiveAlias } from "@plurnk/plurnk-providers";
129
+ import { aggregateProviderAccounting, ProviderError, resolveActiveAlias, scopeEnvToAlias, } from "@plurnk/plurnk-providers";
127
130
  import { validateGbnf } from "@plurnk/gbnf";
128
131
  import ProviderInstantiate from "./ProviderInstantiate.js";
132
+ // A generated rail may constrain only the sampled continuation while the chat
133
+ // template contributes a prefix that llama-server preserves in its response.
134
+ // The artifact names the alternate root that composes those bytes for evidence
135
+ // grading; arbitrary grammars without the declaration retain their transport root.
136
+ const grammarConstraint = (grammar) => {
137
+ const declarations = [...grammar.matchAll(/^# @plurnk-response-root ([A-Za-z][A-Za-z0-9-]*)$/gm)];
138
+ if (declarations.length === 0)
139
+ return { transport: grammar, response: grammar };
140
+ if (declarations.length !== 1)
141
+ throw new Error("GBNF constraint declares multiple @plurnk-response-root values");
142
+ const responseRoot = declarations[0][1];
143
+ if (!grammar.split("\n").some((line) => line.startsWith(`${responseRoot} ::=`))) {
144
+ throw new Error(`GBNF constraint declares missing response root ${responseRoot}`);
145
+ }
146
+ const roots = [...grammar.matchAll(/^root ::= ([A-Za-z][A-Za-z0-9-]*)$/gm)];
147
+ if (roots.length !== 1)
148
+ throw new Error("GBNF constraint must declare exactly one simple root");
149
+ return {
150
+ transport: grammar,
151
+ response: grammar.replace(roots[0][0], `root ::= ${responseRoot}`),
152
+ };
153
+ };
154
+ const TOKEN_BUDGET_OVERFLOW_DETAIL = "Token Budget Overflow: Token Usage exceeded Token Ceiling. Newest log items were automatically FOLDed to fit within token budget. Curate the log and/or perform more conservatively scoped or chunked retrieval operations to recover.";
155
+ const tokenBudgetOverflowFailure = (pressure) => Results.failure("engine:context", "token-budget-overflow", 413, TOKEN_BUDGET_OVERFLOW_DETAIL, {}, { ...pressure });
156
+ const budgetPressure = (usage, ceiling) => {
157
+ if (usage <= ceiling)
158
+ throw new Error("context-envelope admission requires positive ruler debt");
159
+ return { usage, ceiling, deficit: usage - ceiling };
160
+ };
161
+ const contextEnvelopeFailure = (pressure, admission) => Results.failure("engine:context", "context-envelope-admission-failed", 413, `The configured context envelope cannot admit this request: ${admission.detail}.`, {}, {
162
+ ...pressure,
163
+ stage: "context-envelope-admission",
164
+ contextAdmission: admission.reason,
165
+ contextCapacity: admission.capacity,
166
+ ...(admission.measurement === undefined ? {} : {
167
+ promptTokens: admission.measurement.tokens,
168
+ tokenKind: admission.measurement.kind,
169
+ tokenSource: admission.measurement.source,
170
+ }),
171
+ retryable: false,
172
+ });
129
173
  // Runtime normalization for a disposition the engine refuses or resolves as a
130
174
  // continue after dispatch ({§send}). Every admitted emission itself ends in an
131
175
  // explicit disposition SEND ({§emission-admission}).
@@ -155,7 +199,7 @@ const DEFAULT_LOOP_TIMEOUT_MS = 86400000;
155
199
  const readLoopTimeoutMs = () => readPositiveInt("PLURNK_SERVICE_LOOP_TIMEOUT", DEFAULT_LOOP_TIMEOUT_MS);
156
200
  // The wall's abort reason — runLoop branches a mid-turn teardown to the 504 terminal on it.
157
201
  const LOOP_TIMEOUT_REASON = "loop_timeout";
158
- export default class Engine {
202
+ class Engine {
159
203
  static fingerprintTurn(ops) {
160
204
  return StrikeRail.fingerprintTurn(ops);
161
205
  }
@@ -172,7 +216,7 @@ export default class Engine {
172
216
  // Boot-discovered runtime executors. Daemon builds + sets via
173
217
  // setExecutors at start(); undefined until then (and in bare tests).
174
218
  #executors;
175
- // {§send-premature-terminate}/SEND[202]<T> — park deadlines by loopId, written at dispatch (the
219
+ // {§send-premature-terminate}/scoped SEND signal 202 — park deadlines by loopId, written at dispatch (the
176
220
  // marker's seconds; -1 = indefinite), consumed by the daemon's drain park-exit to schedule
177
221
  // the deadline wake. In-memory: a daemon restart drops pending deadlines (documented).
178
222
  parkDeadlines = new Map();
@@ -184,9 +228,6 @@ export default class Engine {
184
228
  #notices;
185
229
  #problems;
186
230
  #strikes;
187
- // {§grinder-hard-413-recovery} - loops granted their one over-ceiling recovery turn. Cleared on a
188
- // fitting turn so a later independent overflow can earn a fresh recovery, and at loop cleanup.
189
- #hardOverflowRecovery = new Set();
190
231
  #packets;
191
232
  searchGate = new SearchGate();
192
233
  #proposals;
@@ -335,9 +376,9 @@ export default class Engine {
335
376
  // parse): warn once per message, never per turn; the turn records railsVerdict "unverifiable".
336
377
  static #railGapWarned = new Set();
337
378
  static #warnRailVerdictGapOnce(message) {
338
- if (Engine.#railGapWarned.has(message))
379
+ if (_a.#railGapWarned.has(message))
339
380
  return;
340
- Engine.#railGapWarned.add(message);
381
+ _a.#railGapWarned.add(message);
341
382
  process.stderr.write(`plurnk-engine: rail verdict unavailable — the configured grammar did not parse in @plurnk/gbnf (${message})\n`);
342
383
  }
343
384
  static #requireGrammarEvidence(response) {
@@ -371,7 +412,8 @@ export default class Engine {
371
412
  discovery: { registry: emptyRegistry(), handlers: new Map(), skipped: [] },
372
413
  });
373
414
  // {§tokenomics-agnostic-ruler} — standalone construction and the daemon
374
- // use the same default; provider counting is confined to physical admission.
415
+ // use the same default; provider counting is confined to hard
416
+ // context-envelope admission.
375
417
  this.#tokenize = tokenize ?? rulerCount;
376
418
  const executors = () => this.#executors;
377
419
  const loopSignal = (loopId) => this.#loopAborts.get(loopId)?.signal;
@@ -381,7 +423,6 @@ export default class Engine {
381
423
  this.#packets = new PacketBuilder({
382
424
  db,
383
425
  schemes,
384
- problems: this.#problems,
385
426
  executors,
386
427
  });
387
428
  this.#proposals = new ProposalLifecycle({
@@ -426,12 +467,16 @@ export default class Engine {
426
467
  if (this.#executors === undefined)
427
468
  throw new Error("registerRuntime: executor registry not wired yet");
428
469
  RuntimeTag.assert(tag, "module runtime");
470
+ const normalized = {
471
+ ...entry,
472
+ invocation: RuntimeInvocation.assert(entry.invocation, entry.namespaceOwner.name, tag),
473
+ };
429
474
  // Preflight both owners before either write; synchronous registration
430
475
  // then cannot leave a half-claimed namespace. {§plugin-namespace-arbitration}
431
- this.#executors.assertCanRegister(tag, entry.namespaceOwner);
432
- this.#schemes.assertRuntimeClaim(tag, entry.namespaceOwner);
433
- this.#schemes.registerRuntimeScheme(tag, entry.executor, entry.namespaceOwner, scheme);
434
- this.#executors.register(tag, entry);
476
+ this.#executors.assertCanRegister(tag, normalized.namespaceOwner);
477
+ this.#schemes.assertRuntimeClaim(tag, normalized.namespaceOwner);
478
+ this.#schemes.registerRuntimeScheme(tag, normalized.executor, normalized.namespaceOwner, scheme);
479
+ this.#executors.register(tag, normalized);
435
480
  }
436
481
  // Supply an explicitly configured local constraint; ANTLR remains the
437
482
  // language authority. {§grammar-enforcement-verified-at-boot}
@@ -455,9 +500,10 @@ export default class Engine {
455
500
  ? variant
456
501
  : fileURLToPath(import.meta.resolve(`@plurnk/plurnk-contracts/${variant}`));
457
502
  const text = await readFile(path, "utf8"); // unresolvable/unreadable throws — a configured rail never silently degrades
458
- this.#gbnfCache.set(variant, text);
503
+ const constraint = grammarConstraint(text);
504
+ this.#gbnfCache.set(variant, constraint);
459
505
  process.stderr.write(`plurnk-engine: GBNF constraint: ${alias || "(bare)"} → ${variant} (${text.length} chars)\n`);
460
- return text;
506
+ return constraint;
461
507
  }
462
508
  // A lineage's no-parent root; a root worker resolves to itself. Fail hard
463
509
  // when corruption leaves a worker without one. {§worker-primary}
@@ -467,6 +513,16 @@ export default class Engine {
467
513
  throw new Error(`resolveWorkerPrimary: worker ${workerId} has no lineage root — corrupt parent chain`);
468
514
  return root.id;
469
515
  }
516
+ async resolveWorkerProviderIdentity(workerId) {
517
+ const identity = await this.#db.engine_worker_provider_identity.get({ worker_id: workerId });
518
+ if (identity === undefined) {
519
+ throw new Error(`resolveWorkerProviderIdentity: worker ${workerId} has no lineage root — corrupt parent chain`);
520
+ }
521
+ return {
522
+ workerId: identity.worker_id,
523
+ primaryWorkerId: identity.primary_worker_id,
524
+ };
525
+ }
470
526
  promptBudgetFor(provider) {
471
527
  return this.#packets.promptBudgetFor(provider);
472
528
  }
@@ -474,6 +530,109 @@ export default class Engine {
474
530
  const tags = Meta.composeAttributions(this.#schemes.attributions(context), this.#executors?.attributions(context) ?? [], await this.#mimetypes.attributions(context), provider.attributions?.(context) ?? []);
475
531
  return [...tags];
476
532
  }
533
+ #providerAttributions(provider, context) {
534
+ return [...Meta.composeAttributions(provider.attributions?.(context) ?? [])];
535
+ }
536
+ static #providerFailure(error, signal) {
537
+ if (error instanceof ProviderError) {
538
+ return { status: error.problem.status, problem: error.problem };
539
+ }
540
+ if (signal?.aborted === true) {
541
+ const timedOut = signal.reason === LOOP_TIMEOUT_REASON;
542
+ return Results.failure("engine:provider", timedOut ? "provider-call-timeout" : "provider-call-cancelled", timedOut ? 504 : 499, timedOut
543
+ ? "The provider call was interrupted by the loop deadline."
544
+ : "The provider call was interrupted by loop cancellation.", {}, { stage: "provider-request", retryable: false });
545
+ }
546
+ console.error("Provider failed outside its Problem Details contract:", error);
547
+ return Results.failure("engine:provider", "provider-contract-violation", 502, "The provider failed without returning its required Problem Details.", {}, { stage: "provider-request", retryable: false });
548
+ }
549
+ async #runBareBatch({ statements, provider, turnId, modelCallSequenceStart, workspaceId, workerId, primaryWorkerId, loopSequence, turnSequence, signal, }) {
550
+ const prepared = [];
551
+ for (const [index, statement] of statements.entries()) {
552
+ const providerWorkerId = randomUUID();
553
+ const attributionContext = Object.freeze({
554
+ workspaceId: String(workspaceId),
555
+ workerId: providerWorkerId,
556
+ primaryWorkerId,
557
+ loop: loopSequence,
558
+ turn: turnSequence,
559
+ attempt: 1,
560
+ });
561
+ const attributions = this.#providerAttributions(provider, attributionContext);
562
+ const modelCall = await ModelCall.open(this.#db, {
563
+ turnId,
564
+ sequence: modelCallSequenceStart + index,
565
+ kind: "bare",
566
+ attributions,
567
+ model: provider.model,
568
+ });
569
+ prepared.push({ statement, modelCall, attributions, providerWorkerId });
570
+ }
571
+ const maxTokens = this.#packets.maxTokensFor(provider) ?? undefined;
572
+ const settlements = await Promise.allSettled(prepared.map(async ({ statement, modelCall, attributions, providerWorkerId }) => {
573
+ try {
574
+ const response = await observed("provider.generate", { model: provider.model, attempt: 1, kind: "bare" }, async (span) => {
575
+ try {
576
+ const generated = await provider.generate({
577
+ messages: [{ role: "user", content: statement.body }],
578
+ workerId: providerWorkerId,
579
+ primaryWorkerId,
580
+ signal,
581
+ maxTokens,
582
+ attributions: attributions.length > 0 ? attributions : undefined,
583
+ workspaceId: String(workspaceId),
584
+ loop: loopSequence,
585
+ turn: turnSequence,
586
+ observeRequest: modelCall.observeRequest,
587
+ callKind: "bare",
588
+ });
589
+ modelCall.assertAccounting(generated.accounting);
590
+ recordCounter(PROVIDER_CALLS, {
591
+ model: provider.model,
592
+ attempt: 1,
593
+ status: "resolved",
594
+ });
595
+ span.setAttribute("status", "resolved");
596
+ return generated;
597
+ }
598
+ catch (error) {
599
+ if (error instanceof ProviderError) {
600
+ modelCall.assertAccounting(error.accounting);
601
+ }
602
+ throw error;
603
+ }
604
+ });
605
+ await modelCall.observeResponse(response);
606
+ return {
607
+ statement,
608
+ modelCallId: modelCall.id,
609
+ result: Results.assert({
610
+ status: 200,
611
+ content: response.assistant.content,
612
+ mimetype: "text/plain",
613
+ }),
614
+ };
615
+ }
616
+ catch (error) {
617
+ if (error instanceof ModelCallPersistenceError || error instanceof ProviderAccountingIntegrityError) {
618
+ throw error;
619
+ }
620
+ const failure = _a.#providerFailure(error, signal);
621
+ if (error instanceof ProviderError && error.attempt !== undefined) {
622
+ await modelCall.observeResponse(error.attempt, failure);
623
+ }
624
+ else {
625
+ await modelCall.fail(failure);
626
+ }
627
+ return { statement, modelCallId: modelCall.id, result: failure };
628
+ }
629
+ }));
630
+ const internalFailure = settlements.find((settlement) => settlement.status === "rejected");
631
+ if (internalFailure !== undefined)
632
+ throw internalFailure.reason;
633
+ signal?.throwIfAborted();
634
+ return settlements.map((settlement) => settlement.value);
635
+ }
477
636
  // {§attribution} — reporting derives from exact provider-request evidence;
478
637
  // malformed durable tags fail here instead of being silently filtered.
479
638
  async loopAttributions(loopId) {
@@ -486,29 +645,22 @@ export default class Engine {
486
645
  });
487
646
  return [...Meta.composeAttributions(tags)];
488
647
  }
489
- // Loop totals are billing evidence; the latest-turn pair is the client
490
- // occupancy gauge. {§tokenomics-client-gauge}, {§notifications-loop-terminated}
648
+ // The request ledger is cardinal evidence; its aggregate and the latest-turn
649
+ // occupancy gauge are derived projections. {§tokenomics-client-gauge},
650
+ // {§notifications-loop-terminated}, {§provider-accounting}
491
651
  async loopUsage(loopId) {
492
652
  const row = await this.#db.engine_loop_usage.get({ loop_id: loopId });
493
653
  if (row === undefined)
494
654
  throw new Error(`loopUsage: loop ${loopId} does not exist`);
495
- const parsedCosts = JSON.parse(row?.costs ?? "[]");
496
- if (!Array.isArray(parsedCosts))
497
- throw new TypeError(`loop ${loopId} monetary evidence is not an array`);
498
- const costs = parsedCosts.map((cost) => validateProviderCost(cost));
655
+ const requests = await this.#db.engine_loop_provider_requests.all({ loop_id: loopId });
499
656
  return {
500
- promptTokens: row?.prompt ?? 0,
501
- completionTokens: row?.completion ?? 0,
502
- reasoningTokens: row?.reasoning ?? 0,
503
- cachedTokens: row?.cached ?? 0,
504
- costUsd: row?.cost_usd ?? null,
505
- costs,
506
- // Latest provider attempt on the latest turn, not the billed total.
507
- contextTokens: row?.context ?? 0,
657
+ accounting: aggregateProviderAccounting(requests.map(providerRequestFromStorageRow)),
658
+ // Latest physical request on the latest turn, not the billed total.
659
+ contextTokens: row.context,
508
660
  // Latest effective packet allowance; null when uncapped or unknown.
509
- promptBudget: row?.context_size ?? null,
661
+ promptBudget: row.context_size,
510
662
  // Latest turn's opaque provider metadata. {§meta-passthrough}
511
- meta: JSON.parse(row?.meta ?? "{}"),
663
+ meta: JSON.parse(row.meta ?? "{}"),
512
664
  };
513
665
  }
514
666
  // A mapped rail divergence is a CODE-POINT offset into the model's content;
@@ -531,7 +683,7 @@ export default class Engine {
531
683
  }
532
684
  return { line, column };
533
685
  }
534
- async runLoop({ provider, messages, requirements = "", workspaceId, workerId, loopId, maxTurns = 50, maxStrikes = readMaxStrikes(), minCycles = readPositiveInt("PLURNK_SERVICE_MIN_CYCLES", DEFAULT_MIN_CYCLES), maxCyclePeriod = readPositiveInt("PLURNK_SERVICE_MAX_CYCLE_PERIOD", DEFAULT_MAX_CYCLE_PERIOD), origin = "model", signal, onDispatch, }) {
686
+ async runLoop({ provider, childProvider = provider, messages, requirements = "", workspaceId, workerId, loopId, maxTurns = 50, maxStrikes = readMaxStrikes(), minCycles = readPositiveInt("PLURNK_SERVICE_MIN_CYCLES", DEFAULT_MIN_CYCLES), maxCyclePeriod = readPositiveInt("PLURNK_SERVICE_MAX_CYCLE_PERIOD", DEFAULT_MAX_CYCLE_PERIOD), origin = "model", signal, onDispatch, }) {
535
687
  // A 202 park suspends this durable loop and a later wake re-enters runLoop.
536
688
  // Its ceiling therefore counts every prior turn, not merely this process-local
537
689
  // execution segment.
@@ -567,11 +719,11 @@ export default class Engine {
567
719
  return { turnIds, result, hitMaxTurns: false, reason: "loop_timeout" };
568
720
  };
569
721
  // Cleanup splits by termination kind:
570
- // - "graceful" (SEND[202] Accepted): in-flight streaming-scheme spawns
722
+ // - "graceful" (SEND signal 202 Accepted): in-flight streaming-scheme spawns
571
723
  // are ALLOWED to outlive the loop — they complete naturally, write final
572
724
  // channel state, and wake-on-completion (E.4) opens a fresh loop. 202 is
573
725
  // the only terminal that means "keep my async work."
574
- // - "forceful" (SEND[200] done, max_turns, strike, cancel, budget, 4xx/5xx):
726
+ // - "forceful" (SEND signal 200 done, max_turns, strike, cancel, context-envelope rejection, 4xx/5xx):
575
727
  // fire the loop-level abort so leftover spawns tear down. "Done" reaps.
576
728
  const cleanup = (kind, reason) => {
577
729
  clearTimeout(wall);
@@ -581,7 +733,6 @@ export default class Engine {
581
733
  this.#loopAborts.delete(loopId);
582
734
  this.#strikes.delete(loopId);
583
735
  this.searchGate.cleanup(loopId);
584
- this.#hardOverflowRecovery.delete(loopId);
585
736
  this.#notices.delete(loopId);
586
737
  };
587
738
  while (true) {
@@ -649,7 +800,7 @@ export default class Engine {
649
800
  turn = await observed(// {§observability-boundary}
650
801
  "loop.turn", { workerId, "loop.id": loopId }, async (span) => {
651
802
  const t = await this.runTurn({
652
- provider, messages, requirements, workspaceId, workerId, loopId, origin, signal, onDispatch,
803
+ provider, childProvider, messages, requirements, workspaceId, workerId, loopId, origin, signal, onDispatch,
653
804
  turnNumber: turnIds.length + 1, maxTurns,
654
805
  invalidEmissionRecoveryEntryId,
655
806
  });
@@ -698,25 +849,23 @@ export default class Engine {
698
849
  return { turnIds, result, hitMaxTurns: false, reason: "invalid_emission" };
699
850
  }
700
851
  invalidEmissionRecoveryEntryId = null;
701
- // SPEC {§grinder}: budget hard-stop — packet won't fit even collapsed → abandon.
852
+ // SPEC {§grinder}: the effective context envelope rejected the request.
702
853
  if (turn.budgetHardStop) {
703
- if (turn.budget === undefined) {
704
- throw new Error("a budget hard-stop requires its measured overflow");
854
+ if (turn.budgetFailure === undefined) {
855
+ throw new Error("a context-envelope hard-stop requires its exact failure");
705
856
  }
706
- const failure = BudgetOverflow.result(turn.budget.usage, turn.budget.ceiling, false);
707
- const result = await this.#lifecycle.finish(loopId, failure);
857
+ const result = await this.#lifecycle.finish(loopId, turn.budgetFailure);
708
858
  if (result === null)
709
859
  throw new Error(`loop ${loopId} became terminal before budget settlement`);
710
- cleanup("forceful", "budget_overflow");
711
- return { turnIds, result, hitMaxTurns: false, reason: "budget_overflow" };
860
+ cleanup("forceful", "context_envelope");
861
+ return { turnIds, result, hitMaxTurns: false, reason: "context_envelope" };
712
862
  }
713
- // {§engine-rails} — per-turn strike accounting (cycle detection, the
714
- // grinder/steer coupling, hard operation outcomes). StrikeRail owns the
863
+ // {§engine-rails} — per-turn strike accounting (cycle detection,
864
+ // steer coupling, hard operation outcomes). StrikeRail owns the
715
865
  // bookkeeping; runLoop owns abandonment.
716
866
  const verdict = this.#strikes.assess(loopId, {
717
867
  fingerprint: turn.fingerprint,
718
868
  outcomes: turn.outcomes,
719
- budgetStruck: turn.budgetStruck,
720
869
  steerStruck: turn.steerStruck,
721
870
  minCycles, maxCyclePeriod, maxStrikes,
722
871
  });
@@ -739,7 +888,7 @@ export default class Engine {
739
888
  }
740
889
  }
741
890
  }
742
- async runTurn({ provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, invalidEmissionRecoveryEntryId, }) {
891
+ async runTurn({ provider, childProvider = provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, invalidEmissionRecoveryEntryId, }) {
743
892
  const allowInvalidEmissionRecovery = invalidEmissionRecoveryEntryId === null;
744
893
  const transientOpenLogEntryId = typeof invalidEmissionRecoveryEntryId === "number"
745
894
  ? invalidEmissionRecoveryEntryId
@@ -800,14 +949,14 @@ export default class Engine {
800
949
  // Model ops dispatch after these pre-model rows.
801
950
  let nextActionIndex = 1;
802
951
  const turnOpenPaths = [];
803
- // {§model-entry} — the worker's first turn opens with the model's own turn-0, mirrored OPEN: a
804
- // worked turn PLAN → the environment FINDs the foist ACTUALLY dispatches → SEND[102]. Built
952
+ // {§worker-initialization-entry} — the worker's first turn opens with a kernel-authored initialization: a
953
+ // worked turn PLAN → the environment FINDs the foist ACTUALLY dispatches → SEND signal 102. Built
805
954
  // from the real ops below (not a static print — we lean into the genuine echo paradigm) and
806
- // written at sequence 1, so it reads first as the emission with the foisted results following.
955
+ // written at sequence 1, so it reads first with the foisted results following.
807
956
  const turnZeroMoves = [];
808
957
  if (seq === 1) {
809
958
  if (workerFirstLoop)
810
- nextActionIndex = 2; // reserve sequence 1 for the turn-0 echo
959
+ nextActionIndex = 2; // reserve sequence 1 for initialization
811
960
  // Operator doc READs (PLURNK_SERVICE_MD_<ALIAS>, {§actor-boundary-doc-injection}). The docs were materialized
812
961
  // as worker://plurnk/<entry> entries by the plurnk worker (LoopDocs, via the
813
962
  // {§actor-boundary} keystone); foist a READ of each into THIS turn-0 so the model
@@ -840,7 +989,6 @@ export default class Engine {
840
989
  const promptPath = promptTarget(promptLoopSeq, seq);
841
990
  const entry = {
842
991
  channels: { body: { content: promptRow.prompt, mimetype: "text/markdown" } },
843
- tags: [],
844
992
  attributes: { openPaths },
845
993
  };
846
994
  await EntryCrud.writeEntry(promptPath.pathname, entry, systemCtx, "prompt", workerId);
@@ -921,97 +1069,71 @@ export default class Engine {
921
1069
  // consume the filesystem divergences a second time.
922
1070
  await this.#queueWorkspaceWarm(systemCtx, true, false);
923
1071
  // Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, {§actor-boundary-catalog-preview}):
924
- // FIND surveys foisted into the worker's first model turn so it opens with its catalog.
925
- // Folder-capable surfaces reveal one level with `*`; each deeper directory is an
926
- // actionable `dir/**` aggregate. The curated kernel docs remain recursive, so the
927
- // opening packet demonstrates both navigation forms. Empty results are orientation.
1072
+ // Four FIND surveys foisted into the worker's first model turn establish the project,
1073
+ // commons, private, and documentation surfaces in that order. Their `init`
1074
+ // classification lets the model curate this opening survey as one log set.
928
1075
  if (seq === 1) {
929
1076
  // {§operator-config-workspace-files-items} — workspace filesItems replaces the env default.
930
1077
  const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
931
1078
  const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
932
1079
  if (filesItems !== null && workerFirstLoop) { // {§actor-boundary-catalog-preview} — once per worker
933
- // engine_scheme_catalog_summary is the workspace-bounded scheme source: ordered,
934
- // one row per stored entry scheme. log:// is absent —
935
- // it lives in log_entries, not the catalog (present-mode, the # Log section).
936
1080
  const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
937
- // Entry-bearing plugin schemes foist alongside the four structural surveys below.
938
- const foistSchemes = catalogSchemes
939
- .filter((catalog) => catalog.scheme !== "prompt" && catalog.scheme !== "worker")
940
- .map(({ scheme, shallow_items }) => ({ scheme, shallow_items }));
941
- // Commons + project files always foist. An empty result establishes that the
942
- // surface exists and currently contains nothing.
943
- foistSchemes.push({
944
- scheme: "worker",
945
- shallow_items: catalogSchemes.find((c) => c.scheme === "worker")?.shallow_items ?? 0,
946
- });
947
- if (!foistSchemes.some((c) => c.scheme === "file"))
948
- foistSchemes.push({ scheme: "file", shallow_items: 0 }); // Empty project surface still receives its orienting FIND.
949
- for (const { scheme, shallow_items: shallowItems } of foistSchemes) {
950
- const isFile = scheme === "file";
951
- const pattern = this.#schemes.manifestFor(scheme)?.folderScopes === true ? "*" : "**";
952
- // Only the file map takes PLURNK_SERVICE_FILES_ITEMS as an explicit first-N
953
- // cap. Every other ordinary survey uses FIND's markerless first page.
954
- const cap = isFile && filesItems > 0 && shallowItems > 0 ? Math.min(filesItems, shallowItems) : null;
955
- const catalogMarker = cap === null ? null : { marks: [1, cap] };
956
- // The file survey foists as the BARE relative glob — the path shape plurnk.md
957
- // teaches (`*`, `src/**`, `**/notes.md`; bare = project-relative) — so the turn-0
958
- // exemplar and the log rows the model reads never train a leading-slash or
959
- // file:/// habit the rest of the teaching contradicts.
960
- const catalogFind = {
961
- op: "FIND", suffix: "", signal: null,
962
- target: isFile ? { kind: "local", raw: pattern } : {
963
- kind: "url",
964
- raw: `${scheme}:///${pattern}`,
965
- scheme,
966
- username: null, password: null, hostname: null, port: null,
967
- pathname: `/${pattern}`,
968
- query: null, fragment: null,
1081
+ const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
1082
+ const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
1083
+ await Owner.kernelId(this.#db, workspaceId);
1084
+ const surveys = [
1085
+ {
1086
+ statement: {
1087
+ op: "FIND", suffix: "", signal: ["+init"],
1088
+ target: { kind: "local", raw: "*" },
1089
+ body: null,
1090
+ lineMarker: fileCap === null ? null : { marks: [1, fileCap] },
1091
+ position: UNKNOWN_POSITION,
969
1092
  },
970
- body: null,
971
- lineMarker: catalogMarker,
972
- position: UNKNOWN_POSITION,
973
- };
974
- await this.dispatch({
975
- statement: catalogFind, workspaceId, workerId, loopId, turnId,
976
- sequence: nextActionIndex, origin: "plurnk", onDispatch,
977
- });
1093
+ exemplar: `## FIND0 [+init] (*)${fileCap === null ? "" : ` <1,${fileCap}>`}`,
1094
+ },
1095
+ {
1096
+ statement: {
1097
+ op: "FIND", suffix: "", signal: ["+init"],
1098
+ target: { kind: "url", raw: "worker:///*", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: "/*", query: null, fragment: null },
1099
+ body: null, lineMarker: null, position: UNKNOWN_POSITION,
1100
+ },
1101
+ exemplar: "## FIND0 [+init] (worker:///*)",
1102
+ },
1103
+ {
1104
+ statement: {
1105
+ op: "FIND", suffix: "", signal: ["+init"],
1106
+ target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
1107
+ body: null, lineMarker: null, position: UNKNOWN_POSITION,
1108
+ },
1109
+ exemplar: "## FIND0 [+init] (worker://~/*)",
1110
+ },
1111
+ {
1112
+ statement: {
1113
+ op: "FIND", suffix: "", signal: ["+init", "+docs"],
1114
+ target: { kind: "url", raw: "worker://plurnk/docs/**", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/docs/**", query: null, fragment: null },
1115
+ body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
1116
+ },
1117
+ exemplar: "## FIND0 [+init,+docs] (worker://plurnk/docs/**) <1,-1>",
1118
+ },
1119
+ ];
1120
+ for (const { statement, exemplar } of surveys) {
1121
+ await this.dispatch({ statement, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
978
1122
  nextActionIndex++;
979
- // {§model-entry} — the same FIND, rendered back to DSL for the turn-0 echo
980
- // (the model's own survey, mirrored OPEN). An explicit file cap rides as
981
- // `<1,N>`; ordinary surveys teach the safe markerless default by example.
982
- const target = isFile ? pattern : `${scheme}:///${pattern}`;
983
- turnZeroMoves.push(`<<FIND(${target})${cap === null ? "" : `<1,${cap}>`}::FIND`);
1123
+ turnZeroMoves.push(exemplar);
984
1124
  }
985
- // The kernel's self-documenting surface — FIND(worker://plurnk/docs/**), uncapped,
986
- // always ({§schemes-directory}, published under {§entry-owner}).
987
- await Owner.kernelId(this.#db, workspaceId); // the row exists even before docs materialize — the empty survey is orienting, never 404
988
- const kernelDocsFind = {
989
- op: "FIND", suffix: "", signal: null,
990
- target: { kind: "url", raw: "worker://plurnk/docs/**", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/docs/**", query: null, fragment: null },
991
- body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
992
- };
993
- await this.dispatch({ statement: kernelDocsFind, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
994
- nextActionIndex++;
995
- turnZeroMoves.push("<<FIND(worker://plurnk/docs/**)<1,-1>::FIND");
996
- // {§worker-scheme} — the building worker's own scratch gets the same markerless
997
- // one-level survey in its perspective alone. It always executes: an empty private
998
- // space is useful orientation, not grounds to hide the surface.
999
- const ownFind = {
1000
- op: "FIND", suffix: "", signal: null,
1001
- target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
1002
- body: null, lineMarker: null, position: UNKNOWN_POSITION,
1003
- };
1004
- await this.dispatch({ statement: ownFind, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
1005
- nextActionIndex++;
1006
- turnZeroMoves.push("<<FIND(worker://~/*)::FIND"); // {§model-entry} — the own-space survey, into the turn-0 echo
1007
1125
  }
1008
- // {§model-entry} — mirror the model's turn-0 OPEN at sequence 1: PLAN → the FINDs actually
1009
- // foisted above (real, their results already in the log) → SEND[102]. Dynamic — it reflects
1126
+ // {§worker-initialization-entry} — write the kernel's turn-0 initialization OPEN at sequence 1: PLAN → the FINDs actually
1127
+ // foisted above (real, their results already in the log) → SEND signal 102. Dynamic — it reflects
1010
1128
  // the true survey, never a frozen print — and OPEN: the worked example the model orients on,
1011
- // so the grammar can stay thin. Subsequent turns mirror the model's real output, folded.
1129
+ // so the grammar can stay thin.
1012
1130
  if (workerFirstLoop) {
1013
- const emission = ["<<PLAN:Survey context, then address the prompt.:PLAN", ...turnZeroMoves, "<<SEND[102]:Next, address the prompt using the survey.:SEND"].join("\n");
1014
- await this.#dispatcher.writeModelEntry({ verbatim: emission, workerId, loopId, turnId, sequence: 1, folded: false, origin: "plurnk" });
1131
+ const initialization = [
1132
+ "# PLAN0\n* Initialization complete.\n* Next: address the prompt.",
1133
+ ...turnZeroMoves,
1134
+ "## SEND0 [102]\nNext, address the prompt.",
1135
+ ].join("\n\n");
1136
+ await this.#dispatcher.writeInitializationEntry({ verbatim: initialization, workerId, loopId, turnId, sequence: 1 });
1015
1137
  }
1016
1138
  }
1017
1139
  // {§methods-loop-run-open-paths}: selected workspace paths belong to
@@ -1058,83 +1180,50 @@ export default class Engine {
1058
1180
  });
1059
1181
  // SPEC {§grinder} — budget grinder, pre-LLM: reclaim window on actual overflow.
1060
1182
  const enforced = await this.#packets.enforceBudget({
1061
- packet: requestPacket, provider, workerId, loopId, turnId,
1062
- // The overflow error row is minted at the turn's running sequence (nextActionIndex), pre-generate;
1063
- // runTurn advances the counter past it below so the post-generate dispatch rows never collide.
1064
- mintSequence: nextActionIndex,
1065
- // Rebuilds re-derive durable errors and retain the one drained notice
1066
- // set; neither path can duplicate or swallow a product failure.
1183
+ packet: requestPacket, provider, loopId, turnId,
1184
+ recordOverflow: async (pressure) => {
1185
+ await this.#problems.record({
1186
+ workerId,
1187
+ loopId,
1188
+ turnId,
1189
+ sequence: nextActionIndex++,
1190
+ origin: "plurnk",
1191
+ source: "engine",
1192
+ result: tokenBudgetOverflowFailure(pressure),
1193
+ });
1194
+ },
1067
1195
  rebuild: () => this.#packets.buildRequestPacket({
1068
1196
  initialMessages: messages, requirements, workspaceId, workerId, loopId,
1069
1197
  currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
1070
1198
  }),
1071
1199
  });
1072
- if (enforced.recorded)
1073
- nextActionIndex += 1; // a fold-to-fit Problem consumed the reserved sequence
1074
1200
  requestPacket = enforced.packet;
1075
1201
  if (!enforced.fit) {
1076
- // {§grinder-hard-413-recovery}/{§grinder-hard-413-abort} — admit
1077
- // one physically sendable informed recovery turn; a physical
1078
- // overflow or consecutive policy overflow terminates immediately.
1079
- let physicalAdmission = await this.#packets.physicalAdmission(requestPacket, provider, this.#loopAborts.get(loopId)?.signal);
1080
- let recoveryAdmitted = false;
1081
- if (physicalAdmission.admitted && !this.#hardOverflowRecovery.has(loopId)) {
1202
+ // {§tokenomics-context-envelope-admission}: the overflow Problem is
1203
+ // already durable. Remaining ruler debt may still be admitted; only
1204
+ // the effective total context envelope can terminally reject the turn.
1205
+ const contextAdmission = await this.#packets.contextEnvelopeAdmission(requestPacket, provider, this.#loopAborts.get(loopId)?.signal);
1206
+ if (!contextAdmission.admitted) {
1082
1207
  const ceiling = this.#packets.ceilingFor(provider);
1083
1208
  if (ceiling === null) {
1084
- throw new Error("an unbounded prompt budget cannot enter budget recovery");
1209
+ throw new Error("an unbounded prompt budget cannot enter context-envelope admission");
1085
1210
  }
1211
+ const failure = contextEnvelopeFailure(budgetPressure(requestPacket.tokens, ceiling), contextAdmission);
1086
1212
  await this.#problems.record({
1087
1213
  workerId,
1088
1214
  loopId,
1089
1215
  turnId,
1090
1216
  sequence: nextActionIndex++,
1091
- origin: "model",
1217
+ origin: "plurnk",
1092
1218
  source: "engine",
1093
- result: BudgetOverflow.result(requestPacket.tokens, ceiling, true),
1219
+ result: failure,
1094
1220
  });
1095
- // Rebuild so the recovery-steer row just minted renders in THIS packet's log +
1096
- // errors sections (the same re-derive contract the soft grind uses).
1221
+ // Preserve the exact terminal evidence in the stored request. It is
1222
+ // never sent because admission has already failed.
1097
1223
  requestPacket = await this.#packets.buildRequestPacket({
1098
1224
  initialMessages: messages, requirements, workspaceId, workerId, loopId,
1099
1225
  currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
1100
1226
  });
1101
- physicalAdmission = await this.#packets.physicalAdmission(requestPacket, provider, this.#loopAborts.get(loopId)?.signal);
1102
- if (physicalAdmission.admitted) {
1103
- this.#hardOverflowRecovery.add(loopId);
1104
- recoveryAdmitted = true;
1105
- }
1106
- }
1107
- if (!recoveryAdmitted) {
1108
- // Hard 413: physically unsendable, or still over after the informed recovery turn.
1109
- const ceiling = this.#packets.ceilingFor(provider);
1110
- if (ceiling === null) {
1111
- throw new Error("an unbounded prompt budget cannot hard-stop");
1112
- }
1113
- if (!enforced.recorded || !physicalAdmission.admitted) {
1114
- await this.#problems.record({
1115
- workerId,
1116
- loopId,
1117
- turnId,
1118
- sequence: nextActionIndex++,
1119
- origin: "plurnk",
1120
- source: "engine",
1121
- result: BudgetOverflow.result(requestPacket.tokens, ceiling, false, physicalAdmission.admitted
1122
- ? undefined
1123
- : {
1124
- reason: physicalAdmission.reason,
1125
- detail: physicalAdmission.detail,
1126
- capacity: physicalAdmission.capacity,
1127
- tokens: physicalAdmission.measurement?.tokens,
1128
- tokenKind: physicalAdmission.measurement?.kind,
1129
- tokenSource: physicalAdmission.measurement?.source,
1130
- }),
1131
- });
1132
- requestPacket = await this.#packets.buildRequestPacket({
1133
- initialMessages: messages, requirements, workspaceId, workerId, loopId,
1134
- currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
1135
- });
1136
- }
1137
- // Skip the LLM, close the turn, and let runLoop abandon.
1138
1227
  await this.#db.engine_close_turn.run({
1139
1228
  id: turnId, status: 413, packet: StoredPacket.stringify(requestPacket),
1140
1229
  // The attempted turn retains its effective allowance even
@@ -1147,20 +1236,14 @@ export default class Engine {
1147
1236
  status: 413,
1148
1237
  outcomes: [],
1149
1238
  fingerprint: "",
1150
- budgetStruck: enforced.struck,
1151
1239
  budgetHardStop: true,
1152
1240
  steerStruck: false,
1153
1241
  emissionAttempts: 0,
1154
1242
  emissionExhausted: false,
1155
- budget: BudgetOverflow.measure(requestPacket.tokens, ceiling),
1243
+ budgetFailure: failure,
1156
1244
  };
1157
1245
  }
1158
1246
  }
1159
- else {
1160
- // A fitting turn clears the recovery grant; a later independent overflow can earn
1161
- // a fresh recovery turn (chronic overflow still strikes out via the rail).
1162
- this.#hardOverflowRecovery.delete(loopId);
1163
- }
1164
1247
  const modelMessages = PacketWire.packetToWireMessages(requestPacket);
1165
1248
  // Packet pressure and provider generation are independent. The grinder governs
1166
1249
  // only the request packet; maxTokens comes only from the provider envelope and
@@ -1168,53 +1251,38 @@ export default class Engine {
1168
1251
  let response;
1169
1252
  let splitResponse;
1170
1253
  let railGrammar;
1254
+ let railResponseGrammar;
1171
1255
  let railEvidence;
1172
1256
  let emissionAttempts = 0;
1173
1257
  let providerCallInFlight = false;
1174
1258
  let providerAttemptSequence = 0;
1175
1259
  let providerAttemptId = null;
1260
+ let providerModelCall = null;
1176
1261
  let providerAttemptAttributions = [];
1177
1262
  const providerSignal = this.#loopAborts.get(loopId)?.signal ?? signal;
1178
1263
  // {§client-metadata}
1179
1264
  const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
1180
- const observeProviderAttempt = async (id, attemptResponse) => {
1181
- const attemptUsage = attemptResponse.assistant.usage;
1182
- await this.#db.engine_observe_turn_attempt_response.run({
1183
- id,
1184
- response: JSON.stringify(attemptResponse),
1185
- usage_prompt: attemptUsage.prompt,
1186
- usage_completion: attemptUsage.completion,
1187
- usage_reasoning: attemptUsage.reasoning,
1188
- usage_cached: attemptUsage.cached,
1189
- usage_cost: JSON.stringify({
1190
- kind: "unknown",
1191
- reason: "provider response retained before monetary classification",
1192
- }),
1193
- finish_reason: attemptResponse.assistant.finishReason,
1194
- model: attemptResponse.assistant.model,
1195
- });
1196
- };
1197
- const classifyProviderAttempt = async (id, attemptResponse, attemptSplit, sequence, accepted, failure = null) => {
1198
- const attemptUsage = attemptSplit.callMetadata.usage;
1199
- const attemptCost = providerCostFor(provider, attemptUsage, attemptResponse.charge);
1200
- const attemptCostUsd = providerCostUsd(attemptCost);
1201
- await this.#db.engine_classify_turn_attempt_response.run({
1265
+ const loopSeq = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
1266
+ const providerIdentity = await this.resolveWorkerProviderIdentity(workerId);
1267
+ const { workerId: providerWorkerId, primaryWorkerId } = providerIdentity;
1268
+ const classifyProviderAttempt = async (id, attemptSplit, sequence, accepted) => {
1269
+ const result = await this.#db.engine_classify_turn_attempt_response.run({
1202
1270
  id,
1203
1271
  accepted: accepted ? 1 : 0,
1204
1272
  parse_errors: JSON.stringify(attemptSplit.parseErrors),
1205
- failure: failure === null ? null : JSON.stringify(failure),
1206
- usage_cost: JSON.stringify(attemptCost),
1207
- usage_cost_usd: attemptCostUsd,
1208
1273
  });
1274
+ if (result.changes !== 1) {
1275
+ throw new Error(`emission attempt ${id} was not awaiting classification`);
1276
+ }
1209
1277
  emissionAttempts = sequence;
1210
1278
  };
1211
1279
  try {
1212
1280
  // {§turn-lifecycle}: bracket the complete provider-attempt window with liveness notices.
1213
1281
  if (!signal?.aborted)
1214
1282
  this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
1215
- const loopSeq = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
1216
- railGrammar = await this.#grammarConstraint(provider);
1217
- const primaryWorkerId = String(await this.resolveWorkerPrimary(workerId));
1283
+ const railConstraint = await this.#grammarConstraint(provider);
1284
+ railGrammar = railConstraint?.transport;
1285
+ railResponseGrammar = railConstraint?.response;
1218
1286
  const attemptLimit = readEmissionAttempts();
1219
1287
  const maxTokens = this.#packets.maxTokensFor(provider) ?? undefined;
1220
1288
  const strikeStreak = this.#strikes.streak(loopId);
@@ -1226,7 +1294,7 @@ export default class Engine {
1226
1294
  providerAttemptSequence = attempt;
1227
1295
  const attributionContext = Object.freeze({
1228
1296
  workspaceId: String(workspaceId),
1229
- workerId: String(workerId),
1297
+ workerId: providerWorkerId,
1230
1298
  primaryWorkerId,
1231
1299
  loop: loopSeq,
1232
1300
  turn: seq,
@@ -1234,51 +1302,67 @@ export default class Engine {
1234
1302
  });
1235
1303
  providerAttemptAttributions = await this.#attemptAttributions(provider, attributionContext);
1236
1304
  requestPacket = { ...requestPacket, attributions: providerAttemptAttributions };
1237
- const attemptRow = await this.#db.engine_open_turn_attempt.get({
1238
- turn_id: turnId,
1305
+ providerModelCall = await ModelCall.open(this.#db, {
1306
+ turnId,
1239
1307
  sequence: attempt,
1240
- attributions: JSON.stringify(providerAttemptAttributions),
1308
+ kind: "emission",
1309
+ attributions: providerAttemptAttributions,
1241
1310
  model: provider.model,
1242
1311
  });
1312
+ const attemptRow = await this.#db.engine_open_turn_attempt.get({
1313
+ model_call_id: providerModelCall.id,
1314
+ });
1243
1315
  if (attemptRow === undefined) {
1244
1316
  throw new Error(`Engine.runTurn: provider attempt ${attempt} did not open`);
1245
1317
  }
1246
1318
  providerAttemptId = attemptRow.id;
1247
1319
  providerCallInFlight = true;
1320
+ const currentModelCall = providerModelCall;
1248
1321
  const completedResponse = await observed(// {§observability-boundary}
1249
1322
  "provider.generate", { model: provider.model, attempt }, async (span) => {
1250
- const generated = await provider.generate({
1251
- messages: modelMessages,
1252
- workerId: String(workerId),
1253
- primaryWorkerId,
1254
- signal: providerSignal,
1255
- grammar: railGrammar,
1256
- maxTokens,
1257
- strikes: strikeStreak,
1258
- attributions: providerAttemptAttributions.length > 0
1259
- ? providerAttemptAttributions
1260
- : undefined,
1261
- client: client ?? undefined,
1262
- workspaceId: String(workspaceId),
1263
- loop: loopSeq,
1264
- turn: seq,
1265
- }); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
1266
- providerCallInFlight = false;
1267
- recordCounter(PROVIDER_CALLS, {
1268
- model: provider.model,
1269
- attempt,
1270
- status: "resolved",
1271
- });
1272
- span.setAttribute("status", "resolved");
1273
- return generated;
1323
+ try {
1324
+ const generated = await provider.generate({
1325
+ messages: modelMessages,
1326
+ workerId: providerWorkerId,
1327
+ primaryWorkerId,
1328
+ signal: providerSignal,
1329
+ grammar: railGrammar,
1330
+ maxTokens,
1331
+ strikes: strikeStreak,
1332
+ attributions: providerAttemptAttributions.length > 0
1333
+ ? providerAttemptAttributions
1334
+ : undefined,
1335
+ client: client ?? undefined,
1336
+ workspaceId: String(workspaceId),
1337
+ loop: loopSeq,
1338
+ turn: seq,
1339
+ observeRequest: currentModelCall.observeRequest,
1340
+ callKind: "emission",
1341
+ }); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
1342
+ currentModelCall.assertAccounting(generated.accounting);
1343
+ providerCallInFlight = false;
1344
+ recordCounter(PROVIDER_CALLS, {
1345
+ model: provider.model,
1346
+ attempt,
1347
+ status: "resolved",
1348
+ });
1349
+ span.setAttribute("status", "resolved");
1350
+ return generated;
1351
+ }
1352
+ catch (error) {
1353
+ if (error instanceof ProviderError) {
1354
+ currentModelCall.assertAccounting(error.accounting);
1355
+ }
1356
+ throw error;
1357
+ }
1274
1358
  });
1275
1359
  response = completedResponse;
1276
- await observeProviderAttempt(attemptRow.id, completedResponse);
1360
+ await currentModelCall.observeResponse(completedResponse);
1277
1361
  railEvidence = railGrammar === undefined
1278
1362
  ? undefined
1279
- : Engine.#requireGrammarEvidence(completedResponse);
1363
+ : _a.#requireGrammarEvidence(completedResponse);
1280
1364
  splitResponse = this.#splitResponse(completedResponse);
1281
- await classifyProviderAttempt(attemptRow.id, completedResponse, splitResponse, attempt, splitResponse.emissionValid);
1365
+ await classifyProviderAttempt(attemptRow.id, splitResponse, attempt, splitResponse.emissionValid);
1282
1366
  if (splitResponse.emissionValid)
1283
1367
  break;
1284
1368
  }
@@ -1288,44 +1372,26 @@ export default class Engine {
1288
1372
  catch (err) {
1289
1373
  // This handler owns only provider-call failures. Parser, cost, SQL,
1290
1374
  // and engine-contract failures retain their original source.
1375
+ if (err instanceof ModelCallPersistenceError || err instanceof ProviderAccountingIntegrityError)
1376
+ throw err;
1291
1377
  if (!providerCallInFlight)
1292
1378
  throw err;
1293
1379
  providerCallInFlight = false;
1294
- if (providerAttemptId === null) {
1295
- throw new Error("provider call failed without a durable attempt identity", { cause: err });
1380
+ if (providerAttemptId === null || providerModelCall === null) {
1381
+ throw new Error("provider call failed without durable model-call and attempt identities", { cause: err });
1296
1382
  }
1297
- const failure = err instanceof ProviderError
1298
- ? { status: err.problem.status, problem: err.problem }
1299
- : providerSignal?.aborted === true
1300
- ? Results.failure("engine:provider", providerSignal.reason === LOOP_TIMEOUT_REASON ? "provider-call-timeout" : "provider-call-cancelled", providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499, providerSignal.reason === LOOP_TIMEOUT_REASON
1301
- ? "The provider call was interrupted by the loop deadline."
1302
- : "The provider call was interrupted by loop cancellation.", {}, { stage: "provider-request", retryable: false })
1303
- : (() => {
1304
- console.error("Provider failed outside its Problem Details contract:", err);
1305
- return Results.failure("engine:provider", "provider-contract-violation", 502, "The provider failed without returning its required Problem Details.", {}, {
1306
- stage: "provider-request",
1307
- retryable: false,
1308
- });
1309
- })();
1383
+ const failure = _a.#providerFailure(err, providerSignal);
1310
1384
  // {§provider-interrupted-attempt} — a provider-declared interruption
1311
1385
  // carries response evidence without becoming a completed exchange.
1312
1386
  // Persist it as an unaccepted attempt before settling the failure.
1313
1387
  if (err instanceof ProviderError && err.attempt !== undefined) {
1314
1388
  response = err.attempt;
1315
- await observeProviderAttempt(providerAttemptId, response);
1389
+ await providerModelCall.observeResponse(response, failure);
1316
1390
  splitResponse = this.#splitResponse(response);
1317
- await classifyProviderAttempt(providerAttemptId, response, splitResponse, providerAttemptSequence, false, failure);
1391
+ await classifyProviderAttempt(providerAttemptId, splitResponse, providerAttemptSequence, false);
1318
1392
  }
1319
1393
  else {
1320
- const unknownCost = {
1321
- kind: "unknown",
1322
- reason: "provider call failed without response-bearing charge evidence",
1323
- };
1324
- await this.#db.engine_fail_turn_attempt.run({
1325
- id: providerAttemptId,
1326
- failure: JSON.stringify(failure),
1327
- usage_cost: JSON.stringify(unknownCost),
1328
- });
1394
+ await providerModelCall.fail(failure);
1329
1395
  emissionAttempts = providerAttemptSequence;
1330
1396
  }
1331
1397
  // {§turn-never-blank} — a ProviderError means no completed exchange exists.
@@ -1370,9 +1436,10 @@ export default class Engine {
1370
1436
  });
1371
1437
  throw new OperationFailureError(recorded.result, { cause: err });
1372
1438
  }
1373
- if (response === undefined || splitResponse === undefined) {
1439
+ if (response === undefined || splitResponse === undefined || providerModelCall === null) {
1374
1440
  throw new Error("provider attempt loop completed without a response");
1375
1441
  }
1442
+ const emissionModelCallId = providerModelCall.id;
1376
1443
  if (!splitResponse.emissionValid) {
1377
1444
  // {§invalid-emission-attempts} The first consecutive exhaustion
1378
1445
  // publishes only the raw final response and generic recovery fact.
@@ -1385,6 +1452,7 @@ export default class Engine {
1385
1452
  turnId,
1386
1453
  sequence: nextActionIndex,
1387
1454
  folded: true,
1455
+ modelCallId: emissionModelCallId,
1388
1456
  admission: "rejected",
1389
1457
  });
1390
1458
  this.#notices.push(workspaceId, loopId, {
@@ -1408,7 +1476,6 @@ export default class Engine {
1408
1476
  status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
1409
1477
  outcomes: [],
1410
1478
  fingerprint: "",
1411
- budgetStruck: enforced.struck,
1412
1479
  budgetHardStop: false,
1413
1480
  steerStruck: false,
1414
1481
  emissionAttempts,
@@ -1446,12 +1513,14 @@ export default class Engine {
1446
1513
  if (railGrammar !== undefined) {
1447
1514
  if (railEvidence === undefined)
1448
1515
  throw new Error("configured GBNF response has no final grammar evidence");
1516
+ if (railResponseGrammar === undefined)
1517
+ throw new Error("configured GBNF has no response grammar");
1449
1518
  let verdict = null;
1450
1519
  try {
1451
- verdict = validateGbnf(railGrammar, railEvidence.input);
1520
+ verdict = validateGbnf(railResponseGrammar, railEvidence.input);
1452
1521
  }
1453
1522
  catch (cause) {
1454
- Engine.#warnRailVerdictGapOnce(cause.message);
1523
+ _a.#warnRailVerdictGapOnce(cause.message);
1455
1524
  }
1456
1525
  railKeys = {
1457
1526
  railsAttached: railEvidence.transported ? "client" : "withheld",
@@ -1551,6 +1620,8 @@ export default class Engine {
1551
1620
  origin, onDispatch,
1552
1621
  });
1553
1622
  const droppedCount = opsCount - opsToDispatch.length;
1623
+ const bareStatements = opsToDispatch.filter((statement) => statement.op === "BARE");
1624
+ let bareResults = null;
1554
1625
  const outcomes = [];
1555
1626
  // Running counter — a multi-file READ writes N rows from one statement (rowsWritten),
1556
1627
  // so the next op's sequence picks up after them. Collapses to nextActionIndex+i when
@@ -1596,11 +1667,45 @@ export default class Engine {
1596
1667
  }
1597
1668
  const result = await observed(// {§observability-boundary}
1598
1669
  "op.dispatch", { op: statement.op }, async (span) => {
1599
- const dispatchResult = await this.#dispatcher.dispatch({
1600
- statement, workspaceId, workerId, loopId, turnId,
1601
- sequence: rowSeq,
1602
- origin, onDispatch,
1603
- });
1670
+ let dispatchResult;
1671
+ if (statement.op === "BARE") {
1672
+ if (bareResults === null) {
1673
+ const batch = await this.#runBareBatch({
1674
+ statements: bareStatements,
1675
+ provider: childProvider,
1676
+ turnId,
1677
+ modelCallSequenceStart: providerAttemptSequence + 1,
1678
+ workspaceId,
1679
+ workerId,
1680
+ primaryWorkerId,
1681
+ loopSequence: loopSeq,
1682
+ turnSequence: seq,
1683
+ signal: providerSignal,
1684
+ });
1685
+ bareResults = new Map(batch.map((item) => [item.statement, item]));
1686
+ }
1687
+ const bare = bareResults.get(statement);
1688
+ if (bare === undefined) {
1689
+ throw new Error("BARE statement reached dispatch without its batch result");
1690
+ }
1691
+ dispatchResult = await this.#dispatcher.recordBareResult({
1692
+ statement,
1693
+ workspaceId,
1694
+ workerId,
1695
+ loopId,
1696
+ turnId,
1697
+ sequence: rowSeq,
1698
+ origin,
1699
+ onDispatch,
1700
+ }, bare.result, bare.modelCallId);
1701
+ }
1702
+ else {
1703
+ dispatchResult = await this.#dispatcher.dispatch({
1704
+ statement, workspaceId, workerId, loopId, turnId,
1705
+ sequence: rowSeq,
1706
+ origin, onDispatch,
1707
+ });
1708
+ }
1604
1709
  span.setAttribute("status", dispatchResult.status);
1605
1710
  recordCounter(OPS_DISPATCHED, { op: statement.op, status: dispatchResult.status });
1606
1711
  return dispatchResult;
@@ -1661,7 +1766,7 @@ export default class Engine {
1661
1766
  }
1662
1767
  : {
1663
1768
  stage: "turn",
1664
- recovery: "Perform an operation before continuing with SEND[102].",
1769
+ recovery: "Perform an operation before continuing with `## SEND0 [102]`.",
1665
1770
  retryable: false,
1666
1771
  };
1667
1772
  await this.#problems.record({
@@ -1687,14 +1792,22 @@ export default class Engine {
1687
1792
  ? response.assistant.reasoningEncrypted
1688
1793
  : undefined;
1689
1794
  if (packetAssistant.content.trim().length > 0 || reasoningItems !== undefined) {
1690
- await this.#dispatcher.writeModelEntry({ verbatim: packetAssistant.content, workerId, loopId, turnId, sequence: errSeq++, folded: true, ...(reasoningItems !== undefined ? { reasoningItems } : {}) });
1795
+ await this.#dispatcher.writeModelEntry({
1796
+ verbatim: packetAssistant.content,
1797
+ workerId,
1798
+ loopId,
1799
+ turnId,
1800
+ sequence: errSeq++,
1801
+ folded: true,
1802
+ modelCallId: emissionModelCallId,
1803
+ ...(reasoningItems !== undefined ? { reasoningItems } : {}),
1804
+ });
1691
1805
  }
1692
1806
  return {
1693
1807
  turnId,
1694
1808
  status: turnStatus,
1695
1809
  outcomes,
1696
1810
  fingerprint: StrikeRail.fingerprintTurn(packetAssistant.ops),
1697
- budgetStruck: enforced.struck,
1698
1811
  budgetHardStop: false,
1699
1812
  steerStruck,
1700
1813
  emissionAttempts,
@@ -1703,7 +1816,8 @@ export default class Engine {
1703
1816
  }
1704
1817
  // Split the wire-level ProviderResponse into the two destinations:
1705
1818
  // packet.assistant gets the model's emission (content, ops, reasoning);
1706
- // Turn columns get the call-metadata (usage, finishReason, model).
1819
+ // Turn columns get accepted-call metadata (finishReason, model). Physical
1820
+ // request usage and cost have their own cardinal persistence path.
1707
1821
  // {§provider-surface} Text-fragment scraping policy lives
1708
1822
  // here — engine owns the parse and the scraping rule, providers stay
1709
1823
  // grammar-unaware.
@@ -1793,7 +1907,7 @@ export default class Engine {
1793
1907
  const reasoning = assistant.reasoning ?? null;
1794
1908
  return {
1795
1909
  packetAssistant: { content: assistant.content, ops, reasoning },
1796
- callMetadata: { usage: assistant.usage, finishReason: assistant.finishReason, model: assistant.model },
1910
+ callMetadata: { finishReason: assistant.finishReason, model: assistant.model },
1797
1911
  parseErrors,
1798
1912
  recoverableParseErrors: emissionValid ? recoverableParseErrors : [],
1799
1913
  parseNotices,
@@ -1834,6 +1948,14 @@ export default class Engine {
1834
1948
  throw new Error(`ambient loop-termination event ${r.event_id} status ${r.status_rx} does not match its terminal result status ${terminal.status}`);
1835
1949
  }
1836
1950
  let attrs = r.attrs ?? "{}";
1951
+ const parsedTags = JSON.parse(r.tags ?? "[]");
1952
+ if (!Array.isArray(parsedTags) || !parsedTags.every((tag) => typeof tag === "string" && tag.length > 0)) {
1953
+ throw new TypeError(`ambient event ${r.event_id} tags must be an array of nonempty strings`);
1954
+ }
1955
+ const tags = [...new Set(parsedTags)].toSorted();
1956
+ if (tags.length !== parsedTags.length || tags.some((tag, index) => tag !== parsedTags[index])) {
1957
+ throw new TypeError(`ambient event ${r.event_id} tags must be unique and sorted`);
1958
+ }
1837
1959
  if (terminal !== null) {
1838
1960
  const inherited = JSON.parse(attrs);
1839
1961
  if (inherited === null || typeof inherited !== "object" || Array.isArray(inherited)) {
@@ -1861,6 +1983,15 @@ export default class Engine {
1861
1983
  expanded: terminal !== null && terminal.status >= 200 && terminal.status < 300 ? 1 : 0,
1862
1984
  attrs,
1863
1985
  });
1986
+ const materialized = inserted ?? await this.#db.engine_ambient_delta_id.get({
1987
+ worker_id: workerId,
1988
+ event_id: r.event_id,
1989
+ });
1990
+ if (materialized === undefined)
1991
+ throw new Error(`ambient event ${r.event_id} has no observer log row after materialization`);
1992
+ for (const tag of tags) {
1993
+ await this.#db.log_write_tag.run({ log_entry_id: materialized.id, tag });
1994
+ }
1864
1995
  if (inserted !== undefined)
1865
1996
  written++;
1866
1997
  }
@@ -2010,7 +2141,8 @@ export default class Engine {
2010
2141
  const span = editedSpan(d.before, d.after);
2011
2142
  await this.#db.engine_insert_log_entry.get({
2012
2143
  worker_id: worker.id, loop_id: loop.id, turn_id: turn.id, sequence: sequence++,
2013
- origin: "plurnk", source: "file", op: "EDIT", suffix: "", signal: null,
2144
+ origin: "plurnk", source: "file", model_call_id: null,
2145
+ op: "EDIT", suffix: "", signal: null,
2014
2146
  // Match Dispatcher.#extractTarget: a bare file address has NULL scheme
2015
2147
  // only in log target metadata; its entry identity remains `file`.
2016
2148
  scheme: null, username: null, password: null, hostname: null, port: null,
@@ -2051,6 +2183,7 @@ export default class Engine {
2051
2183
  sequence,
2052
2184
  origin: "plurnk",
2053
2185
  source: null,
2186
+ model_call_id: null,
2054
2187
  op: "prompt",
2055
2188
  suffix: "",
2056
2189
  signal: null,
@@ -2177,11 +2310,12 @@ export default class Engine {
2177
2310
  };
2178
2311
  const entry = {
2179
2312
  channels: { body: { content: prompt, mimetype: "text/markdown" } },
2180
- tags: [],
2181
2313
  attributes: { openPaths },
2182
2314
  };
2183
2315
  await EntryCrud.writeEntry(pathname, entry, ctx, "prompt", workerId);
2184
2316
  return { loopId, turnSeq };
2185
2317
  }
2186
2318
  }
2319
+ _a = Engine;
2320
+ export default Engine;
2187
2321
  //# sourceMappingURL=Engine.js.map