@plurnk/plurnk-service 1.4.0 → 1.5.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.
- package/.env.defaults +20 -23
- package/INSTALL.md +6 -5
- package/README.md +1 -0
- package/SPEC.md +250 -149
- package/dist/Paths.d.ts.map +1 -1
- package/dist/Paths.js +2 -3
- package/dist/Paths.js.map +1 -1
- package/dist/build-info.json +1 -1
- package/dist/content/index.d.ts +1 -0
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +1 -0
- package/dist/content/index.js.map +1 -1
- package/dist/content/line-marker.d.ts +7 -3
- package/dist/content/line-marker.d.ts.map +1 -1
- package/dist/content/line-marker.js +2 -1
- package/dist/content/line-marker.js.map +1 -1
- package/dist/content/matcher.d.ts +1 -1
- package/dist/content/matcher.d.ts.map +1 -1
- package/dist/content/matcher.js +2 -2
- package/dist/content/matcher.js.map +1 -1
- package/dist/content/read-projector.d.ts +14 -0
- package/dist/content/read-projector.d.ts.map +1 -0
- package/dist/content/read-projector.js +86 -0
- package/dist/content/read-projector.js.map +1 -0
- package/dist/content/read-resolve.d.ts +2 -7
- package/dist/content/read-resolve.d.ts.map +1 -1
- package/dist/content/read-resolve.js +32 -107
- package/dist/content/read-resolve.js.map +1 -1
- package/dist/core/BranchReceipt.d.ts +0 -1
- package/dist/core/BranchReceipt.d.ts.map +1 -1
- package/dist/core/BranchReceipt.js +0 -3
- package/dist/core/BranchReceipt.js.map +1 -1
- package/dist/core/BudgetOverflow.d.ts +0 -3
- package/dist/core/BudgetOverflow.d.ts.map +1 -1
- package/dist/core/BudgetOverflow.js +3 -15
- package/dist/core/BudgetOverflow.js.map +1 -1
- package/dist/core/ChannelWrite.d.ts +3 -0
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +2 -2
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +4 -3
- package/dist/core/CoreSchemeServices.d.ts +10 -2
- package/dist/core/CoreSchemeServices.d.ts.map +1 -1
- package/dist/core/CoreSchemeServices.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +3 -7
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +283 -217
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/Engine.d.ts +19 -12
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +261 -167
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +66 -63
- package/dist/core/LogBody.d.ts +2 -1
- package/dist/core/LogBody.d.ts.map +1 -1
- package/dist/core/LogBody.js +33 -1
- package/dist/core/LogBody.js.map +1 -1
- package/dist/core/LoopLifecycle.d.ts +1 -2
- package/dist/core/LoopLifecycle.d.ts.map +1 -1
- package/dist/core/LoopLifecycle.js +1 -4
- package/dist/core/LoopLifecycle.js.map +1 -1
- package/dist/core/LoopLifecycle.sql +1 -4
- package/dist/core/PacketBuilder.d.ts +3 -2
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +16 -16
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/StrikeRail.d.ts +5 -1
- package/dist/core/StrikeRail.d.ts.map +1 -1
- package/dist/core/StrikeRail.js +4 -3
- package/dist/core/StrikeRail.js.map +1 -1
- package/dist/core/TerminalResult.d.ts +16 -0
- package/dist/core/TerminalResult.d.ts.map +1 -0
- package/dist/core/TerminalResult.js +49 -0
- package/dist/core/TerminalResult.js.map +1 -0
- package/dist/core/caps/CapsResolve.d.ts +5 -1
- package/dist/core/caps/CapsResolve.d.ts.map +1 -1
- package/dist/core/caps/CapsResolve.js +8 -4
- package/dist/core/caps/CapsResolve.js.map +1 -1
- package/dist/core/caps/DbChannelCaps.d.ts +1 -1
- package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
- package/dist/core/caps/DbChannelCaps.js +6 -4
- package/dist/core/caps/DbChannelCaps.js.map +1 -1
- package/dist/core/caps/DbEntryCaps.d.ts +1 -1
- package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
- package/dist/core/caps/DbEntryCaps.js +14 -4
- package/dist/core/caps/DbEntryCaps.js.map +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
- package/dist/core/caps/DbNotifyCaps.js +6 -4
- package/dist/core/caps/DbNotifyCaps.js.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.d.ts +4 -6
- package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.js +27 -13
- package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
- package/dist/core/caps/DbTagCaps.d.ts +1 -1
- package/dist/core/caps/DbTagCaps.d.ts.map +1 -1
- package/dist/core/caps/DbTagCaps.js +6 -4
- package/dist/core/caps/DbTagCaps.js.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts +6 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +6 -6
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/fork.sql +2 -2
- package/dist/core/git-iso.d.ts +3 -2
- package/dist/core/git-iso.d.ts.map +1 -1
- package/dist/core/git-iso.js +27 -12
- package/dist/core/git-iso.js.map +1 -1
- package/dist/core/git-membership.d.ts +1 -0
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +86 -24
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.d.ts +8 -1
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +35 -16
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/namespace.d.ts +1 -0
- package/dist/core/namespace.d.ts.map +1 -1
- package/dist/core/namespace.js +20 -0
- package/dist/core/namespace.js.map +1 -1
- package/dist/core/optimistic-settlement.d.ts +2 -0
- package/dist/core/optimistic-settlement.d.ts.map +1 -0
- package/dist/core/optimistic-settlement.js +14 -0
- package/dist/core/optimistic-settlement.js.map +1 -0
- package/dist/core/packet-wire.d.ts +1 -1
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +81 -35
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/results.d.ts +4 -2
- package/dist/core/results.d.ts.map +1 -1
- package/dist/core/results.js +6 -0
- package/dist/core/results.js.map +1 -1
- package/dist/core/scheme-types.d.ts +0 -12
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/scheme-types.js.map +1 -1
- package/dist/core/worker-ops.sql +4 -4
- package/dist/core/workspace-settings.js +1 -1
- package/dist/core/workspace-settings.js.map +1 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +236 -74
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/digest.sql +9 -5
- package/dist/schemes/Exec.d.ts +4 -4
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +56 -33
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.d.ts +4 -5
- package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +12 -20
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/File.d.ts +1 -3
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +11 -19
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts +5 -5
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +74 -172
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +20 -13
- package/dist/schemes/Prompt.d.ts +2 -3
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +1 -4
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/Skill.d.ts +2 -3
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +0 -3
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts +5 -5
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +92 -81
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts +2 -1
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +14 -1
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +9 -3
- package/dist/schemes/_entry-find.d.ts +37 -11
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +141 -153
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts +9 -5
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +12 -73
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +1 -2
- package/dist/schemes/_entry-semantic.d.ts +5 -7
- package/dist/schemes/_entry-semantic.d.ts.map +1 -1
- package/dist/schemes/_entry-semantic.js +10 -31
- package/dist/schemes/_entry-semantic.js.map +1 -1
- package/dist/schemes/_entry-semantic.sql +2 -2
- package/dist/schemes/_search-index.d.ts.map +1 -1
- package/dist/schemes/_search-index.js +6 -4
- package/dist/schemes/_search-index.js.map +1 -1
- package/dist/server/BranchBatches.d.ts +1 -0
- package/dist/server/BranchBatches.d.ts.map +1 -1
- package/dist/server/BranchBatches.js +1 -0
- package/dist/server/BranchBatches.js.map +1 -1
- package/dist/server/Daemon.d.ts +5 -7
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +222 -54
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DaemonModule.d.ts +3 -3
- package/dist/server/DaemonModule.d.ts.map +1 -1
- package/dist/server/branch-batch.sql +3 -1
- package/dist/server/client-input.d.ts +2 -1
- package/dist/server/client-input.d.ts.map +1 -1
- package/dist/server/client-input.js +7 -1
- package/dist/server/client-input.js.map +1 -1
- package/dist/server/drain.sql +6 -5
- package/dist/server/envelope.d.ts +2 -2
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +0 -1
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +1 -2
- package/dist/server/lifecycle-recovery.sql +34 -1
- package/dist/server/logEntry.d.ts +1 -1
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +2 -1
- package/dist/server/logEntry.js.map +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +13 -1
- package/dist/service.js.map +1 -1
- package/migrations/001_schema.sql +238 -125
- package/package.json +48 -46
- package/dist/matrix/live-specimen.d.ts +0 -9
- package/dist/matrix/live-specimen.d.ts.map +0 -1
- package/dist/matrix/live-specimen.js +0 -37
- package/dist/matrix/live-specimen.js.map +0 -1
package/dist/core/Engine.js
CHANGED
|
@@ -7,7 +7,6 @@ import { Mimetypes, emptyRegistry } from "@plurnk/plurnk-mimetypes";
|
|
|
7
7
|
import Meta from "@plurnk/plurnk-meta";
|
|
8
8
|
import EntryCrud from "../schemes/_entry-crud.js";
|
|
9
9
|
import SearchIndex from "../schemes/_search-index.js";
|
|
10
|
-
import { markTerminal } from "../schemes/Worker.js";
|
|
11
10
|
import GitMembership from "./git-membership.js";
|
|
12
11
|
import GitState from "./git-state.js";
|
|
13
12
|
import WorkspaceSettings from "./workspace-settings.js";
|
|
@@ -26,6 +25,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
26
25
|
import PacketWire from "./packet-wire.js";
|
|
27
26
|
import Results, { OperationFailureError } from "./results.js";
|
|
28
27
|
import BranchReceipt from "./BranchReceipt.js";
|
|
28
|
+
import TerminalResult from "./TerminalResult.js";
|
|
29
29
|
import BudgetOverflow from "./BudgetOverflow.js";
|
|
30
30
|
import WorkerControlAddress from "./WorkerControlAddress.js";
|
|
31
31
|
import JournalTurn from "./JournalTurn.js";
|
|
@@ -41,7 +41,20 @@ import Dispatcher from "./Dispatcher.js";
|
|
|
41
41
|
import { observed, observedSync } from "../observe/spans.js";
|
|
42
42
|
import { OPS_DISPATCHED, PROVIDER_CALLS, recordCounter } from "../observe/metrics.js";
|
|
43
43
|
import { scheduleTurnOps } from "./turn-scheduler.js";
|
|
44
|
+
import { readOptimisticSettlementMs } from "./optimistic-settlement.js";
|
|
44
45
|
const DEFAULT_MAX_STRIKES = 3;
|
|
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
|
+
};
|
|
45
58
|
const ENGINE_PROBLEMS = Object.freeze({
|
|
46
59
|
max_commands_exceeded: {
|
|
47
60
|
status: 429,
|
|
@@ -102,7 +115,7 @@ const readMaxCommands = () => {
|
|
|
102
115
|
return n;
|
|
103
116
|
};
|
|
104
117
|
// PLURNK_SERVICE_FILES_ITEMS — the turn-0 catalog preview. null = off;
|
|
105
|
-
// -1 = the
|
|
118
|
+
// -1 = the ordinary markerless page; positive N explicitly caps file rows. 0 / unset = off.
|
|
106
119
|
const normalizeFilesItems = (n) => (!Number.isFinite(n) || n === 0 ? null : n < 0 ? -1 : n);
|
|
107
120
|
const readFilesItems = () => {
|
|
108
121
|
const raw = process.env.PLURNK_SERVICE_FILES_ITEMS;
|
|
@@ -117,6 +130,7 @@ import ProviderInstantiate from "./ProviderInstantiate.js";
|
|
|
117
130
|
// continue after dispatch ({§send}). Every admitted emission itself ends in an
|
|
118
131
|
// explicit disposition SEND ({§emission-admission}).
|
|
119
132
|
const TURN_STATUS_IMPLICIT_CONTINUE = 102;
|
|
133
|
+
const INVALID_EMISSION_RECOVERY_MESSAGE = "Your previous response contained an unrecoverable syntax error. No operations were performed. Try again.";
|
|
120
134
|
const DEFAULT_MIN_CYCLES = 3;
|
|
121
135
|
const DEFAULT_MAX_CYCLE_PERIOD = 4;
|
|
122
136
|
const readPositiveInt = (envVar, fallback) => {
|
|
@@ -208,7 +222,13 @@ export default class Engine {
|
|
|
208
222
|
if (!invalidate && this.#workspaceWarmStatus.get(workspaceId)?.phase === "complete") {
|
|
209
223
|
return Promise.resolve();
|
|
210
224
|
}
|
|
211
|
-
const state = {
|
|
225
|
+
const state = {
|
|
226
|
+
dirty: false,
|
|
227
|
+
materialize,
|
|
228
|
+
ctx,
|
|
229
|
+
abort: new AbortController(),
|
|
230
|
+
promise: Promise.resolve(),
|
|
231
|
+
};
|
|
212
232
|
// Register before publishing the first synchronous Notice. A
|
|
213
233
|
// listener may request another warm from that callback; it must join
|
|
214
234
|
// this state rather than opening a second pump in the re-entrant gap.
|
|
@@ -231,10 +251,14 @@ export default class Engine {
|
|
|
231
251
|
completed: 0, total: 1, percent: 0, level: "info",
|
|
232
252
|
});
|
|
233
253
|
try {
|
|
254
|
+
const signal = current.signal === undefined
|
|
255
|
+
? state.abort.signal
|
|
256
|
+
: AbortSignal.any([current.signal, state.abort.signal]);
|
|
257
|
+
const cancellable = { ...current, signal };
|
|
234
258
|
if (shouldMaterialize)
|
|
235
|
-
await GitMembership.indexGitMembership(
|
|
259
|
+
await GitMembership.indexGitMembership(cancellable);
|
|
236
260
|
await SearchIndex.maintain({
|
|
237
|
-
...
|
|
261
|
+
...cancellable,
|
|
238
262
|
pushNotice: (notice) => {
|
|
239
263
|
if (notice.kind === "embed_progress"
|
|
240
264
|
&& typeof notice.completed === "number"
|
|
@@ -277,9 +301,26 @@ export default class Engine {
|
|
|
277
301
|
workspaceDerivationStatus(workspaceId) {
|
|
278
302
|
return this.#workspaceWarmStatus.get(workspaceId) ?? null;
|
|
279
303
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
304
|
+
cancelDerivations(reason = new DOMException("derivations cancelled", "AbortError")) {
|
|
305
|
+
for (const state of this.#workspaceWarms.values()) {
|
|
306
|
+
if (!state.abort.signal.aborted)
|
|
307
|
+
state.abort.abort(reason);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Awaited by Daemon.stop before the db closes. Shutdown supplies the exact
|
|
311
|
+
// cancellation reason it owns; every unrelated failure remains visible.
|
|
312
|
+
async drainDerivations(ignoredReason) {
|
|
313
|
+
const results = await Promise.allSettled([...this.#workspaceWarms.values()].map((state) => state.promise));
|
|
314
|
+
const errors = results
|
|
315
|
+
.filter((result) => result.status === "rejected")
|
|
316
|
+
.flatMap((result) => result.reason instanceof AggregateError
|
|
317
|
+
? [...result.reason.errors]
|
|
318
|
+
: [result.reason])
|
|
319
|
+
.filter((error) => error !== ignoredReason);
|
|
320
|
+
if (errors.length === 1)
|
|
321
|
+
throw errors[0];
|
|
322
|
+
if (errors.length > 1)
|
|
323
|
+
throw new AggregateError(errors, "derivation drain failed");
|
|
283
324
|
}
|
|
284
325
|
async drainWorkspaceDerivations(workspaceId) {
|
|
285
326
|
await this.#workspaceWarms.get(workspaceId)?.promise;
|
|
@@ -449,14 +490,19 @@ export default class Engine {
|
|
|
449
490
|
// occupancy gauge. {§tokenomics-client-gauge}, {§notifications-loop-terminated}
|
|
450
491
|
async loopUsage(loopId) {
|
|
451
492
|
const row = await this.#db.engine_loop_usage.get({ loop_id: loopId });
|
|
493
|
+
if (row === undefined)
|
|
494
|
+
throw new Error(`loopUsage: loop ${loopId} does not exist`);
|
|
452
495
|
const parsedCosts = JSON.parse(row?.costs ?? "[]");
|
|
453
496
|
if (!Array.isArray(parsedCosts))
|
|
454
497
|
throw new TypeError(`loop ${loopId} monetary evidence is not an array`);
|
|
498
|
+
const costs = parsedCosts.map((cost) => validateProviderCost(cost));
|
|
455
499
|
return {
|
|
456
500
|
promptTokens: row?.prompt ?? 0,
|
|
457
501
|
completionTokens: row?.completion ?? 0,
|
|
502
|
+
reasoningTokens: row?.reasoning ?? 0,
|
|
503
|
+
cachedTokens: row?.cached ?? 0,
|
|
458
504
|
costUsd: row?.cost_usd ?? null,
|
|
459
|
-
costs
|
|
505
|
+
costs,
|
|
460
506
|
// Latest provider attempt on the latest turn, not the billed total.
|
|
461
507
|
contextTokens: row?.context ?? 0,
|
|
462
508
|
// Latest effective packet allowance; null when uncapped or unknown.
|
|
@@ -490,6 +536,7 @@ export default class Engine {
|
|
|
490
536
|
// Its ceiling therefore counts every prior turn, not merely this process-local
|
|
491
537
|
// execution segment.
|
|
492
538
|
const turnIds = await this.#lifecycle.turnIds(loopId);
|
|
539
|
+
let invalidEmissionRecoveryEntryId = null;
|
|
493
540
|
// Per-loop AbortController for scheme-side cancellation propagation.
|
|
494
541
|
// Chained from the caller's `signal` so an external abort cascades.
|
|
495
542
|
const loopAbort = new AbortController();
|
|
@@ -547,7 +594,7 @@ export default class Engine {
|
|
|
547
594
|
// check, {§worker-lifecycle-wake-requeue-not-terminal}). The wake's intent is KEEP
|
|
548
595
|
// RUNNING: re-claim atomically and continue — the injected prompt is already
|
|
549
596
|
// this loop's next turn. Returning it as "external" broadcast a QUEUED loop
|
|
550
|
-
// as a terminal result with status 100 — the delegation-flags
|
|
597
|
+
// as a terminal result with status 100 — the delegation-flags race.
|
|
551
598
|
await this.#db.engine_reclaim_queued_loop.run({ loop_id: loopId });
|
|
552
599
|
continue; // claimed (or a racer flipped it first — the re-read decides)
|
|
553
600
|
}
|
|
@@ -584,10 +631,6 @@ export default class Engine {
|
|
|
584
631
|
cleanup("forceful", "max_turns");
|
|
585
632
|
return { turnIds, result, hitMaxTurns: true, reason: "max_turns" };
|
|
586
633
|
}
|
|
587
|
-
// PLURNK_SERVICE_EXEC_WAIT_MS — a post-EXEC breath: if a spawn from the prior turn
|
|
588
|
-
// is still in flight, give it a tunable beat to land in THIS turn's packet
|
|
589
|
-
// before we assemble it. A fixed grace beat, never a wait-for-completion;
|
|
590
|
-
// 0/unset = off. Abortable with the loop signal.
|
|
591
634
|
const execHandler = this.#schemes.get("exec");
|
|
592
635
|
// {§exec-hold-until-concluded} — hold matching runtime/effect
|
|
593
636
|
// streams until conclusion or the fail-open cap, then resume the
|
|
@@ -600,11 +643,6 @@ export default class Engine {
|
|
|
600
643
|
await delay(150, undefined, { signal });
|
|
601
644
|
}
|
|
602
645
|
}
|
|
603
|
-
const execWaitMs = Number(process.env.PLURNK_SERVICE_EXEC_WAIT_MS ?? "0");
|
|
604
|
-
if (execWaitMs > 0) {
|
|
605
|
-
if (execHandler?.hasActiveSpawns?.(workerId) === true)
|
|
606
|
-
await delay(execWaitMs, undefined, { signal });
|
|
607
|
-
}
|
|
608
646
|
let turn;
|
|
609
647
|
const releaseWorkspace = await this.#acquireWorkspaceTurn(workspaceId, workerId);
|
|
610
648
|
try {
|
|
@@ -613,6 +651,7 @@ export default class Engine {
|
|
|
613
651
|
const t = await this.runTurn({
|
|
614
652
|
provider, messages, requirements, workspaceId, workerId, loopId, origin, signal, onDispatch,
|
|
615
653
|
turnNumber: turnIds.length + 1, maxTurns,
|
|
654
|
+
invalidEmissionRecoveryEntryId,
|
|
616
655
|
});
|
|
617
656
|
span.setAttribute("turn.id", t.turnId);
|
|
618
657
|
return t;
|
|
@@ -635,10 +674,18 @@ export default class Engine {
|
|
|
635
674
|
releaseWorkspace();
|
|
636
675
|
}
|
|
637
676
|
turnIds.push(turn.turnId);
|
|
638
|
-
// Invalid provider emissions are retried beneath this turn and never
|
|
639
|
-
// reach the strike rail.
|
|
640
|
-
//
|
|
677
|
+
// {§invalid-emission-attempts} Invalid provider emissions are retried beneath this turn and never
|
|
678
|
+
// reach the strike rail. The first consecutive exhaustion has already
|
|
679
|
+
// exposed its bounded lifeline through ordinary next-turn state. A
|
|
680
|
+
// second exhaustion is terminal; an admitted turn clears the sequence.
|
|
641
681
|
if (turn.emissionExhausted) {
|
|
682
|
+
if (invalidEmissionRecoveryEntryId === null) {
|
|
683
|
+
if (turn.rejectedModelEntryId === undefined) {
|
|
684
|
+
throw new Error("an admitted invalid-emission recovery requires its rejected model-entry identity");
|
|
685
|
+
}
|
|
686
|
+
invalidEmissionRecoveryEntryId = turn.rejectedModelEntryId;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
642
689
|
const failure = Results.failure("engine:generation", "invalid-emission-exhausted", 500, `No valid PLAN...SEND turn was received after ${turn.emissionAttempts} emission attempts.`, {}, {
|
|
643
690
|
attempts: turn.emissionAttempts,
|
|
644
691
|
stage: "emission-validation",
|
|
@@ -650,6 +697,7 @@ export default class Engine {
|
|
|
650
697
|
cleanup("forceful", "invalid_emission");
|
|
651
698
|
return { turnIds, result, hitMaxTurns: false, reason: "invalid_emission" };
|
|
652
699
|
}
|
|
700
|
+
invalidEmissionRecoveryEntryId = null;
|
|
653
701
|
// SPEC {§grinder}: budget hard-stop — packet won't fit even collapsed → abandon.
|
|
654
702
|
if (turn.budgetHardStop) {
|
|
655
703
|
if (turn.budget === undefined) {
|
|
@@ -663,11 +711,11 @@ export default class Engine {
|
|
|
663
711
|
return { turnIds, result, hitMaxTurns: false, reason: "budget_overflow" };
|
|
664
712
|
}
|
|
665
713
|
// {§engine-rails} — per-turn strike accounting (cycle detection, the
|
|
666
|
-
// grinder/steer coupling, hard
|
|
714
|
+
// grinder/steer coupling, hard operation outcomes). StrikeRail owns the
|
|
667
715
|
// bookkeeping; runLoop owns abandonment.
|
|
668
716
|
const verdict = this.#strikes.assess(loopId, {
|
|
669
717
|
fingerprint: turn.fingerprint,
|
|
670
|
-
|
|
718
|
+
outcomes: turn.outcomes,
|
|
671
719
|
budgetStruck: turn.budgetStruck,
|
|
672
720
|
steerStruck: turn.steerStruck,
|
|
673
721
|
minCycles, maxCyclePeriod, maxStrikes,
|
|
@@ -691,7 +739,11 @@ export default class Engine {
|
|
|
691
739
|
}
|
|
692
740
|
}
|
|
693
741
|
}
|
|
694
|
-
async runTurn({ provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, }) {
|
|
742
|
+
async runTurn({ provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, invalidEmissionRecoveryEntryId, }) {
|
|
743
|
+
const allowInvalidEmissionRecovery = invalidEmissionRecoveryEntryId === null;
|
|
744
|
+
const transientOpenLogEntryId = typeof invalidEmissionRecoveryEntryId === "number"
|
|
745
|
+
? invalidEmissionRecoveryEntryId
|
|
746
|
+
: null;
|
|
695
747
|
// === Turn-as-container model ===
|
|
696
748
|
//
|
|
697
749
|
// Turn rows are created at runTurn OPEN (status=102, placeholder
|
|
@@ -859,7 +911,10 @@ export default class Engine {
|
|
|
859
911
|
// The warm materialized membership before deriving. This second pass is the
|
|
860
912
|
// ordinary cheap change detector and captures any drift that landed meanwhile.
|
|
861
913
|
const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
|
|
862
|
-
|
|
914
|
+
// {§packet-git-status} — one post-reconciliation snapshot supplies both
|
|
915
|
+
// the compact packet summary and each causal file event's exact XY state.
|
|
916
|
+
const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopAborts.get(loopId)?.signal);
|
|
917
|
+
await this.#logFsFictions(workspaceId, fsDivergences, gitStatus);
|
|
863
918
|
// The refresh above may have changed bodies (including model/client edits
|
|
864
919
|
// since the startup warm). Re-derive to completion before packet/model
|
|
865
920
|
// construction. Membership is already current, so this pass does not
|
|
@@ -894,10 +949,10 @@ export default class Engine {
|
|
|
894
949
|
for (const { scheme, shallow_items: shallowItems } of foistSchemes) {
|
|
895
950
|
const isFile = scheme === "file";
|
|
896
951
|
const pattern = this.#schemes.manifestFor(scheme)?.folderScopes === true ? "*" : "**";
|
|
897
|
-
// Only the file map takes PLURNK_SERVICE_FILES_ITEMS as
|
|
898
|
-
// other
|
|
899
|
-
// survey has no range: `<1,0>` would turn useful orientation into an error.
|
|
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.
|
|
900
954
|
const cap = isFile && filesItems > 0 && shallowItems > 0 ? Math.min(filesItems, shallowItems) : null;
|
|
955
|
+
const catalogMarker = cap === null ? null : { marks: [1, cap] };
|
|
901
956
|
// The file survey foists as the BARE relative glob — the path shape plurnk.md
|
|
902
957
|
// teaches (`*`, `src/**`, `**/notes.md`; bare = project-relative) — so the turn-0
|
|
903
958
|
// exemplar and the log rows the model reads never train a leading-slash or
|
|
@@ -913,7 +968,7 @@ export default class Engine {
|
|
|
913
968
|
query: null, fragment: null,
|
|
914
969
|
},
|
|
915
970
|
body: null,
|
|
916
|
-
lineMarker:
|
|
971
|
+
lineMarker: catalogMarker,
|
|
917
972
|
position: UNKNOWN_POSITION,
|
|
918
973
|
};
|
|
919
974
|
await this.dispatch({
|
|
@@ -921,9 +976,11 @@ export default class Engine {
|
|
|
921
976
|
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
922
977
|
});
|
|
923
978
|
nextActionIndex++;
|
|
924
|
-
// {§model-entry} — the same FIND, rendered back to DSL for the turn-0 echo
|
|
925
|
-
// own survey, mirrored OPEN).
|
|
926
|
-
|
|
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`);
|
|
927
984
|
}
|
|
928
985
|
// The kernel's self-documenting surface — FIND(worker://plurnk/docs/**), uncapped,
|
|
929
986
|
// always ({§schemes-directory}, published under {§entry-owner}).
|
|
@@ -931,13 +988,13 @@ export default class Engine {
|
|
|
931
988
|
const kernelDocsFind = {
|
|
932
989
|
op: "FIND", suffix: "", signal: null,
|
|
933
990
|
target: { kind: "url", raw: "worker://plurnk/docs/**", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/docs/**", query: null, fragment: null },
|
|
934
|
-
body: null, lineMarker:
|
|
991
|
+
body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
935
992
|
};
|
|
936
993
|
await this.dispatch({ statement: kernelDocsFind, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
|
|
937
994
|
nextActionIndex++;
|
|
938
|
-
turnZeroMoves.push("<<FIND(worker://plurnk/docs/**)
|
|
939
|
-
// {§worker-scheme} — the building worker's own scratch gets the same
|
|
940
|
-
// one-level
|
|
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
|
|
941
998
|
// space is useful orientation, not grounds to hide the surface.
|
|
942
999
|
const ownFind = {
|
|
943
1000
|
op: "FIND", suffix: "", signal: null,
|
|
@@ -953,7 +1010,7 @@ export default class Engine {
|
|
|
953
1010
|
// the true survey, never a frozen print — and OPEN: the worked example the model orients on,
|
|
954
1011
|
// so the grammar can stay thin. Subsequent turns mirror the model's real output, folded.
|
|
955
1012
|
if (workerFirstLoop) {
|
|
956
|
-
const emission = ["<<PLAN:
|
|
1013
|
+
const emission = ["<<PLAN:Survey context, then address the prompt.:PLAN", ...turnZeroMoves, "<<SEND[102]:Next, address the prompt using the survey.:SEND"].join("\n");
|
|
957
1014
|
await this.#dispatcher.writeModelEntry({ verbatim: emission, workerId, loopId, turnId, sequence: 1, folded: false, origin: "plurnk" });
|
|
958
1015
|
}
|
|
959
1016
|
}
|
|
@@ -986,10 +1043,8 @@ export default class Engine {
|
|
|
986
1043
|
await this.#reapTurnScopedStreams(workerId);
|
|
987
1044
|
nextActionIndex += await this.#materializeEnvironmentDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
|
|
988
1045
|
nextActionIndex += await this.#materializeStreamDeltas({ workerId, loopId, turnId, fromSequence: nextActionIndex });
|
|
989
|
-
//
|
|
990
|
-
//
|
|
991
|
-
// rebuild too so it isn't re-shelled on overflow.
|
|
992
|
-
const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopAborts.get(loopId)?.signal);
|
|
1046
|
+
// The post-reconciliation Git snapshot above is threaded into the packet
|
|
1047
|
+
// and every budget rebuild; overflow never shells again.
|
|
993
1048
|
// Notices are non-terminal observations, never operation-failure truth.
|
|
994
1049
|
// Drain once and thread the same set through every grinder rebuild.
|
|
995
1050
|
const notices = this.#notices.drain(loopId)
|
|
@@ -999,7 +1054,7 @@ export default class Engine {
|
|
|
999
1054
|
// written (if turn 1) is part of that query result.
|
|
1000
1055
|
let requestPacket = await this.#packets.buildRequestPacket({
|
|
1001
1056
|
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
1002
|
-
currentTurnSeq: seq, provider, gitStatus, notices,
|
|
1057
|
+
currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
|
|
1003
1058
|
});
|
|
1004
1059
|
// SPEC {§grinder} — budget grinder, pre-LLM: reclaim window on actual overflow.
|
|
1005
1060
|
const enforced = await this.#packets.enforceBudget({
|
|
@@ -1011,18 +1066,18 @@ export default class Engine {
|
|
|
1011
1066
|
// set; neither path can duplicate or swallow a product failure.
|
|
1012
1067
|
rebuild: () => this.#packets.buildRequestPacket({
|
|
1013
1068
|
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
1014
|
-
currentTurnSeq: seq, provider, gitStatus, notices,
|
|
1069
|
+
currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
|
|
1015
1070
|
}),
|
|
1016
1071
|
});
|
|
1017
1072
|
if (enforced.recorded)
|
|
1018
1073
|
nextActionIndex += 1; // a fold-to-fit Problem consumed the reserved sequence
|
|
1019
1074
|
requestPacket = enforced.packet;
|
|
1020
|
-
let operationConstraint;
|
|
1021
1075
|
if (!enforced.fit) {
|
|
1022
1076
|
// {§grinder-hard-413-recovery}/{§grinder-hard-413-abort} — admit
|
|
1023
|
-
// one physically sendable
|
|
1077
|
+
// one physically sendable informed recovery turn; a physical
|
|
1024
1078
|
// overflow or consecutive policy overflow terminates immediately.
|
|
1025
1079
|
let physicalAdmission = await this.#packets.physicalAdmission(requestPacket, provider, this.#loopAborts.get(loopId)?.signal);
|
|
1080
|
+
let recoveryAdmitted = false;
|
|
1026
1081
|
if (physicalAdmission.admitted && !this.#hardOverflowRecovery.has(loopId)) {
|
|
1027
1082
|
const ceiling = this.#packets.ceilingFor(provider);
|
|
1028
1083
|
if (ceiling === null) {
|
|
@@ -1041,20 +1096,16 @@ export default class Engine {
|
|
|
1041
1096
|
// errors sections (the same re-derive contract the soft grind uses).
|
|
1042
1097
|
requestPacket = await this.#packets.buildRequestPacket({
|
|
1043
1098
|
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
1044
|
-
currentTurnSeq: seq, provider, gitStatus, notices,
|
|
1099
|
+
currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
|
|
1045
1100
|
});
|
|
1046
1101
|
physicalAdmission = await this.#packets.physicalAdmission(requestPacket, provider, this.#loopAborts.get(loopId)?.signal);
|
|
1047
1102
|
if (physicalAdmission.admitted) {
|
|
1048
1103
|
this.#hardOverflowRecovery.add(loopId);
|
|
1049
|
-
|
|
1050
|
-
code: "budget-recovery",
|
|
1051
|
-
detail: "budget recovery is active",
|
|
1052
|
-
allowedOperations: BudgetOverflow.recoveryOperations,
|
|
1053
|
-
};
|
|
1104
|
+
recoveryAdmitted = true;
|
|
1054
1105
|
}
|
|
1055
1106
|
}
|
|
1056
|
-
if (
|
|
1057
|
-
// Hard 413: physically unsendable, or still over after the
|
|
1107
|
+
if (!recoveryAdmitted) {
|
|
1108
|
+
// Hard 413: physically unsendable, or still over after the informed recovery turn.
|
|
1058
1109
|
const ceiling = this.#packets.ceilingFor(provider);
|
|
1059
1110
|
if (ceiling === null) {
|
|
1060
1111
|
throw new Error("an unbounded prompt budget cannot hard-stop");
|
|
@@ -1080,14 +1131,12 @@ export default class Engine {
|
|
|
1080
1131
|
});
|
|
1081
1132
|
requestPacket = await this.#packets.buildRequestPacket({
|
|
1082
1133
|
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
1083
|
-
currentTurnSeq: seq, provider, gitStatus, notices,
|
|
1134
|
+
currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
|
|
1084
1135
|
});
|
|
1085
1136
|
}
|
|
1086
1137
|
// Skip the LLM, close the turn, and let runLoop abandon.
|
|
1087
1138
|
await this.#db.engine_close_turn.run({
|
|
1088
1139
|
id: turnId, status: 413, packet: StoredPacket.stringify(requestPacket),
|
|
1089
|
-
usage_prompt: 0, usage_completion: 0, usage_reasoning: 0, usage_cached: 0,
|
|
1090
|
-
usage_cost: "[]", usage_cost_usd: 0,
|
|
1091
1140
|
// The attempted turn retains its effective allowance even
|
|
1092
1141
|
// when no provider exchange completed. {§tokenomics-client-gauge}
|
|
1093
1142
|
usage_prompt_budget: this.#packets.promptBudgetFor(provider),
|
|
@@ -1096,7 +1145,7 @@ export default class Engine {
|
|
|
1096
1145
|
return {
|
|
1097
1146
|
turnId,
|
|
1098
1147
|
status: 413,
|
|
1099
|
-
|
|
1148
|
+
outcomes: [],
|
|
1100
1149
|
fingerprint: "",
|
|
1101
1150
|
budgetStruck: enforced.struck,
|
|
1102
1151
|
budgetHardStop: true,
|
|
@@ -1121,43 +1170,42 @@ export default class Engine {
|
|
|
1121
1170
|
let railGrammar;
|
|
1122
1171
|
let railEvidence;
|
|
1123
1172
|
let emissionAttempts = 0;
|
|
1124
|
-
const usage = { prompt: 0, completion: 0, reasoning: 0, cached: 0 };
|
|
1125
|
-
let usageCostUsd = 0;
|
|
1126
|
-
const providerCosts = [];
|
|
1127
1173
|
let providerCallInFlight = false;
|
|
1128
1174
|
let providerAttemptSequence = 0;
|
|
1175
|
+
let providerAttemptId = null;
|
|
1129
1176
|
let providerAttemptAttributions = [];
|
|
1130
1177
|
const providerSignal = this.#loopAborts.get(loopId)?.signal ?? signal;
|
|
1131
1178
|
// {§client-metadata}
|
|
1132
1179
|
const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
1133
|
-
const
|
|
1134
|
-
const attemptUsage =
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
await this.#db.engine_record_turn_attempt.run({
|
|
1138
|
-
turn_id: turnId,
|
|
1139
|
-
sequence,
|
|
1140
|
-
accepted: accepted ? 1 : 0,
|
|
1180
|
+
const observeProviderAttempt = async (id, attemptResponse) => {
|
|
1181
|
+
const attemptUsage = attemptResponse.assistant.usage;
|
|
1182
|
+
await this.#db.engine_observe_turn_attempt_response.run({
|
|
1183
|
+
id,
|
|
1141
1184
|
response: JSON.stringify(attemptResponse),
|
|
1142
|
-
parse_errors: JSON.stringify(attemptSplit.parseErrors),
|
|
1143
|
-
attributions: JSON.stringify(attributions),
|
|
1144
1185
|
usage_prompt: attemptUsage.prompt,
|
|
1145
1186
|
usage_completion: attemptUsage.completion,
|
|
1146
1187
|
usage_reasoning: attemptUsage.reasoning,
|
|
1147
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({
|
|
1202
|
+
id,
|
|
1203
|
+
accepted: accepted ? 1 : 0,
|
|
1204
|
+
parse_errors: JSON.stringify(attemptSplit.parseErrors),
|
|
1205
|
+
failure: failure === null ? null : JSON.stringify(failure),
|
|
1148
1206
|
usage_cost: JSON.stringify(attemptCost),
|
|
1149
1207
|
usage_cost_usd: attemptCostUsd,
|
|
1150
|
-
finish_reason: attemptSplit.callMetadata.finishReason,
|
|
1151
|
-
model: attemptSplit.callMetadata.model,
|
|
1152
1208
|
});
|
|
1153
|
-
usage.prompt += attemptUsage.prompt;
|
|
1154
|
-
usage.completion += attemptUsage.completion;
|
|
1155
|
-
usage.reasoning += attemptUsage.reasoning;
|
|
1156
|
-
usage.cached += attemptUsage.cached;
|
|
1157
|
-
providerCosts.push(attemptCost);
|
|
1158
|
-
usageCostUsd = usageCostUsd === null || attemptCostUsd === null
|
|
1159
|
-
? null
|
|
1160
|
-
: usageCostUsd + attemptCostUsd;
|
|
1161
1209
|
emissionAttempts = sequence;
|
|
1162
1210
|
};
|
|
1163
1211
|
try {
|
|
@@ -1186,6 +1234,16 @@ export default class Engine {
|
|
|
1186
1234
|
});
|
|
1187
1235
|
providerAttemptAttributions = await this.#attemptAttributions(provider, attributionContext);
|
|
1188
1236
|
requestPacket = { ...requestPacket, attributions: providerAttemptAttributions };
|
|
1237
|
+
const attemptRow = await this.#db.engine_open_turn_attempt.get({
|
|
1238
|
+
turn_id: turnId,
|
|
1239
|
+
sequence: attempt,
|
|
1240
|
+
attributions: JSON.stringify(providerAttemptAttributions),
|
|
1241
|
+
model: provider.model,
|
|
1242
|
+
});
|
|
1243
|
+
if (attemptRow === undefined) {
|
|
1244
|
+
throw new Error(`Engine.runTurn: provider attempt ${attempt} did not open`);
|
|
1245
|
+
}
|
|
1246
|
+
providerAttemptId = attemptRow.id;
|
|
1189
1247
|
providerCallInFlight = true;
|
|
1190
1248
|
const completedResponse = await observed(// {§observability-boundary}
|
|
1191
1249
|
"provider.generate", { model: provider.model, attempt }, async (span) => {
|
|
@@ -1205,6 +1263,7 @@ export default class Engine {
|
|
|
1205
1263
|
loop: loopSeq,
|
|
1206
1264
|
turn: seq,
|
|
1207
1265
|
}); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
|
|
1266
|
+
providerCallInFlight = false;
|
|
1208
1267
|
recordCounter(PROVIDER_CALLS, {
|
|
1209
1268
|
model: provider.model,
|
|
1210
1269
|
attempt,
|
|
@@ -1214,12 +1273,12 @@ export default class Engine {
|
|
|
1214
1273
|
return generated;
|
|
1215
1274
|
});
|
|
1216
1275
|
response = completedResponse;
|
|
1217
|
-
|
|
1276
|
+
await observeProviderAttempt(attemptRow.id, completedResponse);
|
|
1218
1277
|
railEvidence = railGrammar === undefined
|
|
1219
1278
|
? undefined
|
|
1220
1279
|
: Engine.#requireGrammarEvidence(completedResponse);
|
|
1221
1280
|
splitResponse = this.#splitResponse(completedResponse);
|
|
1222
|
-
await
|
|
1281
|
+
await classifyProviderAttempt(attemptRow.id, completedResponse, splitResponse, attempt, splitResponse.emissionValid);
|
|
1223
1282
|
if (splitResponse.emissionValid)
|
|
1224
1283
|
break;
|
|
1225
1284
|
}
|
|
@@ -1231,20 +1290,43 @@ export default class Engine {
|
|
|
1231
1290
|
// and engine-contract failures retain their original source.
|
|
1232
1291
|
if (!providerCallInFlight)
|
|
1233
1292
|
throw err;
|
|
1293
|
+
providerCallInFlight = false;
|
|
1294
|
+
if (providerAttemptId === null) {
|
|
1295
|
+
throw new Error("provider call failed without a durable attempt identity", { cause: err });
|
|
1296
|
+
}
|
|
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
|
+
})();
|
|
1234
1310
|
// {§provider-interrupted-attempt} — a provider-declared interruption
|
|
1235
1311
|
// carries response evidence without becoming a completed exchange.
|
|
1236
1312
|
// Persist it as an unaccepted attempt before settling the failure.
|
|
1237
1313
|
if (err instanceof ProviderError && err.attempt !== undefined) {
|
|
1238
1314
|
response = err.attempt;
|
|
1315
|
+
await observeProviderAttempt(providerAttemptId, response);
|
|
1239
1316
|
splitResponse = this.#splitResponse(response);
|
|
1240
|
-
await
|
|
1317
|
+
await classifyProviderAttempt(providerAttemptId, response, splitResponse, providerAttemptSequence, false, failure);
|
|
1241
1318
|
}
|
|
1242
1319
|
else {
|
|
1243
|
-
|
|
1320
|
+
const unknownCost = {
|
|
1244
1321
|
kind: "unknown",
|
|
1245
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),
|
|
1246
1328
|
});
|
|
1247
|
-
|
|
1329
|
+
emissionAttempts = providerAttemptSequence;
|
|
1248
1330
|
}
|
|
1249
1331
|
// {§turn-never-blank} — a ProviderError means no completed exchange exists.
|
|
1250
1332
|
// Persist its exact RFC 9457 result before propagating it. Grammar evidence
|
|
@@ -1258,12 +1340,6 @@ export default class Engine {
|
|
|
1258
1340
|
id: turnId,
|
|
1259
1341
|
status: providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499,
|
|
1260
1342
|
packet: StoredPacket.stringify(requestPacket),
|
|
1261
|
-
usage_prompt: usage.prompt,
|
|
1262
|
-
usage_completion: usage.completion,
|
|
1263
|
-
usage_reasoning: usage.reasoning,
|
|
1264
|
-
usage_cached: usage.cached,
|
|
1265
|
-
usage_cost: JSON.stringify(providerCosts),
|
|
1266
|
-
usage_cost_usd: usageCostUsd,
|
|
1267
1343
|
usage_prompt_budget: this.#packets.promptBudgetFor(provider),
|
|
1268
1344
|
finish_reason: splitResponse?.callMetadata.finishReason ?? null,
|
|
1269
1345
|
model: splitResponse?.callMetadata.model ?? provider.model,
|
|
@@ -1271,15 +1347,6 @@ export default class Engine {
|
|
|
1271
1347
|
});
|
|
1272
1348
|
throw err;
|
|
1273
1349
|
}
|
|
1274
|
-
const failure = err instanceof ProviderError
|
|
1275
|
-
? { status: err.problem.status, problem: err.problem }
|
|
1276
|
-
: (() => {
|
|
1277
|
-
console.error("Provider failed outside its Problem Details contract:", err);
|
|
1278
|
-
return Results.failure("engine:provider", "provider-contract-violation", 502, "The provider failed without returning its required Problem Details.", {}, {
|
|
1279
|
-
stage: "provider-request",
|
|
1280
|
-
retryable: false,
|
|
1281
|
-
});
|
|
1282
|
-
})();
|
|
1283
1350
|
const recorded = await this.#problems.record({
|
|
1284
1351
|
workerId,
|
|
1285
1352
|
loopId,
|
|
@@ -1296,12 +1363,6 @@ export default class Engine {
|
|
|
1296
1363
|
id: turnId,
|
|
1297
1364
|
status: recorded.result.status,
|
|
1298
1365
|
packet: StoredPacket.stringify(requestPacket),
|
|
1299
|
-
usage_prompt: usage.prompt,
|
|
1300
|
-
usage_completion: usage.completion,
|
|
1301
|
-
usage_reasoning: usage.reasoning,
|
|
1302
|
-
usage_cached: usage.cached,
|
|
1303
|
-
usage_cost: JSON.stringify(providerCosts),
|
|
1304
|
-
usage_cost_usd: usageCostUsd,
|
|
1305
1366
|
usage_prompt_budget: this.#packets.promptBudgetFor(provider),
|
|
1306
1367
|
finish_reason: splitResponse?.callMetadata.finishReason ?? null,
|
|
1307
1368
|
model: splitResponse?.callMetadata.model ?? provider.model,
|
|
@@ -1313,16 +1374,30 @@ export default class Engine {
|
|
|
1313
1374
|
throw new Error("provider attempt loop completed without a response");
|
|
1314
1375
|
}
|
|
1315
1376
|
if (!splitResponse.emissionValid) {
|
|
1377
|
+
// {§invalid-emission-attempts} The first consecutive exhaustion
|
|
1378
|
+
// publishes only the raw final response and generic recovery fact.
|
|
1379
|
+
let rejectedModelEntryId;
|
|
1380
|
+
if (allowInvalidEmissionRecovery) {
|
|
1381
|
+
rejectedModelEntryId = await this.#dispatcher.writeModelEntry({
|
|
1382
|
+
verbatim: splitResponse.packetAssistant.content,
|
|
1383
|
+
workerId,
|
|
1384
|
+
loopId,
|
|
1385
|
+
turnId,
|
|
1386
|
+
sequence: nextActionIndex,
|
|
1387
|
+
folded: true,
|
|
1388
|
+
admission: "rejected",
|
|
1389
|
+
});
|
|
1390
|
+
this.#notices.push(workspaceId, loopId, {
|
|
1391
|
+
source: "engine:grammar",
|
|
1392
|
+
kind: "invalid_emission",
|
|
1393
|
+
level: "error",
|
|
1394
|
+
message: INVALID_EMISSION_RECOVERY_MESSAGE,
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1316
1397
|
await this.#db.engine_close_turn.run({
|
|
1317
1398
|
id: turnId,
|
|
1318
|
-
status: 500,
|
|
1399
|
+
status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
|
|
1319
1400
|
packet: StoredPacket.stringify(requestPacket),
|
|
1320
|
-
usage_prompt: usage.prompt,
|
|
1321
|
-
usage_completion: usage.completion,
|
|
1322
|
-
usage_reasoning: usage.reasoning,
|
|
1323
|
-
usage_cached: usage.cached,
|
|
1324
|
-
usage_cost: JSON.stringify(providerCosts),
|
|
1325
|
-
usage_cost_usd: usageCostUsd,
|
|
1326
1401
|
usage_prompt_budget: this.#packets.promptBudgetFor(provider),
|
|
1327
1402
|
finish_reason: splitResponse.callMetadata.finishReason,
|
|
1328
1403
|
model: splitResponse.callMetadata.model,
|
|
@@ -1330,14 +1405,15 @@ export default class Engine {
|
|
|
1330
1405
|
});
|
|
1331
1406
|
return {
|
|
1332
1407
|
turnId,
|
|
1333
|
-
status: 500,
|
|
1334
|
-
|
|
1408
|
+
status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
|
|
1409
|
+
outcomes: [],
|
|
1335
1410
|
fingerprint: "",
|
|
1336
1411
|
budgetStruck: enforced.struck,
|
|
1337
1412
|
budgetHardStop: false,
|
|
1338
1413
|
steerStruck: false,
|
|
1339
1414
|
emissionAttempts,
|
|
1340
1415
|
emissionExhausted: true,
|
|
1416
|
+
...(rejectedModelEntryId === undefined ? {} : { rejectedModelEntryId }),
|
|
1341
1417
|
};
|
|
1342
1418
|
}
|
|
1343
1419
|
// {§packet-stored-shape} — admitted emission data extends the packet;
|
|
@@ -1348,7 +1424,7 @@ export default class Engine {
|
|
|
1348
1424
|
}
|
|
1349
1425
|
// Non-fatal provider transport notices on an accepted turn. Forward each
|
|
1350
1426
|
// Notice with a content-offset `line:col`;
|
|
1351
|
-
// the model resolves it against its own emission — READ the folded
|
|
1427
|
+
// the model resolves it against its own emission — READ the folded model-emission row at the
|
|
1352
1428
|
// cited lines ({§model-entry}) — not an embedded snippet that would duplicate the emission.
|
|
1353
1429
|
for (const notice of response.notices ?? []) {
|
|
1354
1430
|
const located = typeof notice.position === "number"
|
|
@@ -1442,12 +1518,6 @@ export default class Engine {
|
|
|
1442
1518
|
id: turnId,
|
|
1443
1519
|
status: turnStatus,
|
|
1444
1520
|
packet: StoredPacket.stringify(packet),
|
|
1445
|
-
usage_prompt: usage.prompt,
|
|
1446
|
-
usage_completion: usage.completion,
|
|
1447
|
-
usage_reasoning: usage.reasoning,
|
|
1448
|
-
usage_cached: usage.cached,
|
|
1449
|
-
usage_cost: JSON.stringify(providerCosts),
|
|
1450
|
-
usage_cost_usd: usageCostUsd,
|
|
1451
1521
|
usage_prompt_budget: this.#packets.promptBudgetFor(provider), // {§tokenomics-client-gauge}
|
|
1452
1522
|
finish_reason: callMetadata.finishReason,
|
|
1453
1523
|
model: callMetadata.model,
|
|
@@ -1471,22 +1541,17 @@ export default class Engine {
|
|
|
1471
1541
|
// (the PLAN/SEND ops, zero actions), which is its only coherent meaning.
|
|
1472
1542
|
let realCommands = 0;
|
|
1473
1543
|
const admittedOps = packetAssistant.ops.filter((op) => {
|
|
1474
|
-
const constrained = operationConstraint !== undefined
|
|
1475
|
-
&& !operationConstraint.allowedOperations.includes(op.op);
|
|
1476
1544
|
return op.op === "PLAN"
|
|
1477
1545
|
|| op === sendOp
|
|
1478
|
-
|| constrained
|
|
1479
1546
|
|| realCommands++ < maxCommands;
|
|
1480
1547
|
});
|
|
1481
1548
|
const opsToDispatch = scheduleTurnOps(admittedOps);
|
|
1482
|
-
await this.#dispatcher.prepareEditBatches(opsToDispatch.filter((statement) => statement.op === "EDIT"
|
|
1483
|
-
&& (operationConstraint === undefined
|
|
1484
|
-
|| operationConstraint.allowedOperations.includes(statement.op))), {
|
|
1549
|
+
await this.#dispatcher.prepareEditBatches(opsToDispatch.filter((statement) => statement.op === "EDIT"), {
|
|
1485
1550
|
workspaceId, workerId, loopId, turnId,
|
|
1486
1551
|
origin, onDispatch,
|
|
1487
1552
|
});
|
|
1488
1553
|
const droppedCount = opsCount - opsToDispatch.length;
|
|
1489
|
-
const
|
|
1554
|
+
const outcomes = [];
|
|
1490
1555
|
// Running counter — a multi-file READ writes N rows from one statement (rowsWritten),
|
|
1491
1556
|
// so the next op's sequence picks up after them. Collapses to nextActionIndex+i when
|
|
1492
1557
|
// every op writes one row (the common case).
|
|
@@ -1513,7 +1578,7 @@ export default class Engine {
|
|
|
1513
1578
|
retryable: false,
|
|
1514
1579
|
}),
|
|
1515
1580
|
});
|
|
1516
|
-
|
|
1581
|
+
outcomes.push({ op: null, status: recorded.result.status });
|
|
1517
1582
|
onDispatch?.(recorded.id);
|
|
1518
1583
|
}
|
|
1519
1584
|
};
|
|
@@ -1522,20 +1587,25 @@ export default class Engine {
|
|
|
1522
1587
|
&& typeof statement.signal === "number"
|
|
1523
1588
|
&& TERMINAL_SEND_SIGNALS.has(statement.signal)) {
|
|
1524
1589
|
await recordRecoverableParseErrors();
|
|
1590
|
+
// {§worker-optimistic-settlement} — SEND judges the refreshed
|
|
1591
|
+
// lifecycle after this turn's own fast streams receive one
|
|
1592
|
+
// bounded opportunity to conclude. This is not a sleep and
|
|
1593
|
+
// does not delay a later turn for older monitored streams.
|
|
1594
|
+
const execHandler = this.#schemes.get("exec");
|
|
1595
|
+
await execHandler?.settleTurnSpawns?.(workerId, turnId, readOptimisticSettlementMs(), providerSignal);
|
|
1525
1596
|
}
|
|
1526
1597
|
const result = await observed(// {§observability-boundary}
|
|
1527
1598
|
"op.dispatch", { op: statement.op }, async (span) => {
|
|
1528
1599
|
const dispatchResult = await this.#dispatcher.dispatch({
|
|
1529
1600
|
statement, workspaceId, workerId, loopId, turnId,
|
|
1530
1601
|
sequence: rowSeq,
|
|
1531
|
-
...(operationConstraint === undefined ? {} : { operationConstraint }),
|
|
1532
1602
|
origin, onDispatch,
|
|
1533
1603
|
});
|
|
1534
1604
|
span.setAttribute("status", dispatchResult.status);
|
|
1535
1605
|
recordCounter(OPS_DISPATCHED, { op: statement.op, status: dispatchResult.status });
|
|
1536
1606
|
return dispatchResult;
|
|
1537
1607
|
});
|
|
1538
|
-
|
|
1608
|
+
outcomes.push({ op: statement.op, status: result.status });
|
|
1539
1609
|
for (const normalization of result.scopeNormalizations ?? []) {
|
|
1540
1610
|
this.#notices.push(workspaceId, loopId, {
|
|
1541
1611
|
source: "engine:slicer",
|
|
@@ -1622,7 +1692,7 @@ export default class Engine {
|
|
|
1622
1692
|
return {
|
|
1623
1693
|
turnId,
|
|
1624
1694
|
status: turnStatus,
|
|
1625
|
-
|
|
1695
|
+
outcomes,
|
|
1626
1696
|
fingerprint: StrikeRail.fingerprintTurn(packetAssistant.ops),
|
|
1627
1697
|
budgetStruck: enforced.struck,
|
|
1628
1698
|
budgetHardStop: false,
|
|
@@ -1647,10 +1717,8 @@ export default class Engine {
|
|
|
1647
1717
|
const preParsedOps = assistant.ops;
|
|
1648
1718
|
const ops = [];
|
|
1649
1719
|
// PLAN is an ordinary op — emitted by the model, dispatched, and passed to the
|
|
1650
|
-
// client as a log entry. No special hoisting into the reasoning field
|
|
1651
|
-
//
|
|
1652
|
-
// #free-text-capture synthesis of SEND[103] log ops was retired as tech debt
|
|
1653
|
-
// (grammar 0.70 forbids free text between ops, so a prose-only turn strikes 422).
|
|
1720
|
+
// client as a log entry. No special hoisting into the reasoning field. Only
|
|
1721
|
+
// structured operations are executable; interstitial text is not an operation.
|
|
1654
1722
|
// Full PlurnkParseError context is preserved on rejected attempt evidence;
|
|
1655
1723
|
// warnings remain admissible Notices. {§parse-diagnostics}
|
|
1656
1724
|
const parseErrors = [];
|
|
@@ -1671,7 +1739,6 @@ export default class Engine {
|
|
|
1671
1739
|
if (item.kind === "statement") {
|
|
1672
1740
|
ops.push(item.statement);
|
|
1673
1741
|
}
|
|
1674
|
-
// Free text (kind "text") is dropped — #free-text-capture retired (above).
|
|
1675
1742
|
else if (item.kind === "error") {
|
|
1676
1743
|
const err = item.error;
|
|
1677
1744
|
if (err instanceof PlurnkParseError) {
|
|
@@ -1758,11 +1825,28 @@ export default class Engine {
|
|
|
1758
1825
|
|| r.op === null || r.status_rx === null)
|
|
1759
1826
|
continue;
|
|
1760
1827
|
const termination = r.kind === "loop_termination";
|
|
1761
|
-
|
|
1762
|
-
? BranchReceipt.append(markTerminal(r.terminated_by, r.rx) ?? `loop "${r.prompt ?? ""}" ended (${r.status_rx})`, await BranchReceipt.render(this.#db, r.producer_worker_id))
|
|
1763
|
-
: r.rx;
|
|
1764
|
-
if (rx === null)
|
|
1828
|
+
if (r.rx === null)
|
|
1765
1829
|
throw new Error(`ambient event ${r.event_id} has no materializable result`);
|
|
1830
|
+
const terminal = termination
|
|
1831
|
+
? TerminalResult.parse(r.rx, `ambient loop-termination event ${r.event_id}`)
|
|
1832
|
+
: null;
|
|
1833
|
+
if (terminal !== null && terminal.status !== r.status_rx) {
|
|
1834
|
+
throw new Error(`ambient loop-termination event ${r.event_id} status ${r.status_rx} does not match its terminal result status ${terminal.status}`);
|
|
1835
|
+
}
|
|
1836
|
+
let attrs = r.attrs ?? "{}";
|
|
1837
|
+
if (terminal !== null) {
|
|
1838
|
+
const inherited = JSON.parse(attrs);
|
|
1839
|
+
if (inherited === null || typeof inherited !== "object" || Array.isArray(inherited)) {
|
|
1840
|
+
throw new TypeError(`ambient loop-termination event ${r.event_id} attrs must be an object`);
|
|
1841
|
+
}
|
|
1842
|
+
const receipt = await BranchReceipt.render(this.#db, r.producer_worker_id);
|
|
1843
|
+
attrs = JSON.stringify({
|
|
1844
|
+
...inherited,
|
|
1845
|
+
kind: "loop_termination",
|
|
1846
|
+
...(r.terminated_by === null ? {} : { terminatedBy: r.terminated_by }),
|
|
1847
|
+
...(receipt === null ? {} : { receipt }),
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1766
1850
|
const inserted = await this.#db.engine_insert_ambient_delta.get({
|
|
1767
1851
|
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
|
|
1768
1852
|
event_id: r.event_id,
|
|
@@ -1771,11 +1855,11 @@ export default class Engine {
|
|
|
1771
1855
|
scheme: r.scheme,
|
|
1772
1856
|
hostname: r.hostname,
|
|
1773
1857
|
pathname: r.pathname,
|
|
1774
|
-
rx,
|
|
1775
|
-
mimetype_rx:
|
|
1858
|
+
rx: r.rx,
|
|
1859
|
+
mimetype_rx: "application/json",
|
|
1776
1860
|
status: r.status_rx,
|
|
1777
|
-
expanded:
|
|
1778
|
-
attrs
|
|
1861
|
+
expanded: terminal !== null && terminal.status >= 200 && terminal.status < 300 ? 1 : 0,
|
|
1862
|
+
attrs,
|
|
1779
1863
|
});
|
|
1780
1864
|
if (inserted !== undefined)
|
|
1781
1865
|
written++;
|
|
@@ -1801,11 +1885,10 @@ export default class Engine {
|
|
|
1801
1885
|
return this.#liveSubscriptions.cancel(subscriptionId);
|
|
1802
1886
|
}
|
|
1803
1887
|
// {§env-delta} — exec streams as an instance of the ambient-observe machine:
|
|
1804
|
-
// each turn, emit each owned channel's
|
|
1805
|
-
// while the channel streams and preserves the exact terminal result when closed. Ongoing
|
|
1806
|
-
//
|
|
1807
|
-
//
|
|
1808
|
-
// env-observe loop with a byte cursor where env-delta uses a timestamp. {§exec-stream}
|
|
1888
|
+
// each turn, emit each owned channel's next publishable content as a foisted READ row. It is
|
|
1889
|
+
// 200 while the channel streams and preserves the exact terminal result when closed. Ongoing
|
|
1890
|
+
// observations fold; the terminal observation auto-OPENs. The cursor is the streamEnd recorded
|
|
1891
|
+
// on the channel's prior observation. {§exec-stream}
|
|
1809
1892
|
async #materializeStreamDeltas(args) {
|
|
1810
1893
|
const { workerId, loopId, turnId, fromSequence } = args;
|
|
1811
1894
|
const channels = await this.#db.engine_worker_stream_channels.all({ worker_id: workerId });
|
|
@@ -1847,14 +1930,15 @@ export default class Engine {
|
|
|
1847
1930
|
}
|
|
1848
1931
|
return result;
|
|
1849
1932
|
};
|
|
1850
|
-
|
|
1851
|
-
|
|
1933
|
+
const publishEnd = streamPublicationEnd(ch.content, ch.mimetype, cursor, closed);
|
|
1934
|
+
if (publishEnd <= cursor) {
|
|
1935
|
+
// The cursor-terminal race: a channel written in one final
|
|
1852
1936
|
// burst gets fully shown FOLDED while still active; the close then has zero new
|
|
1853
|
-
//
|
|
1937
|
+
// content and the auto-OPEN terminal observation never fired — the model was never shown
|
|
1854
1938
|
// the conclusion of a stream whose result it already holds folded. The same
|
|
1855
|
-
// observation is required when the
|
|
1856
|
-
// information independently of payload.
|
|
1857
|
-
// terse
|
|
1939
|
+
// observation is required when the channel produced no publishable content:
|
|
1940
|
+
// completion is information independently of payload. Text streams retain their
|
|
1941
|
+
// terse marker; structured channels emit a bodyless typed conclusion.
|
|
1858
1942
|
if (closed && priorAttrs.terminal !== true) {
|
|
1859
1943
|
const streamTarget = renderTarget({
|
|
1860
1944
|
scheme: ch.runtime,
|
|
@@ -1867,12 +1951,15 @@ export default class Engine {
|
|
|
1867
1951
|
? `full output already delivered above; READ ${streamTarget} to revisit`
|
|
1868
1952
|
: "stream produced no output";
|
|
1869
1953
|
const sequence = fromSequence + written;
|
|
1954
|
+
const content = baseMimetype(ch.mimetype).startsWith("text/")
|
|
1955
|
+
? `[ stream closed (${ch.close_status ?? 200}) - ${pointer} ]`
|
|
1956
|
+
: "";
|
|
1870
1957
|
await this.#db.engine_insert_stream_delta.run({
|
|
1871
1958
|
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
|
|
1872
1959
|
scheme: ch.runtime, pathname: ch.coord, fragment: visibleFragment,
|
|
1873
1960
|
rx: JSON.stringify(await terminalResult({
|
|
1874
|
-
content
|
|
1875
|
-
mimetype:
|
|
1961
|
+
content,
|
|
1962
|
+
mimetype: ch.mimetype,
|
|
1876
1963
|
}, sequence)),
|
|
1877
1964
|
status: terminal?.status ?? 200,
|
|
1878
1965
|
attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: true }),
|
|
@@ -1886,16 +1973,19 @@ export default class Engine {
|
|
|
1886
1973
|
// into one sequence (lines N..M, then M+1..), not N independent "1:" restarts. {§exec-stream}
|
|
1887
1974
|
const startLine = (ch.content.slice(0, cursor).match(/\n/g)?.length ?? 0) + 1;
|
|
1888
1975
|
const sequence = fromSequence + written;
|
|
1889
|
-
const
|
|
1890
|
-
|
|
1891
|
-
|
|
1976
|
+
const content = ch.content.slice(cursor, publishEnd);
|
|
1977
|
+
const terminalDelivery = closed && publishEnd === ch.content.length;
|
|
1978
|
+
const fields = { content, mimetype: ch.mimetype, startLine };
|
|
1979
|
+
const result = terminalDelivery
|
|
1980
|
+
? await terminalResult(fields, sequence)
|
|
1981
|
+
: { status: 200, ...fields };
|
|
1892
1982
|
await this.#db.engine_insert_stream_delta.run({
|
|
1893
1983
|
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
|
|
1894
1984
|
scheme: ch.runtime, pathname: ch.coord, fragment: visibleFragment,
|
|
1895
1985
|
rx: JSON.stringify(result),
|
|
1896
1986
|
status: result.status,
|
|
1897
|
-
attrs: JSON.stringify({ streamEnd:
|
|
1898
|
-
expanded:
|
|
1987
|
+
attrs: JSON.stringify({ streamEnd: publishEnd, terminal: terminalDelivery }),
|
|
1988
|
+
expanded: terminalDelivery ? 1 : 0, // {§exec-stream} — terminal observation auto-OPENs; ongoing folds
|
|
1899
1989
|
});
|
|
1900
1990
|
written++;
|
|
1901
1991
|
}
|
|
@@ -1903,9 +1993,10 @@ export default class Engine {
|
|
|
1903
1993
|
}
|
|
1904
1994
|
// {§env-delta-filesystem-narration} {§membership-emi-divergence-signal}
|
|
1905
1995
|
// — journal project-file divergence once through the reserved actor.
|
|
1906
|
-
async #logFsFictions(workspaceId, divergences) {
|
|
1996
|
+
async #logFsFictions(workspaceId, divergences, gitStatus) {
|
|
1907
1997
|
if (divergences.length === 0)
|
|
1908
1998
|
return;
|
|
1999
|
+
const gitByPath = new Map(gitStatus?.files.map(({ path, status }) => [path, status]) ?? []);
|
|
1909
2000
|
const worker = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name: "plurnk" })
|
|
1910
2001
|
?? await this.#db.envelope_insert_worker.get({ workspace_id: workspaceId, name: "plurnk", origin: "plurnk" });
|
|
1911
2002
|
if (worker === undefined)
|
|
@@ -1926,7 +2017,10 @@ export default class Engine {
|
|
|
1926
2017
|
pathname: d.pathname, query: null, fragment: null, lineMarker: null,
|
|
1927
2018
|
tx: "", mimetype_tx: "text/plain",
|
|
1928
2019
|
rx: JSON.stringify({ status: 200, entryId: d.entryId, channel: d.channel, span }), mimetype_rx: "application/json",
|
|
1929
|
-
status_rx: 200, tokens: 0, state: "resolved", outcome: null,
|
|
2020
|
+
status_rx: 200, tokens: 0, state: "resolved", outcome: null,
|
|
2021
|
+
attrs: gitByPath.has(d.pathname)
|
|
2022
|
+
? JSON.stringify({ git: gitByPath.get(d.pathname) })
|
|
2023
|
+
: "{}",
|
|
1930
2024
|
});
|
|
1931
2025
|
}
|
|
1932
2026
|
}
|