@plurnk/plurnk-service 1.9.1 → 1.10.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 (76) hide show
  1. package/.env.defaults +3 -5
  2. package/SPEC.md +69 -32
  3. package/dist/build-info.json +1 -1
  4. package/dist/core/BranchReceipt.d.ts.map +1 -1
  5. package/dist/core/BranchReceipt.js +9 -4
  6. package/dist/core/BranchReceipt.js.map +1 -1
  7. package/dist/core/CoreSchemeServices.d.ts +1 -0
  8. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  9. package/dist/core/CoreSchemeServices.js +1 -0
  10. package/dist/core/CoreSchemeServices.js.map +1 -1
  11. package/dist/core/Dispatcher.d.ts +2 -1
  12. package/dist/core/Dispatcher.d.ts.map +1 -1
  13. package/dist/core/Dispatcher.js +49 -13
  14. package/dist/core/Dispatcher.js.map +1 -1
  15. package/dist/core/Engine.d.ts +3 -1
  16. package/dist/core/Engine.d.ts.map +1 -1
  17. package/dist/core/Engine.js +18 -17
  18. package/dist/core/Engine.js.map +1 -1
  19. package/dist/core/Engine.sql +34 -10
  20. package/dist/core/LoopPacket.d.ts +3 -0
  21. package/dist/core/LoopPacket.d.ts.map +1 -0
  22. package/dist/core/LoopPacket.js +2 -0
  23. package/dist/core/LoopPacket.js.map +1 -0
  24. package/dist/core/NoticeChannel.d.ts.map +1 -1
  25. package/dist/core/NoticeChannel.js +14 -2
  26. package/dist/core/NoticeChannel.js.map +1 -1
  27. package/dist/core/OverflowTurn.d.ts.map +1 -1
  28. package/dist/core/OverflowTurn.js +0 -1
  29. package/dist/core/OverflowTurn.js.map +1 -1
  30. package/dist/core/PacketBuilder.d.ts.map +1 -1
  31. package/dist/core/PacketBuilder.js +9 -2
  32. package/dist/core/PacketBuilder.js.map +1 -1
  33. package/dist/core/ResourceMutations.d.ts.map +1 -1
  34. package/dist/core/ResourceMutations.js +21 -3
  35. package/dist/core/ResourceMutations.js.map +1 -1
  36. package/dist/core/TerminalResult.d.ts.map +1 -1
  37. package/dist/core/TerminalResult.js +4 -1
  38. package/dist/core/TerminalResult.js.map +1 -1
  39. package/dist/core/ToolResources.js +4 -4
  40. package/dist/core/ToolResources.js.map +1 -1
  41. package/dist/core/TurnOps.d.ts.map +1 -1
  42. package/dist/core/TurnOps.js +3 -0
  43. package/dist/core/TurnOps.js.map +1 -1
  44. package/dist/core/TurnRunner.d.ts +3 -1
  45. package/dist/core/TurnRunner.d.ts.map +1 -1
  46. package/dist/core/TurnRunner.js +153 -160
  47. package/dist/core/TurnRunner.js.map +1 -1
  48. package/dist/core/packet-wire.d.ts.map +1 -1
  49. package/dist/core/packet-wire.js +5 -1
  50. package/dist/core/packet-wire.js.map +1 -1
  51. package/dist/core/scheme-types.d.ts +1 -0
  52. package/dist/core/scheme-types.d.ts.map +1 -1
  53. package/dist/schemes/Exec.d.ts.map +1 -1
  54. package/dist/schemes/Exec.js +26 -16
  55. package/dist/schemes/Exec.js.map +1 -1
  56. package/dist/schemes/_entry-find.d.ts.map +1 -1
  57. package/dist/schemes/_entry-find.js +24 -4
  58. package/dist/schemes/_entry-find.js.map +1 -1
  59. package/dist/schemes/_entry-graph.js +1 -1
  60. package/dist/schemes/_entry-graph.js.map +1 -1
  61. package/dist/schemes/_entry-send.d.ts.map +1 -1
  62. package/dist/schemes/_entry-send.js +3 -1
  63. package/dist/schemes/_entry-send.js.map +1 -1
  64. package/dist/schemes/_search-index.d.ts +1 -1
  65. package/dist/schemes/_search-index.d.ts.map +1 -1
  66. package/dist/schemes/_search-index.js +39 -35
  67. package/dist/schemes/_search-index.js.map +1 -1
  68. package/dist/server/Daemon.d.ts.map +1 -1
  69. package/dist/server/Daemon.js +4 -0
  70. package/dist/server/Daemon.js.map +1 -1
  71. package/dist/server/branch-batch.sql +4 -2
  72. package/dist/server/dispatch-as-plurnk.js +1 -1
  73. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  74. package/dist/server/seam-loop.sql +5 -1
  75. package/migrations/001_schema.sql +4 -1
  76. package/package.json +32 -32
