@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/server/Daemon.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// the transport-free plugin-module seam ({§rpc}).
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { resolve, dirname } from "node:path";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
6
|
import { execPollBackoffMs } from "./exec-poll-backoff.js";
|
|
6
7
|
import ChannelWrite from "../core/ChannelWrite.js";
|
|
7
8
|
import { Paths } from "../index.js";
|
|
@@ -32,6 +33,7 @@ import BranchBatches from "./BranchBatches.js";
|
|
|
32
33
|
import ErrorDetail from "../core/ErrorDetail.js";
|
|
33
34
|
import { observed, observedSync } from "../observe/spans.js";
|
|
34
35
|
import { LOOP_TERMINALS, recordCounter } from "../observe/metrics.js";
|
|
36
|
+
import { readOptimisticSettlementMs } from "../core/optimistic-settlement.js";
|
|
35
37
|
const clientActionFailure = (error) => {
|
|
36
38
|
if (error instanceof OperationFailureError)
|
|
37
39
|
return error.result;
|
|
@@ -72,6 +74,7 @@ export default class Daemon {
|
|
|
72
74
|
// (subscriptions + Exec.idle), and a concluding stream routes through
|
|
73
75
|
// inject() like any other loop source.
|
|
74
76
|
#activeDrains = new Map();
|
|
77
|
+
#drainExitTasks = new Set();
|
|
75
78
|
// Per-worker cancellation scope. Loops AND the streams they spawn (execs)
|
|
76
79
|
// share this signal, so loop.cancel / shutdown abort it once and every
|
|
77
80
|
// in-flight subscription tears down — even a spawn that registers AFTER the
|
|
@@ -93,6 +96,10 @@ export default class Daemon {
|
|
|
93
96
|
// never deadlock. The drain honors the owed wake at the worker's next park, closing the conclude-
|
|
94
97
|
// before-park race. (Only a live exec stream, unbounded absent a timeout, may hold a park open.)
|
|
95
98
|
#owedWakes = new Set();
|
|
99
|
+
// {§worker-optimistic-settlement} — one non-sliding completion-wake batch
|
|
100
|
+
// per worker. Durable conclusions and client events land before this gate;
|
|
101
|
+
// only the parked loop's provider-dispatch requeue waits.
|
|
102
|
+
#completionWakeGates = new Map();
|
|
96
103
|
constructor({ db, schemes, mimetypes, provider, nodeModulesPath, }) {
|
|
97
104
|
this.#db = db;
|
|
98
105
|
this.#lifecycle = new LoopLifecycle(db);
|
|
@@ -126,10 +133,9 @@ export default class Daemon {
|
|
|
126
133
|
});
|
|
127
134
|
this.#branchBatches = new BranchBatches(db, this.#workspaceGate, {
|
|
128
135
|
settleWorkspace: async (workspaceId) => this.#engine.drainWorkspaceDerivations(workspaceId),
|
|
129
|
-
createChild: async ({ workspaceId, parentWorkerId, op, name, prompt, flags, origin }) => {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
throw new Error("Branch worker: active provider has no resolvable alias");
|
|
136
|
+
createChild: async ({ workspaceId, parentWorkerId, parentLoopId, op, name, prompt, flags, origin }) => {
|
|
137
|
+
const parentPolicy = await this.#providerPolicyForLoop(parentLoopId);
|
|
138
|
+
const providerSpec = parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
|
|
133
139
|
const workerName = WorkerName.assert(name);
|
|
134
140
|
const workerId = op === "FORK"
|
|
135
141
|
? await Fork.fork(this.#db, parentWorkerId, workerName)
|
|
@@ -145,6 +151,7 @@ export default class Daemon {
|
|
|
145
151
|
workerId,
|
|
146
152
|
prompt,
|
|
147
153
|
providerSpec,
|
|
154
|
+
childProviderSpec: parentPolicy.childProviderSpec,
|
|
148
155
|
flags,
|
|
149
156
|
});
|
|
150
157
|
return { workerId, loopId };
|
|
@@ -162,7 +169,7 @@ export default class Daemon {
|
|
|
162
169
|
},
|
|
163
170
|
wakeParent: async (workspaceId, workerId) => {
|
|
164
171
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
165
|
-
await this.#
|
|
172
|
+
await this.#settleCompletionWake(workspaceId, workerId, systemPrompt, false);
|
|
166
173
|
},
|
|
167
174
|
notify: (workspaceId, payload) => {
|
|
168
175
|
this.#broadcast({ workspaceId }, "workspace/branch-batch", payload);
|
|
@@ -182,14 +189,25 @@ export default class Daemon {
|
|
|
182
189
|
// daemon owns provider + the law-file system prompt; the worker scheme
|
|
183
190
|
// handler carries neither. Fire-and-forget: the returned drain runs
|
|
184
191
|
// independently (the sister is its own worker). {§machine-processes}
|
|
185
|
-
injectWorker: async ({ workspaceId, workerId, prompt, flags }) => {
|
|
186
|
-
if (this.#provider === null)
|
|
187
|
-
throw new Error("injectWorker: no provider configured");
|
|
192
|
+
injectWorker: async ({ workspaceId, workerId, prompt, flags, parentLoopId }) => {
|
|
188
193
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
189
|
-
const
|
|
194
|
+
const parentPolicy = parentLoopId === undefined
|
|
195
|
+
? null
|
|
196
|
+
: await this.#providerPolicyForLoop(parentLoopId);
|
|
197
|
+
const providerSpec = parentPolicy === null
|
|
198
|
+
? resolveActiveAlias()
|
|
199
|
+
: parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
|
|
190
200
|
if (providerSpec === null)
|
|
191
201
|
throw new Error("injectWorker: active provider has no resolvable alias");
|
|
192
|
-
const { action, loopId } = await this.inject({
|
|
202
|
+
const { action, loopId } = await this.inject({
|
|
203
|
+
workspaceId,
|
|
204
|
+
workerId,
|
|
205
|
+
prompt,
|
|
206
|
+
providerSpec,
|
|
207
|
+
...(parentPolicy === null ? {} : { childProviderSpec: parentPolicy.childProviderSpec }),
|
|
208
|
+
systemPrompt,
|
|
209
|
+
...(flags === undefined ? {} : { flags }),
|
|
210
|
+
});
|
|
193
211
|
return { action, loopId };
|
|
194
212
|
},
|
|
195
213
|
branchWorker: async (args) => this.#branchBatches.enqueue(args),
|
|
@@ -243,6 +261,11 @@ export default class Daemon {
|
|
|
243
261
|
const openPaths = ClientInput.assertOpenPaths("runLoop", args.openPaths);
|
|
244
262
|
const alias = ClientInput.assertOptionalSelector("runLoop", "alias", args.alias);
|
|
245
263
|
const model = ClientInput.assertOptionalSelector("runLoop", "model", args.model);
|
|
264
|
+
const childAlias = ClientInput.assertOptionalChildAlias("runLoop", args.childAlias);
|
|
265
|
+
const childModel = ClientInput.assertOptionalSelector("runLoop", "childModel", args.childModel);
|
|
266
|
+
if (childAlias === null && childModel !== undefined) {
|
|
267
|
+
throw daemonFailure("daemon:input", "child-provider-conflict", 400, "childModel cannot accompany an inherited child provider policy.", { field: "childModel", recovery: "Omit childModel or select a child alias.", retryable: false });
|
|
268
|
+
}
|
|
246
269
|
const flags = ClientInput.normalizeLoopFlags("runLoop", args.flags);
|
|
247
270
|
// {§methods-loop-run-model} — a client sends alias/model on every loop, so a
|
|
248
271
|
// switch takes effect turn-to-turn. `model` (client-resolved <provider>/<model>) wins
|
|
@@ -256,6 +279,14 @@ export default class Daemon {
|
|
|
256
279
|
retryable: false,
|
|
257
280
|
}));
|
|
258
281
|
}
|
|
282
|
+
// {§methods-loop-run-child-provider}
|
|
283
|
+
const configuredChildAlias = childAlias === undefined && childModel === undefined
|
|
284
|
+
? process.env.PLURNK_MODEL_CHILD
|
|
285
|
+
: childAlias;
|
|
286
|
+
const childSelection = configuredChildAlias === null
|
|
287
|
+
|| (configuredChildAlias === undefined && childModel === undefined)
|
|
288
|
+
? null
|
|
289
|
+
: await this.#resolveLoopProvider(configuredChildAlias, childModel);
|
|
259
290
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
260
291
|
// {§machine-processes} — the model NEVER runs in a client-origin worker (its packets would carry
|
|
261
292
|
// client-action rows). The module resolves the model worker via ensureModelWorker and passes it (or a
|
|
@@ -296,6 +327,7 @@ export default class Daemon {
|
|
|
296
327
|
...(openPaths !== undefined ? { openPaths } : {}),
|
|
297
328
|
turnCeiling,
|
|
298
329
|
providerSpec: selection,
|
|
330
|
+
childProviderSpec: childSelection,
|
|
299
331
|
systemPrompt,
|
|
300
332
|
});
|
|
301
333
|
return { status: 100, action, loopId, ...(turnSeq !== undefined ? { turnSeq } : {}) };
|
|
@@ -332,26 +364,40 @@ export default class Daemon {
|
|
|
332
364
|
}
|
|
333
365
|
return spec;
|
|
334
366
|
}
|
|
335
|
-
|
|
336
|
-
const row = await this.#db.drain_loop_provider_spec.get({ loop_id: loopId });
|
|
337
|
-
if (row === undefined)
|
|
338
|
-
throw new Error(`loop ${loopId}: provider selection row is missing`);
|
|
367
|
+
#parseProviderSpec(loopId, field, encoded) {
|
|
339
368
|
let parsed;
|
|
340
369
|
try {
|
|
341
|
-
parsed = JSON.parse(
|
|
370
|
+
parsed = JSON.parse(encoded);
|
|
342
371
|
}
|
|
343
372
|
catch {
|
|
344
|
-
throw new Error(`loop ${loopId}: persisted
|
|
373
|
+
throw new Error(`loop ${loopId}: persisted ${field} is malformed`);
|
|
345
374
|
}
|
|
346
|
-
if (parsed === null
|
|
347
|
-
|
|
375
|
+
if (parsed === null)
|
|
376
|
+
return null;
|
|
377
|
+
if (typeof parsed.alias !== "string" || parsed.alias.length === 0
|
|
348
378
|
|| typeof parsed.provider !== "string" || parsed.provider.length === 0
|
|
349
379
|
|| typeof parsed.model !== "string" || parsed.model.length === 0
|
|
350
380
|
|| (parsed.baseUrl !== undefined && typeof parsed.baseUrl !== "string")) {
|
|
351
|
-
throw new Error(`loop ${loopId}: persisted
|
|
381
|
+
throw new Error(`loop ${loopId}: persisted ${field} is invalid`);
|
|
352
382
|
}
|
|
353
383
|
return parsed;
|
|
354
384
|
}
|
|
385
|
+
async #providerPolicyForLoop(loopId) {
|
|
386
|
+
const row = await this.#db.drain_loop_provider_spec.get({ loop_id: loopId });
|
|
387
|
+
if (row === undefined)
|
|
388
|
+
throw new Error(`loop ${loopId}: provider selection row is missing`);
|
|
389
|
+
const providerSpec = this.#parseProviderSpec(loopId, "provider_spec", row.provider_spec);
|
|
390
|
+
if (providerSpec === null) {
|
|
391
|
+
throw new Error(`loop ${loopId}: persisted provider selection is missing or invalid — refusing boot-default substitution`);
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
providerSpec,
|
|
395
|
+
childProviderSpec: this.#parseProviderSpec(loopId, "child_provider_spec", row.child_provider_spec),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
async #providerSpecForLoop(loopId) {
|
|
399
|
+
return (await this.#providerPolicyForLoop(loopId)).providerSpec;
|
|
400
|
+
}
|
|
355
401
|
async #providerForLoop(loopId) {
|
|
356
402
|
return ProviderInstantiate.instantiateProvider(await this.#providerSpecForLoop(loopId));
|
|
357
403
|
}
|
|
@@ -370,6 +416,19 @@ export default class Daemon {
|
|
|
370
416
|
});
|
|
371
417
|
}
|
|
372
418
|
}
|
|
419
|
+
async #assertLoopChildProvider(loopId, requested) {
|
|
420
|
+
const selected = (await this.#providerPolicyForLoop(loopId)).childProviderSpec;
|
|
421
|
+
if (JSON.stringify(selected) !== JSON.stringify(requested)) {
|
|
422
|
+
throw daemonFailure("daemon:provider", "loop-child-provider-conflict", 409, `Loop ${loopId} already has a different child provider policy.`, {
|
|
423
|
+
loopId,
|
|
424
|
+
selectedChildAlias: selected?.alias ?? null,
|
|
425
|
+
requestedChildAlias: requested?.alias ?? null,
|
|
426
|
+
stage: "loop-injection",
|
|
427
|
+
recovery: "Cancel or conclude the loop before changing its child provider policy.",
|
|
428
|
+
retryable: false,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
373
432
|
async #assertLoopMaxTurns(loopId, requested) {
|
|
374
433
|
if (requested === undefined)
|
|
375
434
|
return;
|
|
@@ -960,6 +1019,7 @@ export default class Daemon {
|
|
|
960
1019
|
}
|
|
961
1020
|
async #recoverLifecycle() {
|
|
962
1021
|
await this.#db.recovery_fail_active_loops.run({});
|
|
1022
|
+
await this.#db.recovery_fail_open_provider_attempts.run({});
|
|
963
1023
|
await this.#db.recovery_fail_ownerless_proposals.run({});
|
|
964
1024
|
await this.#db.recovery_error_orphan_subscription_channels.run({});
|
|
965
1025
|
await this.#db.recovery_fail_orphan_subscriptions.run({});
|
|
@@ -1009,7 +1069,9 @@ export default class Daemon {
|
|
|
1009
1069
|
// Settle the stopped world FIRST: a drain paused at a pending proposal awaits a resolution
|
|
1010
1070
|
// that will never arrive once clients are gone — allSettled(drains) below would deadlock
|
|
1011
1071
|
// the stop forever (a daemon with a pending HITL proposal could not shut down).
|
|
1072
|
+
const derivationAbort = new DOMException("daemon stopping", "AbortError");
|
|
1012
1073
|
this.#engine.cancelAllProposals("daemon_stopping");
|
|
1074
|
+
this.#engine.cancelDerivations(derivationAbort);
|
|
1013
1075
|
this.#branchBatches.beginStop();
|
|
1014
1076
|
for (const scope of this.#workerAborts.values()) {
|
|
1015
1077
|
if (!scope.signal.aborted)
|
|
@@ -1028,10 +1090,11 @@ export default class Daemon {
|
|
|
1028
1090
|
await this.#branchBatches.idle();
|
|
1029
1091
|
const drainPromises = [...this.#activeDrains.values()].map((d) => d.promise);
|
|
1030
1092
|
await Promise.allSettled(drainPromises);
|
|
1093
|
+
await Promise.allSettled([...this.#drainExitTasks]);
|
|
1031
1094
|
const closeResults = await moduleClose;
|
|
1032
1095
|
const [streamingResult] = await Promise.allSettled([this.#drainStreamingSchemes()]);
|
|
1033
1096
|
const [derivationResult] = await Promise.allSettled([
|
|
1034
|
-
this.#engine.drainDerivations(), // active workspace warms
|
|
1097
|
+
this.#engine.drainDerivations(derivationAbort), // active workspace warms settle before the db closes upstream
|
|
1035
1098
|
]);
|
|
1036
1099
|
const mimetypeResults = this.#ownsMimetypes
|
|
1037
1100
|
? await Promise.allSettled([this.#mimetypes.dispose()])
|
|
@@ -1108,6 +1171,8 @@ export default class Daemon {
|
|
|
1108
1171
|
if (active !== undefined) {
|
|
1109
1172
|
await this.#assertFoldPosture(workerId, args.flags, active.id); // compare with the exact durable loop
|
|
1110
1173
|
await this.#assertLoopProvider(active.id, args.providerSpec);
|
|
1174
|
+
if (args.childProviderSpec !== undefined)
|
|
1175
|
+
await this.#assertLoopChildProvider(active.id, args.childProviderSpec);
|
|
1111
1176
|
await this.#assertLoopMaxTurns(active.id, args.turnCeiling?.source === "explicit" ? args.turnCeiling.effective : undefined);
|
|
1112
1177
|
}
|
|
1113
1178
|
const result = await this.#engine.inject(workerId, prompt, args.openPaths ?? []);
|
|
@@ -1125,6 +1190,8 @@ export default class Daemon {
|
|
|
1125
1190
|
if (slept !== undefined) {
|
|
1126
1191
|
await this.#assertFoldPosture(workerId, args.flags, slept.id); // resume drops nothing silently
|
|
1127
1192
|
await this.#assertLoopProvider(slept.id, args.providerSpec);
|
|
1193
|
+
if (args.childProviderSpec !== undefined)
|
|
1194
|
+
await this.#assertLoopChildProvider(slept.id, args.childProviderSpec);
|
|
1128
1195
|
await this.#assertLoopMaxTurns(slept.id, args.turnCeiling?.source === "explicit" ? args.turnCeiling.effective : undefined);
|
|
1129
1196
|
const injected = await this.#engine.inject(workerId, prompt, args.openPaths ?? []);
|
|
1130
1197
|
await this.#lifecycle.wake(slept.id);
|
|
@@ -1138,6 +1205,7 @@ export default class Daemon {
|
|
|
1138
1205
|
workerId,
|
|
1139
1206
|
prompt,
|
|
1140
1207
|
providerSpec: args.providerSpec,
|
|
1208
|
+
childProviderSpec: args.childProviderSpec ?? null,
|
|
1141
1209
|
maxTurns: args.turnCeiling?.effective,
|
|
1142
1210
|
flags: args.flags,
|
|
1143
1211
|
openPaths: args.openPaths,
|
|
@@ -1168,6 +1236,7 @@ export default class Daemon {
|
|
|
1168
1236
|
sequence: seqRow.next,
|
|
1169
1237
|
prompt: args.prompt,
|
|
1170
1238
|
provider_spec: JSON.stringify(args.providerSpec),
|
|
1239
|
+
child_provider_spec: JSON.stringify(args.childProviderSpec),
|
|
1171
1240
|
max_turns: args.maxTurns ?? Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "50"),
|
|
1172
1241
|
});
|
|
1173
1242
|
if (loopRow === undefined)
|
|
@@ -1353,12 +1422,18 @@ export default class Daemon {
|
|
|
1353
1422
|
// {§methods-loop-cancel} — loop.cancel / shutdown aborted the live drain. A cancellation
|
|
1354
1423
|
// is the loop's TERMINAL state (499), delivered via loop/terminated (runLoop no
|
|
1355
1424
|
// longer blocks to return it). A genuine error rejects firstLoopPromise.
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
:
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
:
|
|
1425
|
+
let usage = {
|
|
1426
|
+
promptTokens: 0,
|
|
1427
|
+
completionTokens: 0,
|
|
1428
|
+
reasoningTokens: 0,
|
|
1429
|
+
cachedTokens: 0,
|
|
1430
|
+
costUsd: 0,
|
|
1431
|
+
costs: [],
|
|
1432
|
+
contextTokens: 0,
|
|
1433
|
+
promptBudget: null,
|
|
1434
|
+
meta: {},
|
|
1435
|
+
};
|
|
1436
|
+
let attributions = [];
|
|
1362
1437
|
const message = ErrorDetail.preview(controller.signal.reason ?? "user_cancelled")
|
|
1363
1438
|
|| "no reason was supplied";
|
|
1364
1439
|
if (currentLoopId !== null) {
|
|
@@ -1369,6 +1444,10 @@ export default class Daemon {
|
|
|
1369
1444
|
stage: "loop",
|
|
1370
1445
|
retryable: false,
|
|
1371
1446
|
}), { terminatedBy: "cancel" });
|
|
1447
|
+
[usage, attributions] = await Promise.all([
|
|
1448
|
+
this.#engine.loopUsage(currentLoopId),
|
|
1449
|
+
this.#engine.loopAttributions(currentLoopId),
|
|
1450
|
+
]);
|
|
1372
1451
|
if (cancelled !== null) {
|
|
1373
1452
|
this.#broadcast({ workspaceId }, "loop/terminated", {
|
|
1374
1453
|
workerId,
|
|
@@ -1456,8 +1535,12 @@ export default class Daemon {
|
|
|
1456
1535
|
// Topology join ({§worker-loop-lifecycle}): when this drain exits having CONCLUDED the worker, wake its parent
|
|
1457
1536
|
// if parked. Runs after the drain fully tears down (settled promise) so the quiescence check sees
|
|
1458
1537
|
// final state; speculative (#onDrainExit no-ops unless the worker concluded AND the parent is parked).
|
|
1459
|
-
|
|
1538
|
+
const drainExitTask = drainPromise.then(() => this.#onDrainExit(workspaceId, workerId, systemPrompt), () => this.#onDrainExit(workspaceId, workerId, systemPrompt));
|
|
1539
|
+
this.#drainExitTasks.add(drainExitTask);
|
|
1540
|
+
void drainExitTask.catch((err) => {
|
|
1460
1541
|
console.error(`parent wake after worker ${workerId} settlement failed:`, err);
|
|
1542
|
+
}).finally(() => {
|
|
1543
|
+
this.#drainExitTasks.delete(drainExitTask);
|
|
1461
1544
|
});
|
|
1462
1545
|
// Swallow unhandled rejections (drain aborts with no awaiter); the
|
|
1463
1546
|
// error already surfaced via firstLoopPromise or was logged inside.
|
|
@@ -1515,6 +1598,7 @@ export default class Daemon {
|
|
|
1515
1598
|
prompt: first.body,
|
|
1516
1599
|
flags: first.flags,
|
|
1517
1600
|
provider_spec: first.provider_spec,
|
|
1601
|
+
child_provider_spec: first.child_provider_spec,
|
|
1518
1602
|
max_turns: first.max_turns,
|
|
1519
1603
|
open_paths: first.open_paths ?? "[]",
|
|
1520
1604
|
orphan_source_loop_id: endedLoopId,
|
|
@@ -1632,23 +1716,24 @@ export default class Daemon {
|
|
|
1632
1716
|
}
|
|
1633
1717
|
/**
|
|
1634
1718
|
* Wake-on-completion handler. Streaming schemes call this when a
|
|
1635
|
-
* subscription closes.
|
|
1636
|
-
*
|
|
1637
|
-
*
|
|
1638
|
-
* summary as the user prompt so the model gets a chance to react.
|
|
1719
|
+
* subscription closes. A parked loop resumes in place; an active loop
|
|
1720
|
+
* observes the channel transition at its next turn boundary. No synthetic
|
|
1721
|
+
* prompt or replacement loop is created.
|
|
1639
1722
|
*
|
|
1640
1723
|
* Skipped on result.status=499 (aborted): the model already knows about
|
|
1641
|
-
* its own SEND[499], and a forcefully-cancelled loop's spawn-abort
|
|
1642
|
-
*
|
|
1724
|
+
* its own SEND[499], and a forcefully-cancelled loop's spawn-abort must
|
|
1725
|
+
* not resurrect the worker.
|
|
1643
1726
|
*/
|
|
1644
1727
|
async #handleWakeWorker(payload) {
|
|
1728
|
+
const { entryOwnerId, ...wake } = payload;
|
|
1729
|
+
const conclusion = { ...wake, workerId: entryOwnerId };
|
|
1645
1730
|
// {§search-gate} — settle the dedup registration: promote on a 200 conclusion, drop on
|
|
1646
1731
|
// failure (a dead search must never serve as a duplicate). No-op for non-search streams.
|
|
1647
1732
|
this.#engine.searchGate.settle(payload.target.replace(/^[a-z+.-]+:\/\//, "/").replace(/^\/+/, "/"), payload.result.status);
|
|
1648
1733
|
// Aborted streams don't wake — the abort was deliberate.
|
|
1649
1734
|
if (payload.result.status === 499) {
|
|
1650
1735
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1651
|
-
...
|
|
1736
|
+
...conclusion, wakeAction: "skipped-aborted",
|
|
1652
1737
|
});
|
|
1653
1738
|
return;
|
|
1654
1739
|
}
|
|
@@ -1662,7 +1747,7 @@ export default class Daemon {
|
|
|
1662
1747
|
const scope = this.#workerAborts.get(payload.workerId);
|
|
1663
1748
|
if (scope?.signal.aborted === true && !this.#activeDrains.has(payload.workerId)) {
|
|
1664
1749
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1665
|
-
...
|
|
1750
|
+
...conclusion, wakeAction: "skipped-cancelled",
|
|
1666
1751
|
});
|
|
1667
1752
|
return;
|
|
1668
1753
|
}
|
|
@@ -1676,16 +1761,14 @@ export default class Daemon {
|
|
|
1676
1761
|
// the manifest. {§worker-lifecycle-wake-liveness}.
|
|
1677
1762
|
const slept = await this.#db.drain_find_slept_loop.get({ worker_id: payload.workerId });
|
|
1678
1763
|
if (slept !== undefined) {
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1764
|
+
// {§worker-optimistic-settlement} — publish this conclusion now,
|
|
1765
|
+
// then let the worker-local gate coalesce only the provider
|
|
1766
|
+
// dispatch. Concurrent stream/child callbacks join that one gate.
|
|
1767
|
+
void this.#settleCompletionWake(payload.workspaceId, payload.workerId, systemPrompt, false).catch((err) => {
|
|
1768
|
+
console.error("completion wake settlement failed:", err instanceof Error ? err.message : String(err));
|
|
1683
1769
|
});
|
|
1684
1770
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1685
|
-
...
|
|
1686
|
-
});
|
|
1687
|
-
started?.drainPromise?.catch((err) => {
|
|
1688
|
-
console.error("wake resume drain failed:", err instanceof Error ? err.message : String(err));
|
|
1771
|
+
...conclusion, wakeAction: "resumed-loop", wakeLoopId: slept.id,
|
|
1689
1772
|
});
|
|
1690
1773
|
return;
|
|
1691
1774
|
}
|
|
@@ -1695,14 +1778,14 @@ export default class Daemon {
|
|
|
1695
1778
|
// synthesis (which clobbered the model's actual goal) is retired; just tell the client.
|
|
1696
1779
|
if (this.#activeDrains.has(payload.workerId)) {
|
|
1697
1780
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1698
|
-
...
|
|
1781
|
+
...conclusion, wakeAction: "no-op-active-loop",
|
|
1699
1782
|
});
|
|
1700
1783
|
return;
|
|
1701
1784
|
}
|
|
1702
1785
|
// No slept loop, no active drain — nothing to resume (e.g. a SEND[200]-done worker whose
|
|
1703
1786
|
// streams were swept). Surface the conclusion without opening a loop.
|
|
1704
1787
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1705
|
-
...
|
|
1788
|
+
...conclusion, wakeAction: "no-loop",
|
|
1706
1789
|
});
|
|
1707
1790
|
}
|
|
1708
1791
|
catch (err) {
|
|
@@ -1748,13 +1831,13 @@ export default class Daemon {
|
|
|
1748
1831
|
delayMs = execPollBackoffMs(step, base, turns);
|
|
1749
1832
|
this.#pollBackoff.set(workerId, step + 1);
|
|
1750
1833
|
}
|
|
1751
|
-
// Floored by the
|
|
1752
|
-
// faster than
|
|
1753
|
-
const
|
|
1834
|
+
// Floored by the optimistic settlement cap so a `<…,1>` cannot wake a
|
|
1835
|
+
// parked loop faster than the preceding turn's settlement scale.
|
|
1836
|
+
const optimisticWaitMs = readOptimisticSettlementMs();
|
|
1754
1837
|
const timer = setTimeout(() => {
|
|
1755
1838
|
this.#pollTimers.delete(workerId);
|
|
1756
1839
|
void this.#wakeParkedWorker(workspaceId, workerId, systemPrompt);
|
|
1757
|
-
}, Math.max(delayMs,
|
|
1840
|
+
}, Math.max(delayMs, optimisticWaitMs));
|
|
1758
1841
|
timer.unref();
|
|
1759
1842
|
this.#pollTimers.set(workerId, timer);
|
|
1760
1843
|
}
|
|
@@ -1763,6 +1846,8 @@ export default class Daemon {
|
|
|
1763
1846
|
* or a child worker finishing ({§worker-loop-lifecycle} topology join) all call this. A no-op if the worker was
|
|
1764
1847
|
* cancelled or isn't actually parked (no slept loop) — so calling it speculatively is safe. */
|
|
1765
1848
|
async #wakeParkedWorker(workspaceId, workerId, systemPrompt, oweIfActive = true) {
|
|
1849
|
+
if (!this.#started)
|
|
1850
|
+
return;
|
|
1766
1851
|
const scope = this.#workerAborts.get(workerId);
|
|
1767
1852
|
if (scope?.signal.aborted === true && !this.#activeDrains.has(workerId))
|
|
1768
1853
|
return; // cancelled — no resurrection
|
|
@@ -1772,17 +1857,100 @@ export default class Daemon {
|
|
|
1772
1857
|
// conclusion that fired this wake arrived before the 202 committed (the conclude-before-park
|
|
1773
1858
|
// race). Owe the wake: the drain honors it at park so a worker hibernation never deadlocks.
|
|
1774
1859
|
// (No active drain → already concluded/running; nothing to wake.)
|
|
1775
|
-
if (
|
|
1776
|
-
|
|
1860
|
+
if (this.#activeDrains.has(workerId)) {
|
|
1861
|
+
if (oweIfActive)
|
|
1862
|
+
this.#owedWakes.add(workerId);
|
|
1863
|
+
}
|
|
1777
1864
|
return;
|
|
1778
1865
|
}
|
|
1779
|
-
await this.#lifecycle.wake(slept.id);
|
|
1866
|
+
const woke = await this.#lifecycle.wake(slept.id);
|
|
1867
|
+
if (!woke)
|
|
1868
|
+
return;
|
|
1780
1869
|
const started = await this.#ensureDrain({
|
|
1781
1870
|
workspaceId, workerId, systemPrompt,
|
|
1782
1871
|
});
|
|
1783
1872
|
started?.drainPromise?.catch((err) => {
|
|
1784
|
-
|
|
1873
|
+
if (this.#started) {
|
|
1874
|
+
console.error("wake-parked resume drain failed:", err instanceof Error ? err.message : String(err));
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
async #workerHasLiveObligation(workerId) {
|
|
1879
|
+
const [openSubscriptions, liveChild] = await Promise.all([
|
|
1880
|
+
this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId }),
|
|
1881
|
+
this.#db.engine_worker_has_live_child.get({ worker_id: workerId }),
|
|
1882
|
+
]);
|
|
1883
|
+
return openSubscriptions.length > 0 || liveChild !== undefined;
|
|
1884
|
+
}
|
|
1885
|
+
#settleCompletionWake(workspaceId, workerId, systemPrompt, oweIfActive = true) {
|
|
1886
|
+
const existing = this.#completionWakeGates.get(workerId);
|
|
1887
|
+
if (existing !== undefined) {
|
|
1888
|
+
existing.conclusions++;
|
|
1889
|
+
existing.poke.resolve();
|
|
1890
|
+
return existing.promise;
|
|
1891
|
+
}
|
|
1892
|
+
const completed = Promise.withResolvers();
|
|
1893
|
+
const gate = {
|
|
1894
|
+
conclusions: 1,
|
|
1895
|
+
poke: Promise.withResolvers(),
|
|
1896
|
+
promise: completed.promise,
|
|
1897
|
+
};
|
|
1898
|
+
this.#completionWakeGates.set(workerId, gate);
|
|
1899
|
+
void this.#runCompletionWake(workspaceId, workerId, systemPrompt, oweIfActive, gate).then(completed.resolve, completed.reject).finally(() => {
|
|
1900
|
+
if (this.#completionWakeGates.get(workerId) === gate) {
|
|
1901
|
+
this.#completionWakeGates.delete(workerId);
|
|
1902
|
+
}
|
|
1785
1903
|
});
|
|
1904
|
+
return gate.promise;
|
|
1905
|
+
}
|
|
1906
|
+
async #runCompletionWake(workspaceId, workerId, systemPrompt, oweIfActive, gate) {
|
|
1907
|
+
const slept = await this.#db.drain_find_slept_loop.get({ worker_id: workerId });
|
|
1908
|
+
if (slept === undefined) {
|
|
1909
|
+
this.#releaseCompletionWake(workerId, gate);
|
|
1910
|
+
return this.#wakeParkedWorker(workspaceId, workerId, systemPrompt, oweIfActive);
|
|
1911
|
+
}
|
|
1912
|
+
const timeoutMs = readOptimisticSettlementMs();
|
|
1913
|
+
if (timeoutMs === 0 || !(await this.#workerHasLiveObligation(workerId))) {
|
|
1914
|
+
this.#releaseCompletionWake(workerId, gate);
|
|
1915
|
+
return this.#wakeParkedWorker(workspaceId, workerId, systemPrompt, false);
|
|
1916
|
+
}
|
|
1917
|
+
return observed("worker.wake.settlement", { "window.ms": timeoutMs }, async (span) => {
|
|
1918
|
+
const startedAt = performance.now();
|
|
1919
|
+
const signal = this.#workerAborts.get(workerId)?.signal;
|
|
1920
|
+
const deadline = delay(timeoutMs, "deadline", { signal, ref: false })
|
|
1921
|
+
.catch((cause) => {
|
|
1922
|
+
if (signal?.aborted === true)
|
|
1923
|
+
return "cancelled";
|
|
1924
|
+
throw cause;
|
|
1925
|
+
});
|
|
1926
|
+
let release = "quiescent";
|
|
1927
|
+
while (await this.#workerHasLiveObligation(workerId)) {
|
|
1928
|
+
const poke = gate.poke;
|
|
1929
|
+
const outcome = await Promise.race([
|
|
1930
|
+
poke.promise.then(() => "arrival"),
|
|
1931
|
+
deadline,
|
|
1932
|
+
]);
|
|
1933
|
+
if (outcome === "arrival") {
|
|
1934
|
+
if (gate.poke === poke)
|
|
1935
|
+
gate.poke = Promise.withResolvers();
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
release = outcome;
|
|
1939
|
+
break;
|
|
1940
|
+
}
|
|
1941
|
+
span.setAttribute("release", release);
|
|
1942
|
+
span.setAttribute("conclusions", gate.conclusions);
|
|
1943
|
+
span.setAttribute("elapsed.ms", Math.round(performance.now() - startedAt));
|
|
1944
|
+
this.#releaseCompletionWake(workerId, gate);
|
|
1945
|
+
if (release === "cancelled")
|
|
1946
|
+
return;
|
|
1947
|
+
return this.#wakeParkedWorker(workspaceId, workerId, systemPrompt, false);
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
#releaseCompletionWake(workerId, gate) {
|
|
1951
|
+
if (this.#completionWakeGates.get(workerId) === gate) {
|
|
1952
|
+
this.#completionWakeGates.delete(workerId);
|
|
1953
|
+
}
|
|
1786
1954
|
}
|
|
1787
1955
|
/** A worker's drain exited. If the worker truly CONCLUDED — no 202-blocked loop, no open stream — then
|
|
1788
1956
|
* wake its PARENT in place if the parent is blocked on the join (the structured-concurrency join — a
|
|
@@ -1800,7 +1968,7 @@ export default class Daemon {
|
|
|
1800
1968
|
const parent = await this.#db.worker_parent_id.get({ worker_id: workerId });
|
|
1801
1969
|
if (parent?.parent_worker_id == null)
|
|
1802
1970
|
return; // a root worker — nobody to wake
|
|
1803
|
-
await this.#
|
|
1971
|
+
await this.#settleCompletionWake(workspaceId, parent.parent_worker_id, systemPrompt);
|
|
1804
1972
|
}
|
|
1805
1973
|
// {§methods-event-subscribe} — subscriber failures are transport-local:
|
|
1806
1974
|
// log them at this boundary and never re-enter engine control flow.
|