@plurnk/plurnk-service 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.defaults +37 -31
- package/INSTALL.md +16 -11
- package/README.md +1 -0
- package/SPEC.md +631 -397
- 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/edit-collision.d.ts +5 -0
- package/dist/content/edit-collision.d.ts.map +1 -0
- package/dist/content/edit-collision.js +14 -0
- package/dist/content/edit-collision.js.map +1 -0
- package/dist/content/edit-receipt.d.ts +3 -2
- package/dist/content/edit-receipt.d.ts.map +1 -1
- package/dist/content/edit-receipt.js +58 -13
- package/dist/content/edit-receipt.js.map +1 -1
- package/dist/content/index.d.ts +5 -1
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -1
- package/dist/content/index.js.map +1 -1
- package/dist/content/line-anchors.d.ts +41 -0
- package/dist/content/line-anchors.d.ts.map +1 -0
- package/dist/content/line-anchors.js +191 -0
- package/dist/content/line-anchors.js.map +1 -0
- 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 +20 -0
- package/dist/content/read-projector.d.ts.map +1 -0
- package/dist/content/read-projector.js +122 -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/BudgetReadout.d.ts.map +1 -1
- package/dist/core/BudgetReadout.js +9 -2
- package/dist/core/BudgetReadout.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 +3 -3
- 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 +14 -8
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +656 -312
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/DurableStatement.d.ts.map +1 -1
- package/dist/core/DurableStatement.js +2 -0
- package/dist/core/DurableStatement.js.map +1 -1
- package/dist/core/Engine.d.ts +24 -18
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +620 -392
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +188 -119
- package/dist/core/ExecutableTools.d.ts +10 -0
- package/dist/core/ExecutableTools.d.ts.map +1 -0
- package/dist/core/ExecutableTools.js +41 -0
- package/dist/core/ExecutableTools.js.map +1 -0
- package/dist/core/ExecutorRegistry.d.ts +3 -3
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +2 -2
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/LogBody.d.ts +4 -1
- package/dist/core/LogBody.d.ts.map +1 -1
- package/dist/core/LogBody.js +46 -2
- package/dist/core/LogBody.js.map +1 -1
- package/dist/core/LogEntryProjection.d.ts +14 -0
- package/dist/core/LogEntryProjection.d.ts.map +1 -0
- package/dist/core/LogEntryProjection.js +38 -0
- package/dist/core/LogEntryProjection.js.map +1 -0
- 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/ModelCall.d.ts +27 -0
- package/dist/core/ModelCall.d.ts.map +1 -0
- package/dist/core/ModelCall.js +125 -0
- package/dist/core/ModelCall.js.map +1 -0
- package/dist/core/PacketBuilder.d.ts +13 -12
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +146 -118
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProblemLog.d.ts.map +1 -1
- package/dist/core/ProblemLog.js +1 -0
- package/dist/core/ProblemLog.js.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +3 -4
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +23 -6
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/SchemeRegistry.js +7 -7
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/StrikeRail.d.ts +5 -2
- package/dist/core/StrikeRail.d.ts.map +1 -1
- package/dist/core/StrikeRail.js +10 -7
- 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 +9 -5
- 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 +2 -1
- package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
- package/dist/core/caps/DbEntryCaps.js +21 -8
- 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/DbProjectionCaps.d.ts +1 -0
- package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbProjectionCaps.js +29 -0
- package/dist/core/caps/DbProjectionCaps.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/SchemeCtxImpl.d.ts +11 -3
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +10 -8
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/fork.d.ts.map +1 -1
- package/dist/core/fork.js +23 -7
- package/dist/core/fork.js.map +1 -1
- package/dist/core/fork.sql +35 -14
- 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 +87 -26
- 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 +36 -17
- 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 +5 -2
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +318 -114
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/provider-accounting.d.ts +24 -0
- package/dist/core/provider-accounting.d.ts.map +1 -0
- package/dist/core/provider-accounting.js +76 -0
- package/dist/core/provider-accounting.js.map +1 -0
- 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/search-gate.d.ts +2 -2
- package/dist/core/search-gate.d.ts.map +1 -1
- package/dist/core/search-gate.js +8 -8
- package/dist/core/search-gate.js.map +1 -1
- package/dist/core/token-ruler.js +1 -1
- package/dist/core/token-ruler.js.map +1 -1
- package/dist/core/turn-scheduler.js +1 -1
- package/dist/core/turn-scheduler.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 +349 -76
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/digest.sql +42 -16
- package/dist/schemes/Exec.d.ts +4 -4
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +167 -134
- 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 +5 -6
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +35 -41
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts +16 -5
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +229 -262
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +50 -27
- package/dist/schemes/Prompt.d.ts +3 -4
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +5 -5
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/Skill.d.ts +4 -5
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +3 -3
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts +7 -7
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +99 -84
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts +2 -2
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +29 -10
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +11 -17
- package/dist/schemes/_entry-find.d.ts +41 -14
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +159 -166
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-find.sql +2 -21
- package/dist/schemes/_entry-graph.sql +1 -1
- package/dist/schemes/_entry-manifest.d.ts +5 -17
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +28 -24
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts +13 -8
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +98 -119
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +31 -6
- 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/_entry-send.js +4 -4
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/schemes/_search-index.d.ts.map +1 -1
- package/dist/schemes/_search-index.js +16 -6
- package/dist/schemes/_search-index.js.map +1 -1
- package/dist/schemes/exec-abort.js +2 -2
- package/dist/schemes/exec-abort.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 +6 -8
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +238 -68
- 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 +8 -7
- package/dist/server/envelope.d.ts +0 -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 +3 -4
- package/dist/server/lifecycle-recovery.sql +44 -1
- package/dist/server/logEntry.d.ts +2 -1
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +15 -2
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/logEntry.sql +5 -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 +730 -221
- package/package.json +53 -46
- package/dist/core/BudgetOverflow.d.ts +0 -23
- package/dist/core/BudgetOverflow.d.ts.map +0 -1
- package/dist/core/BudgetOverflow.js +0 -61
- package/dist/core/BudgetOverflow.js.map +0 -1
- package/dist/core/caps/DbTagCaps.d.ts +0 -10
- package/dist/core/caps/DbTagCaps.d.ts.map +0 -1
- package/dist/core/caps/DbTagCaps.js +0 -44
- package/dist/core/caps/DbTagCaps.js.map +0 -1
- 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";
|
|
@@ -9,6 +10,8 @@ import Engine from "../core/Engine.js";
|
|
|
9
10
|
import ExecutorRegistry from "../core/ExecutorRegistry.js";
|
|
10
11
|
import SchemeRegistry from "../core/SchemeRegistry.js";
|
|
11
12
|
import { Mimetypes } from "@plurnk/plurnk-mimetypes";
|
|
13
|
+
import { RuntimeDeclaration } from "@plurnk/plurnk-execs";
|
|
14
|
+
import { aggregateProviderAccounting } from "@plurnk/plurnk-providers";
|
|
12
15
|
import { parsePath, Validator, } from "@plurnk/plurnk-contracts";
|
|
13
16
|
import LogEntry from "./logEntry.js";
|
|
14
17
|
import Envelope from "./envelope.js";
|
|
@@ -32,6 +35,7 @@ import BranchBatches from "./BranchBatches.js";
|
|
|
32
35
|
import ErrorDetail from "../core/ErrorDetail.js";
|
|
33
36
|
import { observed, observedSync } from "../observe/spans.js";
|
|
34
37
|
import { LOOP_TERMINALS, recordCounter } from "../observe/metrics.js";
|
|
38
|
+
import { readOptimisticSettlementMs } from "../core/optimistic-settlement.js";
|
|
35
39
|
const clientActionFailure = (error) => {
|
|
36
40
|
if (error instanceof OperationFailureError)
|
|
37
41
|
return error.result;
|
|
@@ -72,6 +76,7 @@ export default class Daemon {
|
|
|
72
76
|
// (subscriptions + Exec.idle), and a concluding stream routes through
|
|
73
77
|
// inject() like any other loop source.
|
|
74
78
|
#activeDrains = new Map();
|
|
79
|
+
#drainExitTasks = new Set();
|
|
75
80
|
// Per-worker cancellation scope. Loops AND the streams they spawn (execs)
|
|
76
81
|
// share this signal, so loop.cancel / shutdown abort it once and every
|
|
77
82
|
// in-flight subscription tears down — even a spawn that registers AFTER the
|
|
@@ -93,6 +98,10 @@ export default class Daemon {
|
|
|
93
98
|
// never deadlock. The drain honors the owed wake at the worker's next park, closing the conclude-
|
|
94
99
|
// before-park race. (Only a live exec stream, unbounded absent a timeout, may hold a park open.)
|
|
95
100
|
#owedWakes = new Set();
|
|
101
|
+
// {§worker-optimistic-settlement} — one non-sliding completion-wake batch
|
|
102
|
+
// per worker. Durable conclusions and client events land before this gate;
|
|
103
|
+
// only the parked loop's provider-dispatch requeue waits.
|
|
104
|
+
#completionWakeGates = new Map();
|
|
96
105
|
constructor({ db, schemes, mimetypes, provider, nodeModulesPath, }) {
|
|
97
106
|
this.#db = db;
|
|
98
107
|
this.#lifecycle = new LoopLifecycle(db);
|
|
@@ -126,10 +135,9 @@ export default class Daemon {
|
|
|
126
135
|
});
|
|
127
136
|
this.#branchBatches = new BranchBatches(db, this.#workspaceGate, {
|
|
128
137
|
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");
|
|
138
|
+
createChild: async ({ workspaceId, parentWorkerId, parentLoopId, op, name, prompt, flags, origin }) => {
|
|
139
|
+
const parentPolicy = await this.#providerPolicyForLoop(parentLoopId);
|
|
140
|
+
const providerSpec = parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
|
|
133
141
|
const workerName = WorkerName.assert(name);
|
|
134
142
|
const workerId = op === "FORK"
|
|
135
143
|
? await Fork.fork(this.#db, parentWorkerId, workerName)
|
|
@@ -145,6 +153,7 @@ export default class Daemon {
|
|
|
145
153
|
workerId,
|
|
146
154
|
prompt,
|
|
147
155
|
providerSpec,
|
|
156
|
+
childProviderSpec: parentPolicy.childProviderSpec,
|
|
148
157
|
flags,
|
|
149
158
|
});
|
|
150
159
|
return { workerId, loopId };
|
|
@@ -162,7 +171,7 @@ export default class Daemon {
|
|
|
162
171
|
},
|
|
163
172
|
wakeParent: async (workspaceId, workerId) => {
|
|
164
173
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
165
|
-
await this.#
|
|
174
|
+
await this.#settleCompletionWake(workspaceId, workerId, systemPrompt, false);
|
|
166
175
|
},
|
|
167
176
|
notify: (workspaceId, payload) => {
|
|
168
177
|
this.#broadcast({ workspaceId }, "workspace/branch-batch", payload);
|
|
@@ -182,14 +191,25 @@ export default class Daemon {
|
|
|
182
191
|
// daemon owns provider + the law-file system prompt; the worker scheme
|
|
183
192
|
// handler carries neither. Fire-and-forget: the returned drain runs
|
|
184
193
|
// 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");
|
|
194
|
+
injectWorker: async ({ workspaceId, workerId, prompt, flags, parentLoopId }) => {
|
|
188
195
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
189
|
-
const
|
|
196
|
+
const parentPolicy = parentLoopId === undefined
|
|
197
|
+
? null
|
|
198
|
+
: await this.#providerPolicyForLoop(parentLoopId);
|
|
199
|
+
const providerSpec = parentPolicy === null
|
|
200
|
+
? resolveActiveAlias()
|
|
201
|
+
: parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
|
|
190
202
|
if (providerSpec === null)
|
|
191
203
|
throw new Error("injectWorker: active provider has no resolvable alias");
|
|
192
|
-
const { action, loopId } = await this.inject({
|
|
204
|
+
const { action, loopId } = await this.inject({
|
|
205
|
+
workspaceId,
|
|
206
|
+
workerId,
|
|
207
|
+
prompt,
|
|
208
|
+
providerSpec,
|
|
209
|
+
...(parentPolicy === null ? {} : { childProviderSpec: parentPolicy.childProviderSpec }),
|
|
210
|
+
systemPrompt,
|
|
211
|
+
...(flags === undefined ? {} : { flags }),
|
|
212
|
+
});
|
|
193
213
|
return { action, loopId };
|
|
194
214
|
},
|
|
195
215
|
branchWorker: async (args) => this.#branchBatches.enqueue(args),
|
|
@@ -243,6 +263,11 @@ export default class Daemon {
|
|
|
243
263
|
const openPaths = ClientInput.assertOpenPaths("runLoop", args.openPaths);
|
|
244
264
|
const alias = ClientInput.assertOptionalSelector("runLoop", "alias", args.alias);
|
|
245
265
|
const model = ClientInput.assertOptionalSelector("runLoop", "model", args.model);
|
|
266
|
+
const childAlias = ClientInput.assertOptionalChildAlias("runLoop", args.childAlias);
|
|
267
|
+
const childModel = ClientInput.assertOptionalSelector("runLoop", "childModel", args.childModel);
|
|
268
|
+
if (childAlias === null && childModel !== undefined) {
|
|
269
|
+
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 });
|
|
270
|
+
}
|
|
246
271
|
const flags = ClientInput.normalizeLoopFlags("runLoop", args.flags);
|
|
247
272
|
// {§methods-loop-run-model} — a client sends alias/model on every loop, so a
|
|
248
273
|
// switch takes effect turn-to-turn. `model` (client-resolved <provider>/<model>) wins
|
|
@@ -256,6 +281,14 @@ export default class Daemon {
|
|
|
256
281
|
retryable: false,
|
|
257
282
|
}));
|
|
258
283
|
}
|
|
284
|
+
// {§methods-loop-run-child-provider}
|
|
285
|
+
const configuredChildAlias = childAlias === undefined && childModel === undefined
|
|
286
|
+
? process.env.PLURNK_MODEL_CHILD
|
|
287
|
+
: childAlias;
|
|
288
|
+
const childSelection = configuredChildAlias === null
|
|
289
|
+
|| (configuredChildAlias === undefined && childModel === undefined)
|
|
290
|
+
? null
|
|
291
|
+
: await this.#resolveLoopProvider(configuredChildAlias, childModel);
|
|
259
292
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
260
293
|
// {§machine-processes} — the model NEVER runs in a client-origin worker (its packets would carry
|
|
261
294
|
// client-action rows). The module resolves the model worker via ensureModelWorker and passes it (or a
|
|
@@ -296,6 +329,7 @@ export default class Daemon {
|
|
|
296
329
|
...(openPaths !== undefined ? { openPaths } : {}),
|
|
297
330
|
turnCeiling,
|
|
298
331
|
providerSpec: selection,
|
|
332
|
+
childProviderSpec: childSelection,
|
|
299
333
|
systemPrompt,
|
|
300
334
|
});
|
|
301
335
|
return { status: 100, action, loopId, ...(turnSeq !== undefined ? { turnSeq } : {}) };
|
|
@@ -332,28 +366,47 @@ export default class Daemon {
|
|
|
332
366
|
}
|
|
333
367
|
return spec;
|
|
334
368
|
}
|
|
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`);
|
|
369
|
+
#parseProviderSpec(loopId, field, encoded) {
|
|
339
370
|
let parsed;
|
|
340
371
|
try {
|
|
341
|
-
parsed = JSON.parse(
|
|
372
|
+
parsed = JSON.parse(encoded);
|
|
342
373
|
}
|
|
343
374
|
catch {
|
|
344
|
-
throw new Error(`loop ${loopId}: persisted
|
|
375
|
+
throw new Error(`loop ${loopId}: persisted ${field} is malformed`);
|
|
345
376
|
}
|
|
346
|
-
if (parsed === null
|
|
347
|
-
|
|
377
|
+
if (parsed === null)
|
|
378
|
+
return null;
|
|
379
|
+
if (typeof parsed.alias !== "string" || parsed.alias.length === 0
|
|
348
380
|
|| typeof parsed.provider !== "string" || parsed.provider.length === 0
|
|
349
381
|
|| typeof parsed.model !== "string" || parsed.model.length === 0
|
|
350
382
|
|| (parsed.baseUrl !== undefined && typeof parsed.baseUrl !== "string")) {
|
|
351
|
-
throw new Error(`loop ${loopId}: persisted
|
|
383
|
+
throw new Error(`loop ${loopId}: persisted ${field} is invalid`);
|
|
352
384
|
}
|
|
353
385
|
return parsed;
|
|
354
386
|
}
|
|
355
|
-
async #
|
|
356
|
-
|
|
387
|
+
async #providerPolicyForLoop(loopId) {
|
|
388
|
+
const row = await this.#db.drain_loop_provider_spec.get({ loop_id: loopId });
|
|
389
|
+
if (row === undefined)
|
|
390
|
+
throw new Error(`loop ${loopId}: provider selection row is missing`);
|
|
391
|
+
const providerSpec = this.#parseProviderSpec(loopId, "provider_spec", row.provider_spec);
|
|
392
|
+
if (providerSpec === null) {
|
|
393
|
+
throw new Error(`loop ${loopId}: persisted provider selection is missing or invalid — refusing boot-default substitution`);
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
providerSpec,
|
|
397
|
+
childProviderSpec: this.#parseProviderSpec(loopId, "child_provider_spec", row.child_provider_spec),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
async #providerSpecForLoop(loopId) {
|
|
401
|
+
return (await this.#providerPolicyForLoop(loopId)).providerSpec;
|
|
402
|
+
}
|
|
403
|
+
async #providersForLoop(loopId) {
|
|
404
|
+
const policy = await this.#providerPolicyForLoop(loopId);
|
|
405
|
+
const provider = await ProviderInstantiate.instantiateProvider(policy.providerSpec);
|
|
406
|
+
const childProvider = policy.childProviderSpec === null
|
|
407
|
+
? provider
|
|
408
|
+
: await ProviderInstantiate.instantiateProvider(policy.childProviderSpec);
|
|
409
|
+
return { provider, childProvider };
|
|
357
410
|
}
|
|
358
411
|
async #assertLoopProvider(loopId, requested) {
|
|
359
412
|
const selected = await this.#providerSpecForLoop(loopId);
|
|
@@ -370,6 +423,19 @@ export default class Daemon {
|
|
|
370
423
|
});
|
|
371
424
|
}
|
|
372
425
|
}
|
|
426
|
+
async #assertLoopChildProvider(loopId, requested) {
|
|
427
|
+
const selected = (await this.#providerPolicyForLoop(loopId)).childProviderSpec;
|
|
428
|
+
if (JSON.stringify(selected) !== JSON.stringify(requested)) {
|
|
429
|
+
throw daemonFailure("daemon:provider", "loop-child-provider-conflict", 409, `Loop ${loopId} already has a different child provider policy.`, {
|
|
430
|
+
loopId,
|
|
431
|
+
selectedChildAlias: selected?.alias ?? null,
|
|
432
|
+
requestedChildAlias: requested?.alias ?? null,
|
|
433
|
+
stage: "loop-injection",
|
|
434
|
+
recovery: "Cancel or conclude the loop before changing its child provider policy.",
|
|
435
|
+
retryable: false,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
373
439
|
async #assertLoopMaxTurns(loopId, requested) {
|
|
374
440
|
if (requested === undefined)
|
|
375
441
|
return;
|
|
@@ -798,14 +864,12 @@ export default class Daemon {
|
|
|
798
864
|
state: c.state,
|
|
799
865
|
};
|
|
800
866
|
}
|
|
801
|
-
const tagRows = await this.#db.crud_read_tags.all({ entry_id: row.id });
|
|
802
867
|
return entryReadResult({
|
|
803
868
|
status: 200,
|
|
804
869
|
entry: {
|
|
805
870
|
entryId: row.id,
|
|
806
871
|
target: location.target,
|
|
807
872
|
channels,
|
|
808
|
-
tags: tagRows.map((tag) => tag.tag),
|
|
809
873
|
},
|
|
810
874
|
});
|
|
811
875
|
}
|
|
@@ -863,12 +927,13 @@ export default class Daemon {
|
|
|
863
927
|
if (typeof namespaceOwner !== "string" || namespaceOwner.trim().length === 0) {
|
|
864
928
|
throw new Error("registerRuntime: namespaceOwner must be a non-empty string");
|
|
865
929
|
}
|
|
866
|
-
|
|
930
|
+
const runtime = RuntimeDeclaration.assert(decl, namespaceOwner);
|
|
931
|
+
this.#engine.registerRuntime(runtime.name, {
|
|
867
932
|
executor,
|
|
868
933
|
namespaceOwner: { kind: "module", name: namespaceOwner },
|
|
869
|
-
glyph:
|
|
870
|
-
|
|
871
|
-
documentation:
|
|
934
|
+
glyph: runtime.glyph ?? "",
|
|
935
|
+
invocation: runtime.invocation,
|
|
936
|
+
documentation: runtime.documentation ?? "",
|
|
872
937
|
available: availability.available,
|
|
873
938
|
detail: availability.detail,
|
|
874
939
|
}, scheme);
|
|
@@ -960,6 +1025,8 @@ export default class Daemon {
|
|
|
960
1025
|
}
|
|
961
1026
|
async #recoverLifecycle() {
|
|
962
1027
|
await this.#db.recovery_fail_active_loops.run({});
|
|
1028
|
+
await this.#db.recovery_settle_open_provider_requests.run({});
|
|
1029
|
+
await this.#db.recovery_fail_open_model_calls.run({});
|
|
963
1030
|
await this.#db.recovery_fail_ownerless_proposals.run({});
|
|
964
1031
|
await this.#db.recovery_error_orphan_subscription_channels.run({});
|
|
965
1032
|
await this.#db.recovery_fail_orphan_subscriptions.run({});
|
|
@@ -1009,7 +1076,9 @@ export default class Daemon {
|
|
|
1009
1076
|
// Settle the stopped world FIRST: a drain paused at a pending proposal awaits a resolution
|
|
1010
1077
|
// that will never arrive once clients are gone — allSettled(drains) below would deadlock
|
|
1011
1078
|
// the stop forever (a daemon with a pending HITL proposal could not shut down).
|
|
1079
|
+
const derivationAbort = new DOMException("daemon stopping", "AbortError");
|
|
1012
1080
|
this.#engine.cancelAllProposals("daemon_stopping");
|
|
1081
|
+
this.#engine.cancelDerivations(derivationAbort);
|
|
1013
1082
|
this.#branchBatches.beginStop();
|
|
1014
1083
|
for (const scope of this.#workerAborts.values()) {
|
|
1015
1084
|
if (!scope.signal.aborted)
|
|
@@ -1028,10 +1097,11 @@ export default class Daemon {
|
|
|
1028
1097
|
await this.#branchBatches.idle();
|
|
1029
1098
|
const drainPromises = [...this.#activeDrains.values()].map((d) => d.promise);
|
|
1030
1099
|
await Promise.allSettled(drainPromises);
|
|
1100
|
+
await Promise.allSettled([...this.#drainExitTasks]);
|
|
1031
1101
|
const closeResults = await moduleClose;
|
|
1032
1102
|
const [streamingResult] = await Promise.allSettled([this.#drainStreamingSchemes()]);
|
|
1033
1103
|
const [derivationResult] = await Promise.allSettled([
|
|
1034
|
-
this.#engine.drainDerivations(), // active workspace warms
|
|
1104
|
+
this.#engine.drainDerivations(derivationAbort), // active workspace warms settle before the db closes upstream
|
|
1035
1105
|
]);
|
|
1036
1106
|
const mimetypeResults = this.#ownsMimetypes
|
|
1037
1107
|
? await Promise.allSettled([this.#mimetypes.dispose()])
|
|
@@ -1108,6 +1178,8 @@ export default class Daemon {
|
|
|
1108
1178
|
if (active !== undefined) {
|
|
1109
1179
|
await this.#assertFoldPosture(workerId, args.flags, active.id); // compare with the exact durable loop
|
|
1110
1180
|
await this.#assertLoopProvider(active.id, args.providerSpec);
|
|
1181
|
+
if (args.childProviderSpec !== undefined)
|
|
1182
|
+
await this.#assertLoopChildProvider(active.id, args.childProviderSpec);
|
|
1111
1183
|
await this.#assertLoopMaxTurns(active.id, args.turnCeiling?.source === "explicit" ? args.turnCeiling.effective : undefined);
|
|
1112
1184
|
}
|
|
1113
1185
|
const result = await this.#engine.inject(workerId, prompt, args.openPaths ?? []);
|
|
@@ -1125,6 +1197,8 @@ export default class Daemon {
|
|
|
1125
1197
|
if (slept !== undefined) {
|
|
1126
1198
|
await this.#assertFoldPosture(workerId, args.flags, slept.id); // resume drops nothing silently
|
|
1127
1199
|
await this.#assertLoopProvider(slept.id, args.providerSpec);
|
|
1200
|
+
if (args.childProviderSpec !== undefined)
|
|
1201
|
+
await this.#assertLoopChildProvider(slept.id, args.childProviderSpec);
|
|
1128
1202
|
await this.#assertLoopMaxTurns(slept.id, args.turnCeiling?.source === "explicit" ? args.turnCeiling.effective : undefined);
|
|
1129
1203
|
const injected = await this.#engine.inject(workerId, prompt, args.openPaths ?? []);
|
|
1130
1204
|
await this.#lifecycle.wake(slept.id);
|
|
@@ -1138,6 +1212,7 @@ export default class Daemon {
|
|
|
1138
1212
|
workerId,
|
|
1139
1213
|
prompt,
|
|
1140
1214
|
providerSpec: args.providerSpec,
|
|
1215
|
+
childProviderSpec: args.childProviderSpec ?? null,
|
|
1141
1216
|
maxTurns: args.turnCeiling?.effective,
|
|
1142
1217
|
flags: args.flags,
|
|
1143
1218
|
openPaths: args.openPaths,
|
|
@@ -1168,6 +1243,7 @@ export default class Daemon {
|
|
|
1168
1243
|
sequence: seqRow.next,
|
|
1169
1244
|
prompt: args.prompt,
|
|
1170
1245
|
provider_spec: JSON.stringify(args.providerSpec),
|
|
1246
|
+
child_provider_spec: JSON.stringify(args.childProviderSpec),
|
|
1171
1247
|
max_turns: args.maxTurns ?? Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "50"),
|
|
1172
1248
|
});
|
|
1173
1249
|
if (loopRow === undefined)
|
|
@@ -1245,7 +1321,7 @@ export default class Daemon {
|
|
|
1245
1321
|
// {§methods-loop-run-model} — provider identity belongs to the claimed loop, not the
|
|
1246
1322
|
// drain that happened to claim it. A drain can consume multiple
|
|
1247
1323
|
// queued loops; resolve each durable selection at this boundary.
|
|
1248
|
-
const provider = await this.#
|
|
1324
|
+
const { provider, childProvider } = await this.#providersForLoop(loopRow.id);
|
|
1249
1325
|
const onDispatch = (logEntryId) => {
|
|
1250
1326
|
// {§methods-event-subscribe} — a log-broadcast failure must never crash the drain.
|
|
1251
1327
|
void (async () => {
|
|
@@ -1256,7 +1332,7 @@ export default class Daemon {
|
|
|
1256
1332
|
const result = await observed(// {§observability-boundary}
|
|
1257
1333
|
"loop.run", { workspaceId, workerId, "loop.id": loopRow.id }, async (span) => {
|
|
1258
1334
|
const loopResult = await this.#engine.runLoop({
|
|
1259
|
-
provider, workspaceId, workerId, loopId: loopRow.id, maxTurns: loopRow.max_turns,
|
|
1335
|
+
provider, childProvider, workspaceId, workerId, loopId: loopRow.id, maxTurns: loopRow.max_turns,
|
|
1260
1336
|
messages: [
|
|
1261
1337
|
{ role: "system", content: systemPrompt },
|
|
1262
1338
|
{ role: "user", content: loopRow.prompt },
|
|
@@ -1270,12 +1346,12 @@ export default class Daemon {
|
|
|
1270
1346
|
return loopResult;
|
|
1271
1347
|
});
|
|
1272
1348
|
if (result.result.status === 202) {
|
|
1273
|
-
// The loop slept via SEND
|
|
1349
|
+
// The loop slept via SEND signal 202 — suspended, not terminated. Leave it at 202
|
|
1274
1350
|
// (resumable); no loop/terminated, no orphan-reconcile. A stream conclusion
|
|
1275
1351
|
// (#handleWakeWorker) re-queues it; and if it holds a polled stream, a poll timer
|
|
1276
1352
|
// wakes it every P to inspect ({§exec-poll}). {§worker-lifecycle-wake-liveness}.
|
|
1277
1353
|
void this.#schedulePollWake(workspaceId, workerId, systemPrompt).catch((err) => console.error("poll-wake scheduling failed:", err instanceof Error ? err.message : String(err)));
|
|
1278
|
-
// {§send-premature-terminate}/SEND
|
|
1354
|
+
// {§send-premature-terminate}/scoped SEND signal 202 — the park deadline:
|
|
1279
1355
|
// dispatcher recorded the marker's seconds; a bounded park is woken at T
|
|
1280
1356
|
// regardless of arrivals, so a park always has a next turn. -1 (indefinite:
|
|
1281
1357
|
// the butler, a [300] ask) schedules nothing — irc/inject/conclusions wake it.
|
|
@@ -1353,12 +1429,13 @@ export default class Daemon {
|
|
|
1353
1429
|
// {§methods-loop-cancel} — loop.cancel / shutdown aborted the live drain. A cancellation
|
|
1354
1430
|
// is the loop's TERMINAL state (499), delivered via loop/terminated (runLoop no
|
|
1355
1431
|
// longer blocks to return it). A genuine error rejects firstLoopPromise.
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
:
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1432
|
+
let usage = {
|
|
1433
|
+
accounting: aggregateProviderAccounting([]),
|
|
1434
|
+
contextTokens: null,
|
|
1435
|
+
promptBudget: null,
|
|
1436
|
+
meta: {},
|
|
1437
|
+
};
|
|
1438
|
+
let attributions = [];
|
|
1362
1439
|
const message = ErrorDetail.preview(controller.signal.reason ?? "user_cancelled")
|
|
1363
1440
|
|| "no reason was supplied";
|
|
1364
1441
|
if (currentLoopId !== null) {
|
|
@@ -1369,6 +1446,10 @@ export default class Daemon {
|
|
|
1369
1446
|
stage: "loop",
|
|
1370
1447
|
retryable: false,
|
|
1371
1448
|
}), { terminatedBy: "cancel" });
|
|
1449
|
+
[usage, attributions] = await Promise.all([
|
|
1450
|
+
this.#engine.loopUsage(currentLoopId),
|
|
1451
|
+
this.#engine.loopAttributions(currentLoopId),
|
|
1452
|
+
]);
|
|
1372
1453
|
if (cancelled !== null) {
|
|
1373
1454
|
this.#broadcast({ workspaceId }, "loop/terminated", {
|
|
1374
1455
|
workerId,
|
|
@@ -1456,8 +1537,12 @@ export default class Daemon {
|
|
|
1456
1537
|
// Topology join ({§worker-loop-lifecycle}): when this drain exits having CONCLUDED the worker, wake its parent
|
|
1457
1538
|
// if parked. Runs after the drain fully tears down (settled promise) so the quiescence check sees
|
|
1458
1539
|
// final state; speculative (#onDrainExit no-ops unless the worker concluded AND the parent is parked).
|
|
1459
|
-
|
|
1540
|
+
const drainExitTask = drainPromise.then(() => this.#onDrainExit(workspaceId, workerId, systemPrompt), () => this.#onDrainExit(workspaceId, workerId, systemPrompt));
|
|
1541
|
+
this.#drainExitTasks.add(drainExitTask);
|
|
1542
|
+
void drainExitTask.catch((err) => {
|
|
1460
1543
|
console.error(`parent wake after worker ${workerId} settlement failed:`, err);
|
|
1544
|
+
}).finally(() => {
|
|
1545
|
+
this.#drainExitTasks.delete(drainExitTask);
|
|
1461
1546
|
});
|
|
1462
1547
|
// Swallow unhandled rejections (drain aborts with no awaiter); the
|
|
1463
1548
|
// error already surfaced via firstLoopPromise or was logged inside.
|
|
@@ -1515,6 +1600,7 @@ export default class Daemon {
|
|
|
1515
1600
|
prompt: first.body,
|
|
1516
1601
|
flags: first.flags,
|
|
1517
1602
|
provider_spec: first.provider_spec,
|
|
1603
|
+
child_provider_spec: first.child_provider_spec,
|
|
1518
1604
|
max_turns: first.max_turns,
|
|
1519
1605
|
open_paths: first.open_paths ?? "[]",
|
|
1520
1606
|
orphan_source_loop_id: endedLoopId,
|
|
@@ -1632,23 +1718,24 @@ export default class Daemon {
|
|
|
1632
1718
|
}
|
|
1633
1719
|
/**
|
|
1634
1720
|
* 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.
|
|
1721
|
+
* subscription closes. A parked loop resumes in place; an active loop
|
|
1722
|
+
* observes the channel transition at its next turn boundary. No synthetic
|
|
1723
|
+
* prompt or replacement loop is created.
|
|
1639
1724
|
*
|
|
1640
1725
|
* Skipped on result.status=499 (aborted): the model already knows about
|
|
1641
|
-
* its own SEND
|
|
1642
|
-
*
|
|
1726
|
+
* its own SEND signal 499, and a forcefully-cancelled loop's spawn-abort must
|
|
1727
|
+
* not resurrect the worker.
|
|
1643
1728
|
*/
|
|
1644
1729
|
async #handleWakeWorker(payload) {
|
|
1730
|
+
const { entryOwnerId, ...wake } = payload;
|
|
1731
|
+
const conclusion = { ...wake, workerId: entryOwnerId };
|
|
1645
1732
|
// {§search-gate} — settle the dedup registration: promote on a 200 conclusion, drop on
|
|
1646
1733
|
// failure (a dead search must never serve as a duplicate). No-op for non-search streams.
|
|
1647
1734
|
this.#engine.searchGate.settle(payload.target.replace(/^[a-z+.-]+:\/\//, "/").replace(/^\/+/, "/"), payload.result.status);
|
|
1648
1735
|
// Aborted streams don't wake — the abort was deliberate.
|
|
1649
1736
|
if (payload.result.status === 499) {
|
|
1650
1737
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1651
|
-
...
|
|
1738
|
+
...conclusion, wakeAction: "skipped-aborted",
|
|
1652
1739
|
});
|
|
1653
1740
|
return;
|
|
1654
1741
|
}
|
|
@@ -1662,13 +1749,13 @@ export default class Daemon {
|
|
|
1662
1749
|
const scope = this.#workerAborts.get(payload.workerId);
|
|
1663
1750
|
if (scope?.signal.aborted === true && !this.#activeDrains.has(payload.workerId)) {
|
|
1664
1751
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1665
|
-
...
|
|
1752
|
+
...conclusion, wakeAction: "skipped-cancelled",
|
|
1666
1753
|
});
|
|
1667
1754
|
return;
|
|
1668
1755
|
}
|
|
1669
1756
|
try {
|
|
1670
1757
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
1671
|
-
// A slept (202) loop means the worker parked via SEND
|
|
1758
|
+
// A slept (202) loop means the worker parked via SEND signal 202 → resume it in place: re-queue
|
|
1672
1759
|
// it (202→100) so the drain re-claims and CONTINUES it (seq>1 → no re-foist). Checked
|
|
1673
1760
|
// FIRST: the slept status is the worker's true disposition regardless of a draining
|
|
1674
1761
|
// sibling mid-teardown (the #ensureDrain lock serializes the re-claim). No fresh loop,
|
|
@@ -1676,16 +1763,14 @@ export default class Daemon {
|
|
|
1676
1763
|
// the manifest. {§worker-lifecycle-wake-liveness}.
|
|
1677
1764
|
const slept = await this.#db.drain_find_slept_loop.get({ worker_id: payload.workerId });
|
|
1678
1765
|
if (slept !== undefined) {
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1766
|
+
// {§worker-optimistic-settlement} — publish this conclusion now,
|
|
1767
|
+
// then let the worker-local gate coalesce only the provider
|
|
1768
|
+
// dispatch. Concurrent stream/child callbacks join that one gate.
|
|
1769
|
+
void this.#settleCompletionWake(payload.workspaceId, payload.workerId, systemPrompt, false).catch((err) => {
|
|
1770
|
+
console.error("completion wake settlement failed:", err instanceof Error ? err.message : String(err));
|
|
1683
1771
|
});
|
|
1684
1772
|
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));
|
|
1773
|
+
...conclusion, wakeAction: "resumed-loop", wakeLoopId: slept.id,
|
|
1689
1774
|
});
|
|
1690
1775
|
return;
|
|
1691
1776
|
}
|
|
@@ -1695,14 +1780,14 @@ export default class Daemon {
|
|
|
1695
1780
|
// synthesis (which clobbered the model's actual goal) is retired; just tell the client.
|
|
1696
1781
|
if (this.#activeDrains.has(payload.workerId)) {
|
|
1697
1782
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1698
|
-
...
|
|
1783
|
+
...conclusion, wakeAction: "no-op-active-loop",
|
|
1699
1784
|
});
|
|
1700
1785
|
return;
|
|
1701
1786
|
}
|
|
1702
|
-
// No slept loop, no active drain — nothing to resume (e.g. a SEND
|
|
1787
|
+
// No slept loop, no active drain — nothing to resume (e.g. a SEND-200-done worker whose
|
|
1703
1788
|
// streams were swept). Surface the conclusion without opening a loop.
|
|
1704
1789
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1705
|
-
...
|
|
1790
|
+
...conclusion, wakeAction: "no-loop",
|
|
1706
1791
|
});
|
|
1707
1792
|
}
|
|
1708
1793
|
catch (err) {
|
|
@@ -1748,13 +1833,13 @@ export default class Daemon {
|
|
|
1748
1833
|
delayMs = execPollBackoffMs(step, base, turns);
|
|
1749
1834
|
this.#pollBackoff.set(workerId, step + 1);
|
|
1750
1835
|
}
|
|
1751
|
-
// Floored by the
|
|
1752
|
-
// faster than
|
|
1753
|
-
const
|
|
1836
|
+
// Floored by the optimistic settlement cap so a `<…,1>` cannot wake a
|
|
1837
|
+
// parked loop faster than the preceding turn's settlement scale.
|
|
1838
|
+
const optimisticWaitMs = readOptimisticSettlementMs();
|
|
1754
1839
|
const timer = setTimeout(() => {
|
|
1755
1840
|
this.#pollTimers.delete(workerId);
|
|
1756
1841
|
void this.#wakeParkedWorker(workspaceId, workerId, systemPrompt);
|
|
1757
|
-
}, Math.max(delayMs,
|
|
1842
|
+
}, Math.max(delayMs, optimisticWaitMs));
|
|
1758
1843
|
timer.unref();
|
|
1759
1844
|
this.#pollTimers.set(workerId, timer);
|
|
1760
1845
|
}
|
|
@@ -1763,6 +1848,8 @@ export default class Daemon {
|
|
|
1763
1848
|
* or a child worker finishing ({§worker-loop-lifecycle} topology join) all call this. A no-op if the worker was
|
|
1764
1849
|
* cancelled or isn't actually parked (no slept loop) — so calling it speculatively is safe. */
|
|
1765
1850
|
async #wakeParkedWorker(workspaceId, workerId, systemPrompt, oweIfActive = true) {
|
|
1851
|
+
if (!this.#started)
|
|
1852
|
+
return;
|
|
1766
1853
|
const scope = this.#workerAborts.get(workerId);
|
|
1767
1854
|
if (scope?.signal.aborted === true && !this.#activeDrains.has(workerId))
|
|
1768
1855
|
return; // cancelled — no resurrection
|
|
@@ -1772,17 +1859,100 @@ export default class Daemon {
|
|
|
1772
1859
|
// conclusion that fired this wake arrived before the 202 committed (the conclude-before-park
|
|
1773
1860
|
// race). Owe the wake: the drain honors it at park so a worker hibernation never deadlocks.
|
|
1774
1861
|
// (No active drain → already concluded/running; nothing to wake.)
|
|
1775
|
-
if (
|
|
1776
|
-
|
|
1862
|
+
if (this.#activeDrains.has(workerId)) {
|
|
1863
|
+
if (oweIfActive)
|
|
1864
|
+
this.#owedWakes.add(workerId);
|
|
1865
|
+
}
|
|
1777
1866
|
return;
|
|
1778
1867
|
}
|
|
1779
|
-
await this.#lifecycle.wake(slept.id);
|
|
1868
|
+
const woke = await this.#lifecycle.wake(slept.id);
|
|
1869
|
+
if (!woke)
|
|
1870
|
+
return;
|
|
1780
1871
|
const started = await this.#ensureDrain({
|
|
1781
1872
|
workspaceId, workerId, systemPrompt,
|
|
1782
1873
|
});
|
|
1783
1874
|
started?.drainPromise?.catch((err) => {
|
|
1784
|
-
|
|
1875
|
+
if (this.#started) {
|
|
1876
|
+
console.error("wake-parked resume drain failed:", err instanceof Error ? err.message : String(err));
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
async #workerHasLiveObligation(workerId) {
|
|
1881
|
+
const [openSubscriptions, liveChild] = await Promise.all([
|
|
1882
|
+
this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId }),
|
|
1883
|
+
this.#db.engine_worker_has_live_child.get({ worker_id: workerId }),
|
|
1884
|
+
]);
|
|
1885
|
+
return openSubscriptions.length > 0 || liveChild !== undefined;
|
|
1886
|
+
}
|
|
1887
|
+
#settleCompletionWake(workspaceId, workerId, systemPrompt, oweIfActive = true) {
|
|
1888
|
+
const existing = this.#completionWakeGates.get(workerId);
|
|
1889
|
+
if (existing !== undefined) {
|
|
1890
|
+
existing.conclusions++;
|
|
1891
|
+
existing.poke.resolve();
|
|
1892
|
+
return existing.promise;
|
|
1893
|
+
}
|
|
1894
|
+
const completed = Promise.withResolvers();
|
|
1895
|
+
const gate = {
|
|
1896
|
+
conclusions: 1,
|
|
1897
|
+
poke: Promise.withResolvers(),
|
|
1898
|
+
promise: completed.promise,
|
|
1899
|
+
};
|
|
1900
|
+
this.#completionWakeGates.set(workerId, gate);
|
|
1901
|
+
void this.#runCompletionWake(workspaceId, workerId, systemPrompt, oweIfActive, gate).then(completed.resolve, completed.reject).finally(() => {
|
|
1902
|
+
if (this.#completionWakeGates.get(workerId) === gate) {
|
|
1903
|
+
this.#completionWakeGates.delete(workerId);
|
|
1904
|
+
}
|
|
1785
1905
|
});
|
|
1906
|
+
return gate.promise;
|
|
1907
|
+
}
|
|
1908
|
+
async #runCompletionWake(workspaceId, workerId, systemPrompt, oweIfActive, gate) {
|
|
1909
|
+
const slept = await this.#db.drain_find_slept_loop.get({ worker_id: workerId });
|
|
1910
|
+
if (slept === undefined) {
|
|
1911
|
+
this.#releaseCompletionWake(workerId, gate);
|
|
1912
|
+
return this.#wakeParkedWorker(workspaceId, workerId, systemPrompt, oweIfActive);
|
|
1913
|
+
}
|
|
1914
|
+
const timeoutMs = readOptimisticSettlementMs();
|
|
1915
|
+
if (timeoutMs === 0 || !(await this.#workerHasLiveObligation(workerId))) {
|
|
1916
|
+
this.#releaseCompletionWake(workerId, gate);
|
|
1917
|
+
return this.#wakeParkedWorker(workspaceId, workerId, systemPrompt, false);
|
|
1918
|
+
}
|
|
1919
|
+
return observed("worker.wake.settlement", { "window.ms": timeoutMs }, async (span) => {
|
|
1920
|
+
const startedAt = performance.now();
|
|
1921
|
+
const signal = this.#workerAborts.get(workerId)?.signal;
|
|
1922
|
+
const deadline = delay(timeoutMs, "deadline", { signal, ref: false })
|
|
1923
|
+
.catch((cause) => {
|
|
1924
|
+
if (signal?.aborted === true)
|
|
1925
|
+
return "cancelled";
|
|
1926
|
+
throw cause;
|
|
1927
|
+
});
|
|
1928
|
+
let release = "quiescent";
|
|
1929
|
+
while (await this.#workerHasLiveObligation(workerId)) {
|
|
1930
|
+
const poke = gate.poke;
|
|
1931
|
+
const outcome = await Promise.race([
|
|
1932
|
+
poke.promise.then(() => "arrival"),
|
|
1933
|
+
deadline,
|
|
1934
|
+
]);
|
|
1935
|
+
if (outcome === "arrival") {
|
|
1936
|
+
if (gate.poke === poke)
|
|
1937
|
+
gate.poke = Promise.withResolvers();
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
release = outcome;
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
span.setAttribute("release", release);
|
|
1944
|
+
span.setAttribute("conclusions", gate.conclusions);
|
|
1945
|
+
span.setAttribute("elapsed.ms", Math.round(performance.now() - startedAt));
|
|
1946
|
+
this.#releaseCompletionWake(workerId, gate);
|
|
1947
|
+
if (release === "cancelled")
|
|
1948
|
+
return;
|
|
1949
|
+
return this.#wakeParkedWorker(workspaceId, workerId, systemPrompt, false);
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
#releaseCompletionWake(workerId, gate) {
|
|
1953
|
+
if (this.#completionWakeGates.get(workerId) === gate) {
|
|
1954
|
+
this.#completionWakeGates.delete(workerId);
|
|
1955
|
+
}
|
|
1786
1956
|
}
|
|
1787
1957
|
/** A worker's drain exited. If the worker truly CONCLUDED — no 202-blocked loop, no open stream — then
|
|
1788
1958
|
* wake its PARENT in place if the parent is blocked on the join (the structured-concurrency join — a
|
|
@@ -1800,7 +1970,7 @@ export default class Daemon {
|
|
|
1800
1970
|
const parent = await this.#db.worker_parent_id.get({ worker_id: workerId });
|
|
1801
1971
|
if (parent?.parent_worker_id == null)
|
|
1802
1972
|
return; // a root worker — nobody to wake
|
|
1803
|
-
await this.#
|
|
1973
|
+
await this.#settleCompletionWake(workspaceId, parent.parent_worker_id, systemPrompt);
|
|
1804
1974
|
}
|
|
1805
1975
|
// {§methods-event-subscribe} — subscriber failures are transport-local:
|
|
1806
1976
|
// log them at this boundary and never re-enter engine control flow.
|