@@ -1,5 +1,5 @@
1
1
  var _a;
2
- import { PathSyntax, PlurnkParser, PlurnkParseError, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
2
+ import { PlurnkParser, PlurnkParseError, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
3
3
  const TERMINAL_SEND_SIGNALS = new Set([102, 200, 202, 499]);
4
4
  const comparePosition = (a, b) => a.line - b.line || a.column - b.column;
5
5
  import Meta from "@plurnk/plurnk-meta";
@@ -8,7 +8,8 @@ import GitMembership from "./git-membership.js";
8
8
  import GitState from "./git-state.js";
9
9
  import WorkspaceSettings from "./workspace-settings.js";
10
10
  import { editedSpan } from "../content/index.js";
11
- import { authorityParts, generatedPathname, promptPathname, promptLoopPrefix, renderAddress } from "./plurnk-uri.js";
11
+ import ReadResolve from "../content/read-resolve.js";
12
+ import { authorityParts, generatedPathname, promptPathname, promptLoopPrefix } from "./plurnk-uri.js";
12
13
  import { readFile } from "node:fs/promises";
13
14
  // {§grammar-rail-registration} — bare names are the built-in rail namespace;
14
15
  // everything else is an import specifier resolved through the Node chain.
@@ -43,18 +44,6 @@ import { readOptimisticSettlementMs } from "./optimistic-settlement.js";
43
44
  import ModelCall, { ModelCallPersistenceError, ProviderAccountingIntegrityError, } from "./ModelCall.js";
44
45
  import OverflowTurn from "./OverflowTurn.js";
45
46
  import TurnOps from "./TurnOps.js";
46
- const RECORD_STREAM_MIMETYPES = new Set(["application/jsonl", "application/x-ndjson"]);
47
- const baseMimetype = (mimetype) => mimetype.split(";", 1)[0].trim().toLowerCase();
48
- // {§exec-stream} Active streams publish only independently meaningful units.
49
- // Atomic documents wait for close; JSONL stops after its last complete record.
50
- const streamPublicationEnd = (content, mimetype, cursor, closed) => {
51
- const type = baseMimetype(mimetype);
52
- if (closed || type.startsWith("text/"))
53
- return content.length;
54
- if (!RECORD_STREAM_MIMETYPES.has(type))
55
- return cursor;
56
- return Math.max(cursor, content.lastIndexOf("\n") + 1);
57
- };
58
47
  const ENGINE_PROBLEMS = Object.freeze({
59
48
  max_commands_exceeded: {
60
49
  status: 429,
@@ -182,6 +171,7 @@ class TurnRunner {
182
171
  #liveSubscriptions;
183
172
  #streamEventNotify;
184
173
  #reasoningEventNotify;
174
+ #loopPacketNotify;
185
175
  #wakeWorkerNotify;
186
176
  #executors;
187
177
  #loopSignal;
@@ -190,7 +180,7 @@ class TurnRunner {
190
180
  #dispatch;
191
181
  #resolveWorkerProviderIdentity;
192
182
  #gbnfCache = new Map();
193
- constructor({ db, schemes, mimetypes, weigh, notices, problems, strikes, packets, dispatcher, liveSubscriptions, streamEventNotify, reasoningEventNotify, wakeWorkerNotify, executors, loopSignal, interactions, warmWorkspace, dispatch, resolveWorkerProviderIdentity, }) {
183
+ constructor({ db, schemes, mimetypes, weigh, notices, problems, strikes, packets, dispatcher, liveSubscriptions, streamEventNotify, reasoningEventNotify, loopPacketNotify, wakeWorkerNotify, executors, loopSignal, interactions, warmWorkspace, dispatch, resolveWorkerProviderIdentity, }) {
194
184
  this.#db = db;
195
185
  this.#schemes = schemes;
196
186
  this.#mimetypes = mimetypes;
@@ -203,6 +193,7 @@ class TurnRunner {
203
193
  this.#liveSubscriptions = liveSubscriptions;
204
194
  this.#streamEventNotify = streamEventNotify;
205
195
  this.#reasoningEventNotify = reasoningEventNotify;
196
+ this.#loopPacketNotify = loopPacketNotify;
206
197
  this.#wakeWorkerNotify = wakeWorkerNotify;
207
198
  this.#executors = executors;
208
199
  this.#loopSignal = loopSignal;
@@ -211,6 +202,19 @@ class TurnRunner {
211
202
  this.#dispatch = dispatch;
212
203
  this.#resolveWorkerProviderIdentity = resolveWorkerProviderIdentity;
213
204
  }
205
+ async #recordInference(args) {
206
+ await Turn.recordInference(this.#db, args.turnId, args.evidence);
207
+ if (this.#loopPacketNotify === undefined)
208
+ return;
209
+ const packetCount = await this.#db.engine_loop_packet_count.get({ loop_id: args.loopId });
210
+ if (packetCount === undefined)
211
+ throw new Error(`loop ${args.loopId}: packet count row missing`);
212
+ this.#loopPacketNotify(args.workspaceId, {
213
+ workerId: args.workerId,
214
+ loopId: args.loopId,
215
+ packetCount: packetCount.count,
216
+ });
217
+ }
214
218
  // {§rail-truth-engine-verdict} — the verify GAP (a configured grammar @plurnk/gbnf can't
215
219
  // parse): warn once per message, never per turn; the turn records railsVerdict "unverifiable".
216
220
  static #railGapWarned = new Set();
@@ -653,6 +657,29 @@ class TurnRunner {
653
657
  requestInteraction: (request) => this.#interactions.request(request, { workspaceId, workerId, loopId, turnId: contextTurnId }, this.#loopSignal(loopId)),
654
658
  });
655
659
  let systemCtx = systemContext(initializationTurn?.id ?? modelTurn.id);
660
+ // {§prompt-entry} — the prompt entry exists before any turn of the loop
661
+ // runs, so the initialization COPY archives a real source; its `prompt`
662
+ // log row is published to the model turn below (one durable publication
663
+ // per loop, decided by that row).
664
+ const promptPublication = turnNumber === 1 && loopRow?.prompt_published === 0
665
+ && typeof loopRow.prompt === "string" && loopRow.prompt.length > 0
666
+ ? {
667
+ content: loopRow.prompt,
668
+ source: loopRow.prompt_source,
669
+ path: promptTarget(loopRow.sequence, 1),
670
+ openPaths: assertOpenPaths(JSON.parse(loopRow.open_paths), `Loop ${loopId} open_paths`),
671
+ }
672
+ : null;
673
+ if (promptPublication !== null) {
674
+ const entry = {
675
+ channels: { body: { content: promptPublication.content, mimetype: "text/markdown" } },
676
+ attributes: {
677
+ openPaths: promptPublication.openPaths,
678
+ ...(promptPublication.source === null ? {} : { source: promptPublication.source }),
679
+ },
680
+ };
681
+ await EntryCrud.writeEntry({ authority: "", pathname: promptPublication.path.pathname }, entry, systemCtx, "prompt", workerId);
682
+ }
656
683
  const initializationStatements = [];
657
684
  // {§worker-initialization-entry} — the worker's first turn is the worked
658
685
  // example itself: an ordinary PLAN, the actual orienting operations,
@@ -663,12 +690,28 @@ class TurnRunner {
663
690
  signal: null, target: null, lineMarker: null,
664
691
  body: [{
665
692
  content: "Discover the tooling available and survey the workspace file root.",
666
- priority: "medium",
667
693
  status: "in_progress",
668
694
  }],
669
695
  position: UNKNOWN_POSITION,
670
696
  };
671
697
  initializationStatements.push(plan);
698
+ // {§worker-initialization-entry} — the prompt is archived into the
699
+ // worker's private space: the worked COPY specimen, and the private
700
+ // space shown as scratch. An append onto an absent entry creates it.
701
+ // Authored first among the operations because it executes first
702
+ // ({§op-mode-phases}: Mutate precedes Observe).
703
+ if (promptPublication !== null) {
704
+ const archive = {
705
+ op: "COPY", delimiter: "", annotation: "append latest prompt", signal: ["+_plurnk", "+backup"],
706
+ target: promptPublication.path, lineMarker: null,
707
+ body: {
708
+ target: { kind: "url", raw: "worker://~/prompts.md", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/prompts.md", query: null, fragment: null },
709
+ lineMarker: { marks: [-1] },
710
+ },
711
+ position: UNKNOWN_POSITION,
712
+ };
713
+ initializationStatements.push(archive);
714
+ }
672
715
  // {§turn0-agents-stunt} — the project AGENTS.md (materialized by LoopDocs as
673
716
  // worker://~/_plurnk/agents.md) gets one foisted READ on the worker's first
674
717
  // loop, so local repo guidance is visible turn-0 content. Global policy
@@ -738,11 +781,11 @@ class TurnRunner {
738
781
  const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
739
782
  const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
740
783
  const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
741
- // {§tools-resource-materialization} — PLURNK_MCP_EXPANDED servers
742
- // contribute one complete family-document READ each, directly
743
- // after the family survey, so turn 0 names every tool they
744
- // expose in order. (The family document is the whole tool
745
- // teaching — per-target child documents do not exist.)
784
+ // {§tools-resource-materialization} — a PLURNK_MCP_EXPANDED server
785
+ // contributes one FIND over its family document matching the
786
+ // `## EXEC0` headings: one row per tool — heading, annotation,
787
+ // signature — paged like every survey. No document is delivered
788
+ // unasked; per-target child documents do not exist.
746
789
  const registry = this.#executors();
747
790
  const toolExpansions = [];
748
791
  for (const tag of registry?.availableRuntimes(workerId) ?? []) {
@@ -751,9 +794,10 @@ class TurnRunner {
751
794
  continue;
752
795
  toolExpansions.push({
753
796
  statement: {
754
- op: "READ", delimiter: "", annotation: `enabled tools: ${tag}`, signal: ["+_plurnk", "+init", "+tools"],
797
+ op: "FIND", delimiter: "", annotation: `enabled tools: ${tag}`, signal: ["+_plurnk", "+init", "+tools"],
755
798
  target: { kind: "url", raw: `worker://~/_plurnk/tools/${tag}.md`, scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: generatedPathname(`/tools/${tag}.md`), query: null, fragment: null },
756
- body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
799
+ body: { dialect: "regex", raw: "/^## EXEC0 .*\\n.*$/m", pattern: "^## EXEC0 .*\\n.*$", flags: "m" },
800
+ lineMarker: null, position: UNKNOWN_POSITION,
757
801
  },
758
802
  });
759
803
  }
@@ -864,32 +908,18 @@ class TurnRunner {
864
908
  // Model operations continue the same turn sequence after these rows.
865
909
  let nextActionIndex = 1;
866
910
  const turnOpenPaths = [];
867
- if (turnNumber === 1 && loopRow?.prompt_published === 0) {
868
- const promptRow = loopRow; // {§prompt-entry} — one durable publication per loop
869
- if (promptRow !== undefined && typeof promptRow.prompt === "string" && promptRow.prompt.length > 0) {
870
- const openPaths = assertOpenPaths(JSON.parse(promptRow.open_paths), `Loop ${loopId} open_paths`);
871
- const promptLoopSeq = promptRow.sequence;
872
- const promptPath = promptTarget(promptLoopSeq, 1);
873
- const entry = {
874
- channels: { body: { content: promptRow.prompt, mimetype: "text/markdown" } },
875
- attributes: {
876
- openPaths,
877
- ...(promptRow.prompt_source === null ? {} : { source: promptRow.prompt_source }),
878
- },
879
- };
880
- await EntryCrud.writeEntry({ authority: "", pathname: promptPath.pathname }, entry, systemCtx, "prompt", workerId);
881
- turnOpenPaths.push(...openPaths);
882
- const promptLogId = await this.#writePromptLog({
883
- workerId,
884
- loopId,
885
- turnId,
886
- sequence: nextActionIndex++,
887
- target: promptPath,
888
- content: promptRow.prompt,
889
- source: promptRow.prompt_source,
890
- });
891
- onDispatch?.(promptLogId);
892
- }
911
+ if (promptPublication !== null) { // {§prompt-entry} — one durable publication per loop
912
+ turnOpenPaths.push(...promptPublication.openPaths);
913
+ const promptLogId = await this.#writePromptLog({
914
+ workerId,
915
+ loopId,
916
+ turnId,
917
+ sequence: nextActionIndex++,
918
+ target: promptPublication.path,
919
+ content: promptPublication.content,
920
+ source: promptPublication.source,
921
+ });
922
+ onDispatch?.(promptLogId);
893
923
  }
894
924
  // {§prompt-loop-containment}: the loop contains every prompt that arrived
895
925
  // while it ran. Publish each undelivered frame oldest-first exactly once.
@@ -1270,12 +1300,15 @@ class TurnRunner {
1270
1300
  // runLoop/Daemon settle the exact 504/499 loop result.
1271
1301
  if (providerSignal?.aborted) {
1272
1302
  const status = providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499;
1273
- await Turn.recordInference(this.#db, turnId, {
1274
- packet: StoredPacket.stringify(requestPacket),
1275
- usageCurationBudget: this.#packets.curationBudgetFor(provider),
1276
- finishReason: splitResponse?.callMetadata.finishReason ?? null,
1277
- model: splitResponse?.callMetadata.model ?? provider.model,
1278
- meta: JSON.stringify(response?.meta ?? {}),
1303
+ await this.#recordInference({
1304
+ workspaceId, workerId, loopId, turnId,
1305
+ evidence: {
1306
+ packet: StoredPacket.stringify(requestPacket),
1307
+ usageCurationBudget: this.#packets.curationBudgetFor(provider),
1308
+ finishReason: splitResponse?.callMetadata.finishReason ?? null,
1309
+ model: splitResponse?.callMetadata.model ?? provider.model,
1310
+ meta: JSON.stringify(response?.meta ?? {}),
1311
+ },
1279
1312
  });
1280
1313
  await Turn.complete(this.#db, turnId, status);
1281
1314
  throw err;
@@ -1292,12 +1325,15 @@ class TurnRunner {
1292
1325
  // The provider call was attempted, but no completed exchange exists.
1293
1326
  // Persist the exact request half and failure status; omitting assistant
1294
1327
  // is materially different from fabricating an empty model turn.
1295
- await Turn.recordInference(this.#db, turnId, {
1296
- packet: StoredPacket.stringify(requestPacket),
1297
- usageCurationBudget: this.#packets.curationBudgetFor(provider),
1298
- finishReason: splitResponse?.callMetadata.finishReason ?? null,
1299
- model: splitResponse?.callMetadata.model ?? provider.model,
1300
- meta: JSON.stringify(response?.meta ?? {}),
1328
+ await this.#recordInference({
1329
+ workspaceId, workerId, loopId, turnId,
1330
+ evidence: {
1331
+ packet: StoredPacket.stringify(requestPacket),
1332
+ usageCurationBudget: this.#packets.curationBudgetFor(provider),
1333
+ finishReason: splitResponse?.callMetadata.finishReason ?? null,
1334
+ model: splitResponse?.callMetadata.model ?? provider.model,
1335
+ meta: JSON.stringify(response?.meta ?? {}),
1336
+ },
1301
1337
  });
1302
1338
  await Turn.complete(this.#db, turnId, recorded.result.status);
1303
1339
  if (capacityFailure) {
@@ -1338,20 +1374,31 @@ class TurnRunner {
1338
1374
  ? { reasoningItems: response.assistant.reasoningEncrypted }
1339
1375
  : {}),
1340
1376
  });
1377
+ // {§invalid-emission-attempts} — the informed turn carries the parser's own
1378
+ // diagnostic and position: the model sees WHY, not only that it was refused.
1379
+ const diagnostic = splitResponse.parseErrors[0];
1341
1380
  this.#notices.push(workspaceId, loopId, {
1342
1381
  source: "engine:grammar",
1343
1382
  kind: "invalid_emission",
1344
1383
  level: "error",
1345
- message: INVALID_EMISSION_RECOVERY_MESSAGE,
1384
+ message: diagnostic === undefined
1385
+ ? INVALID_EMISSION_RECOVERY_MESSAGE
1386
+ : `${INVALID_EMISSION_RECOVERY_MESSAGE} Parser: ${diagnostic.message}`,
1387
+ ...(diagnostic !== undefined && diagnostic.line > 0
1388
+ ? { position: { type: "content-offset", line: diagnostic.line, column: diagnostic.column } }
1389
+ : {}),
1346
1390
  });
1347
1391
  }
1348
1392
  const status = allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500;
1349
- await Turn.recordInference(this.#db, turnId, {
1350
- packet: StoredPacket.stringify(requestPacket),
1351
- usageCurationBudget: this.#packets.curationBudgetFor(provider),
1352
- finishReason: splitResponse.callMetadata.finishReason,
1353
- model: splitResponse.callMetadata.model,
1354
- meta: JSON.stringify(response.meta ?? {}),
1393
+ await this.#recordInference({
1394
+ workspaceId, workerId, loopId, turnId,
1395
+ evidence: {
1396
+ packet: StoredPacket.stringify(requestPacket),
1397
+ usageCurationBudget: this.#packets.curationBudgetFor(provider),
1398
+ finishReason: splitResponse.callMetadata.finishReason,
1399
+ model: splitResponse.callMetadata.model,
1400
+ meta: JSON.stringify(response.meta ?? {}),
1401
+ },
1355
1402
  });
1356
1403
  await Turn.complete(this.#db, turnId, status);
1357
1404
  return {
@@ -1436,14 +1483,17 @@ class TurnRunner {
1436
1483
  // the producer-neutral admitted-turn executor settles every operation
1437
1484
  // and its exact source artifact.
1438
1485
  const packet = StoredPacket.admit(requestPacket, packetAssistant, response.assistantRaw);
1439
- await Turn.recordInference(this.#db, turnId, {
1440
- packet: StoredPacket.stringify(packet),
1441
- usageCurationBudget: this.#packets.curationBudgetFor(provider), // {§tokenomics-client-gauge}
1442
- finishReason: callMetadata.finishReason,
1443
- model: callMetadata.model,
1444
- // Opaque provider metadata plus engine-authored rail keys.
1445
- // {§meta-passthrough}, {§rail-truth-engine-verdict}
1446
- meta: JSON.stringify({ ...(response.meta ?? {}), ...(railKeys ?? {}) }),
1486
+ await this.#recordInference({
1487
+ workspaceId, workerId, loopId, turnId,
1488
+ evidence: {
1489
+ packet: StoredPacket.stringify(packet),
1490
+ usageCurationBudget: this.#packets.curationBudgetFor(provider), // {§tokenomics-client-gauge}
1491
+ finishReason: callMetadata.finishReason,
1492
+ model: callMetadata.model,
1493
+ // Opaque provider metadata plus engine-authored rail keys.
1494
+ // {§meta-passthrough}, {§rail-truth-engine-verdict}
1495
+ meta: JSON.stringify({ ...(response.meta ?? {}), ...(railKeys ?? {}) }),
1496
+ },
1447
1497
  });
1448
1498
  // {§operator-config-workspace-max-commands} — workspace maxCommands
1449
1499
  // narrows the operator ceiling before the admitted program reaches the
@@ -1736,90 +1786,33 @@ class TurnRunner {
1736
1786
  && ch.channel === this.#schemes.defaultChannelFor(ch.runtime, workerId)
1737
1787
  ? null
1738
1788
  : ch.channel;
1739
- const streamBase = renderAddress({
1740
- scheme: ch.runtime,
1741
- authority: ch.authority,
1742
- pathname: ch.coord,
1743
- });
1744
- const streamTarget = visibleFragment === null
1745
- ? streamBase
1746
- : `${streamBase}#${PathSyntax.escapeTarget(visibleFragment)}`;
1747
1789
  const targetParts = authorityParts(ch.authority);
1748
- const cursor = ch.published_end;
1749
- const closed = ch.state === "closed" || ch.state === "errored";
1750
- const terminal = closed
1751
- ? Results.assert(JSON.parse(ch.producer_result ?? "null"))
1752
- : null;
1753
- const terminalResult = async (fields, sequence) => {
1754
- if (terminal === null)
1755
- throw new Error(`closed subscription ${ch.subscription_id} has no terminal result`);
1756
- const result = Results.assert({
1757
- ...terminal,
1758
- ...(terminal.problem === undefined ? {} : { problem: { ...terminal.problem } }),
1759
- ...fields,
1760
- });
1761
- if (result.problem !== undefined) {
1762
- const seqs = await this.#db.engine_loop_turn_seqs.get({
1763
- loop_id: loopId,
1764
- turn_id: turnId,
1765
- });
1766
- if (seqs === undefined)
1767
- throw new Error(`stream delta has no log coordinate for loop=${loopId} turn=${turnId}`);
1768
- Results.attachInstance(result, `log:///${seqs.loop_seq}/${seqs.turn_seq}/${sequence}/READ`);
1769
- }
1770
- return result;
1771
- };
1772
- const publishEnd = streamPublicationEnd(ch.content, ch.mimetype, cursor, closed);
1773
- if (publishEnd <= cursor) {
1774
- // The cursor-terminal race: a channel written in one final
1775
- // burst gets fully shown FOLDED while still active; the close then has zero new
1776
- // content and the auto-OPEN terminal observation never fired — the model was never shown
1777
- // the conclusion of a stream whose result it already holds folded. The same
1778
- // observation is required when the channel produced no publishable content:
1779
- // completion is information independently of payload. A text stream that delivered
1780
- // content retains its terse revisit marker; an empty text stream and every structured
1781
- // channel emit a bodyless typed conclusion.
1782
- if (terminal !== null) {
1783
- const sequence = fromSequence + written;
1784
- const content = cursor > 0 && baseMimetype(ch.mimetype).startsWith("text/")
1785
- ? `[ stream closed (${terminal.status}) - full output already delivered above; READ ${streamTarget} to revisit ]`
1786
- : "";
1787
- const rx = JSON.stringify(await terminalResult({
1788
- content,
1789
- mimetype: ch.mimetype,
1790
- }, sequence));
1791
- await this.#db.engine_insert_stream_delta.run({
1792
- worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
1793
- subscription_publication_id: ch.publication_id,
1794
- scheme: ch.runtime, hostname: targetParts.hostname, port: targetParts.port,
1795
- pathname: ch.coord, fragment: visibleFragment,
1796
- rx,
1797
- weight: LogBody.weight({
1798
- op: "READ",
1799
- attrs: {},
1800
- tx: "",
1801
- rx,
1802
- mimetypeTx: "text/plain",
1803
- mimetypeRx: "application/json",
1804
- }, this.#weighContent),
1805
- status: terminal?.status ?? 200,
1806
- attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: true }),
1807
- folded: LogVisibility.serialize(LogVisibility.OPEN),
1808
- });
1809
- written++;
1810
- }
1790
+ // {§exec-stream} — nothing publishes while a stream is active: the Child Streams
1791
+ // section reports its size and growth ({§child-orientation}); the model READs any
1792
+ // range it wants. At close, ONE foisted READ that is exactly a markerless READ —
1793
+ // the first page, the extent, the terminal status and Problem — born OPEN. {§exec-stream-page}
1794
+ if (ch.state !== "closed" && ch.state !== "errored")
1811
1795
  continue;
1812
- }
1813
- // startLine continues the line count across turns: a multi-turn stream's deltas number
1814
- // into one sequence (lines N..M, then M+1..), not N independent "1:" restarts. {§exec-stream}
1815
- const startLine = (ch.content.slice(0, cursor).match(/\n/g)?.length ?? 0) + 1;
1796
+ const terminal = Results.assert(JSON.parse(ch.producer_result ?? "null"));
1816
1797
  const sequence = fromSequence + written;
1817
- const content = ch.content.slice(cursor, publishEnd);
1818
- const terminalDelivery = closed && publishEnd === ch.content.length;
1819
- const fields = { content, mimetype: ch.mimetype, startLine };
1820
- const result = terminalDelivery
1821
- ? await terminalResult(fields, sequence)
1822
- : { status: 200, ...fields };
1798
+ const page = await ReadResolve.resolve({ content: ch.content, mimetype: ch.mimetype, lineMarker: null });
1799
+ const result = Results.assert({
1800
+ ...terminal,
1801
+ ...(terminal.problem === undefined ? {} : { problem: { ...terminal.problem } }),
1802
+ content: page.content ?? "",
1803
+ mimetype: page.mimetype,
1804
+ ...(page.startLine === undefined || page.startLine === null ? {} : { startLine: page.startLine }),
1805
+ ...(page.range === undefined ? {} : { range: page.range }),
1806
+ });
1807
+ if (result.problem !== undefined) {
1808
+ const seqs = await this.#db.engine_loop_turn_seqs.get({
1809
+ loop_id: loopId,
1810
+ turn_id: turnId,
1811
+ });
1812
+ if (seqs === undefined)
1813
+ throw new Error(`stream delta has no log coordinate for loop=${loopId} turn=${turnId}`);
1814
+ Results.attachInstance(result, `log:///${seqs.loop_seq}/${seqs.turn_seq}/${sequence}/READ`);
1815
+ }
1823
1816
  const rx = JSON.stringify(result);
1824
1817
  await this.#db.engine_insert_stream_delta.run({
1825
1818
  worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
@@ -1835,9 +1828,9 @@ class TurnRunner {
1835
1828
  mimetypeTx: "text/plain",
1836
1829
  mimetypeRx: "application/json",
1837
1830
  }, this.#weighContent),
1838
- status: result.status,
1839
- attrs: JSON.stringify({ streamEnd: publishEnd, terminal: terminalDelivery }),
1840
- folded: LogVisibility.serialize(terminalDelivery ? LogVisibility.OPEN : LogVisibility.FOLDED), // {§exec-stream} — terminal observation auto-OPENs; ongoing folds
1831
+ status: terminal.status,
1832
+ attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: true }),
1833
+ folded: LogVisibility.serialize(LogVisibility.OPEN), // {§exec-stream} — the conclusion is born OPEN
1841
1834
  });
1842
1835
  written++;
1843
1836
  }