@plurnk/plurnk-service 1.5.0 → 1.6.1
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 +22 -27
- package/INSTALL.md +22 -17
- package/README.md +1 -1
- package/SPEC.md +505 -362
- 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 +4 -1
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +3 -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/read-projector.d.ts +7 -1
- package/dist/content/read-projector.d.ts.map +1 -1
- package/dist/content/read-projector.js +46 -10
- package/dist/content/read-projector.js.map +1 -1
- package/dist/content/read-resolve.js +3 -3
- package/dist/content/read-resolve.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.js +1 -1
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +3 -3
- package/dist/core/CoreSchemeServices.d.ts +1 -1
- package/dist/core/CoreSchemeServices.d.ts.map +1 -1
- package/dist/core/CoreSchemeServices.js +1 -1
- package/dist/core/CoreSchemeServices.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +15 -5
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +220 -978
- 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 +18 -43
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +90 -1554
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +192 -112
- 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 +3 -0
- package/dist/core/LogBody.d.ts.map +1 -1
- package/dist/core/LogBody.js +21 -5
- 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/ModelCall.d.ts +27 -0
- package/dist/core/ModelCall.d.ts.map +1 -0
- package/dist/core/ModelCall.js +127 -0
- package/dist/core/ModelCall.js.map +1 -0
- package/dist/core/PacketBuilder.d.ts +14 -25
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +170 -244
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProblemLog.d.ts +1 -1
- package/dist/core/ProblemLog.d.ts.map +1 -1
- package/dist/core/ProblemLog.js +15 -3
- package/dist/core/ProblemLog.js.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts +2 -2
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +17 -9
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +25 -8
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/ResourceMutations.d.ts +73 -0
- package/dist/core/ResourceMutations.d.ts.map +1 -0
- package/dist/core/ResourceMutations.js +1142 -0
- package/dist/core/ResourceMutations.js.map +1 -0
- package/dist/core/SchemeRegistry.js +10 -10
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/StoredPacket.d.ts +2 -2
- package/dist/core/StoredPacket.js +7 -7
- package/dist/core/StrikeRail.d.ts +0 -1
- package/dist/core/StrikeRail.d.ts.map +1 -1
- package/dist/core/StrikeRail.js +6 -4
- package/dist/core/StrikeRail.js.map +1 -1
- package/dist/core/TurnRunner.d.ts +72 -0
- package/dist/core/TurnRunner.d.ts.map +1 -0
- package/dist/core/TurnRunner.js +1730 -0
- package/dist/core/TurnRunner.js.map +1 -0
- package/dist/core/caps/CapsResolve.js +1 -1
- package/dist/core/caps/CapsResolve.js.map +1 -1
- package/dist/core/caps/DbChannelCaps.js +4 -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 +8 -5
- package/dist/core/caps/DbEntryCaps.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/SchemeCtxImpl.d.ts +5 -2
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +6 -4
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/content-hash.js +2 -2
- package/dist/core/content-hash.js.map +1 -1
- package/dist/core/content-weight.d.ts +2 -0
- package/dist/core/content-weight.d.ts.map +1 -0
- package/dist/core/content-weight.js +4 -0
- package/dist/core/content-weight.js.map +1 -0
- package/dist/core/content_weight.d.ts +4 -0
- package/dist/core/content_weight.d.ts.map +1 -0
- package/dist/core/content_weight.js +6 -0
- package/dist/core/content_weight.js.map +1 -0
- 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 +38 -17
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +1 -2
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.js +1 -1
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/packet-wire.d.ts +6 -3
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +257 -99
- 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/scheme-types.d.ts +1 -1
- package/dist/core/scheme-types.d.ts.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/turn-scheduler.js +1 -1
- package/dist/core/turn-scheduler.js.map +1 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +221 -110
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/digest.sql +40 -18
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +136 -110
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +1 -1
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/File.d.ts +5 -4
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +25 -23
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts +11 -0
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +172 -107
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +48 -32
- package/dist/schemes/Prompt.d.ts +3 -3
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +4 -1
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/Skill.d.ts +4 -4
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +3 -0
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts +4 -4
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +11 -7
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts +0 -1
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +19 -13
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +4 -16
- package/dist/schemes/_entry-find.d.ts +17 -16
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +58 -52
- 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 +32 -28
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts +5 -4
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +89 -49
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +30 -4
- 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 +10 -2
- 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/Daemon.d.ts +7 -42
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +139 -900
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DrainSupervisor.d.ts +117 -0
- package/dist/server/DrainSupervisor.d.ts.map +1 -0
- package/dist/server/DrainSupervisor.js +802 -0
- package/dist/server/DrainSupervisor.js.map +1 -0
- package/dist/server/drain.sql +2 -2
- package/dist/server/envelope.d.ts +0 -2
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +2 -2
- package/dist/server/lifecycle-recovery.sql +28 -15
- package/dist/server/logEntry.d.ts +2 -1
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +14 -2
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/logEntry.sql +5 -1
- package/dist/server/seam-entry-read.sql +2 -2
- package/dist/service.js +1 -1
- package/dist/service.js.map +1 -1
- package/migrations/001_schema.sql +622 -216
- package/package.json +39 -34
- package/dist/core/BudgetOverflow.d.ts +0 -20
- package/dist/core/BudgetOverflow.d.ts.map +0 -1
- package/dist/core/BudgetOverflow.js +0 -49
- 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 -46
- package/dist/core/caps/DbTagCaps.js.map +0 -1
- package/dist/core/ruler_count.d.ts +0 -4
- package/dist/core/ruler_count.d.ts.map +0 -1
- package/dist/core/ruler_count.js +0 -7
- package/dist/core/ruler_count.js.map +0 -1
- package/dist/core/token-ruler.d.ts +0 -2
- package/dist/core/token-ruler.d.ts.map +0 -1
- package/dist/core/token-ruler.js +0 -5
- package/dist/core/token-ruler.js.map +0 -1
package/dist/server/Daemon.js
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
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
|
|
6
|
-
import { execPollBackoffMs } from "./exec-poll-backoff.js";
|
|
7
|
-
import ChannelWrite from "../core/ChannelWrite.js";
|
|
8
|
-
import { Paths } from "../index.js";
|
|
5
|
+
import Paths from "../Paths.js";
|
|
9
6
|
import Engine from "../core/Engine.js";
|
|
10
7
|
import ExecutorRegistry from "../core/ExecutorRegistry.js";
|
|
11
8
|
import SchemeRegistry from "../core/SchemeRegistry.js";
|
|
12
9
|
import { Mimetypes } from "@plurnk/plurnk-mimetypes";
|
|
10
|
+
import { RuntimeDeclaration } from "@plurnk/plurnk-execs";
|
|
11
|
+
import DrainSupervisor from "./DrainSupervisor.js";
|
|
13
12
|
import { parsePath, Validator, } from "@plurnk/plurnk-contracts";
|
|
14
13
|
import LogEntry from "./logEntry.js";
|
|
15
14
|
import Envelope from "./envelope.js";
|
|
@@ -21,19 +20,15 @@ import Fork from "../core/fork.js";
|
|
|
21
20
|
import WorkerName from "../core/WorkerName.js";
|
|
22
21
|
import LoopLifecycle from "../core/LoopLifecycle.js";
|
|
23
22
|
import { promptLoopPrefix } from "../core/plurnk-uri.js";
|
|
24
|
-
import {
|
|
23
|
+
import { contentWeight } from "../core/content-weight.js";
|
|
25
24
|
import { parseAliasesFromEnv, resolveActiveAlias } from "@plurnk/plurnk-providers";
|
|
26
25
|
import ProviderInstantiate from "../core/ProviderInstantiate.js";
|
|
27
26
|
import { resolveLoopAlias } from "./loop-model.js";
|
|
28
|
-
import { DEFAULT_LOOP_FLAGS } from "../core/scheme-types.js";
|
|
29
27
|
import LoopFlagsReader from "../core/LoopFlagsReader.js";
|
|
30
28
|
import Results, { OperationFailureError } from "../core/results.js";
|
|
31
29
|
import WorkspaceGate from "../core/WorkspaceGate.js";
|
|
32
30
|
import BranchBatches from "./BranchBatches.js";
|
|
33
|
-
import ErrorDetail from "../core/ErrorDetail.js";
|
|
34
31
|
import { observed, observedSync } from "../observe/spans.js";
|
|
35
|
-
import { LOOP_TERMINALS, recordCounter } from "../observe/metrics.js";
|
|
36
|
-
import { readOptimisticSettlementMs } from "../core/optimistic-settlement.js";
|
|
37
32
|
const clientActionFailure = (error) => {
|
|
38
33
|
if (error instanceof OperationFailureError)
|
|
39
34
|
return error.result;
|
|
@@ -51,6 +46,7 @@ export default class Daemon {
|
|
|
51
46
|
#workspaceGate;
|
|
52
47
|
#branchBatches;
|
|
53
48
|
#lifecycle;
|
|
49
|
+
#drains;
|
|
54
50
|
#schemes;
|
|
55
51
|
#mimetypes;
|
|
56
52
|
#ownsMimetypes;
|
|
@@ -66,40 +62,6 @@ export default class Daemon {
|
|
|
66
62
|
// module (plurnk-agui) subscribes and fans out to its OWN clients; core emits, never owns
|
|
67
63
|
// client transport or connection state.
|
|
68
64
|
#eventSubscribers = new Set();
|
|
69
|
-
// Worker-level drain registry. At most one drain per worker. The stored object
|
|
70
|
-
// is the drain's identity handle: start/exit compare it by reference so a
|
|
71
|
-
// drain exiting never clobbers a successor that raced in, and a loop
|
|
72
|
-
// enqueued during teardown is never stranded. A drain is a pure queue
|
|
73
|
-
// consumer (claim → run → exit on empty queue); streams live independently
|
|
74
|
-
// (subscriptions + Exec.idle), and a concluding stream routes through
|
|
75
|
-
// inject() like any other loop source.
|
|
76
|
-
#activeDrains = new Map();
|
|
77
|
-
#drainExitTasks = new Set();
|
|
78
|
-
// Per-worker cancellation scope. Loops AND the streams they spawn (execs)
|
|
79
|
-
// share this signal, so loop.cancel / shutdown abort it once and every
|
|
80
|
-
// in-flight subscription tears down — even a spawn that registers AFTER the
|
|
81
|
-
// cancel self-aborts against the already-aborted signal (no race). Outlives
|
|
82
|
-
// any single (ephemeral) drain; replaced with a fresh controller once
|
|
83
|
-
// aborted so a later runLoop request isn't born cancelled.
|
|
84
|
-
#workerAborts = new Map();
|
|
85
|
-
// grammar 0.74.20 EXEC `<T,P>` — per-worker hibernation poll-wake timer. When a loop parks at
|
|
86
|
-
// a park with a polled stream, a timer fires every P seconds to resume it ({§exec-poll}). One
|
|
87
|
-
// per worker (the tightest cadence); cleared/replaced on each park and on cancel.
|
|
88
|
-
#parkTimers = new Map();
|
|
89
|
-
#pollTimers = new Map();
|
|
90
|
-
#pollBackoff = new Map(); // {§exec-poll} — backoff step per worker
|
|
91
|
-
// Per-worker drain-transition lock — see #withDrainLock (R4 / {§worker-lifecycle-single-drain}).
|
|
92
|
-
#drainLocks = new Map();
|
|
93
|
-
// {§worker-lifecycle-child-wake} — workers owed a wake: a child/stream conclusion fired while the worker was
|
|
94
|
-
// mid-turn (not yet slept), so #wakeParkedWorker could not resume it. A child-worker conclusion is a
|
|
95
|
-
// BOUNDED, lossless wake (a worker always concludes), so a hibernation awaiting one MUST return —
|
|
96
|
-
// never deadlock. The drain honors the owed wake at the worker's next park, closing the conclude-
|
|
97
|
-
// before-park race. (Only a live exec stream, unbounded absent a timeout, may hold a park open.)
|
|
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();
|
|
103
65
|
constructor({ db, schemes, mimetypes, provider, nodeModulesPath, }) {
|
|
104
66
|
this.#db = db;
|
|
105
67
|
this.#lifecycle = new LoopLifecycle(db);
|
|
@@ -147,7 +109,7 @@ export default class Daemon {
|
|
|
147
109
|
}))?.id;
|
|
148
110
|
if (workerId === undefined)
|
|
149
111
|
throw new Error("Branch worker insert returned no row");
|
|
150
|
-
const loopId = await this.#enqueueFreshLoop({
|
|
112
|
+
const loopId = await this.#drains.enqueueFreshLoop({
|
|
151
113
|
workerId,
|
|
152
114
|
prompt,
|
|
153
115
|
providerSpec,
|
|
@@ -158,7 +120,7 @@ export default class Daemon {
|
|
|
158
120
|
},
|
|
159
121
|
startChild: async (workspaceId, workerId, loopId) => {
|
|
160
122
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
161
|
-
const started = await this.#ensureDrain({ workspaceId, workerId, systemPrompt });
|
|
123
|
+
const started = await this.#drains.ensureDrain({ workspaceId, workerId, systemPrompt });
|
|
162
124
|
if (started === null)
|
|
163
125
|
throw new Error(`Branch worker ${workerId} already has a live drain`);
|
|
164
126
|
const result = await started.firstLoopPromise;
|
|
@@ -169,7 +131,7 @@ export default class Daemon {
|
|
|
169
131
|
},
|
|
170
132
|
wakeParent: async (workspaceId, workerId) => {
|
|
171
133
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
172
|
-
await this.#settleCompletionWake(workspaceId, workerId, systemPrompt, false);
|
|
134
|
+
await this.#drains.settleCompletionWake(workspaceId, workerId, systemPrompt, false);
|
|
173
135
|
},
|
|
174
136
|
notify: (workspaceId, payload) => {
|
|
175
137
|
this.#broadcast({ workspaceId }, "workspace/branch-batch", payload);
|
|
@@ -177,13 +139,12 @@ export default class Daemon {
|
|
|
177
139
|
});
|
|
178
140
|
this.#engine = new Engine({
|
|
179
141
|
db, schemes: this.#schemes, mimetypes: this.#mimetypes,
|
|
180
|
-
// {§tokenomics-agnostic-ruler} —
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
tokenize: rulerCount,
|
|
142
|
+
// {§tokenomics-agnostic-ruler} — stored and catalog curation weights
|
|
143
|
+
// are workspace-wide across concurrent models, so they remain
|
|
144
|
+
// model-independent. Request-shaped token facts stay provider-owned.
|
|
145
|
+
weigh: contentWeight,
|
|
185
146
|
streamEventNotify: (workspaceId, event) => this.notifyStreamEvent(workspaceId, event),
|
|
186
|
-
wakeWorkerNotify: (payload) =>
|
|
147
|
+
wakeWorkerNotify: (payload) => this.#drains.notifyWakeWorker(payload),
|
|
187
148
|
// worker:// loop-start primitive — spawn/fork/irc deliver through
|
|
188
149
|
// Daemon.inject (active sister → fold; idle → enqueue + drain). The
|
|
189
150
|
// daemon owns provider + the law-file system prompt; the worker scheme
|
|
@@ -216,10 +177,58 @@ export default class Daemon {
|
|
|
216
177
|
workspaceTurnCompleted: async ({ turnId }) => this.#branchBatches.sealTurn(turnId),
|
|
217
178
|
// worker:// KILL (terminate) — cancel the addressed worker subtree and
|
|
218
179
|
// tear down its held streams before the operation completes.
|
|
219
|
-
cancelWorker: async (workerId, reason) => this.#cancelWorkerTree(workerId, reason),
|
|
220
|
-
cancelDescendants: async (workerId, reason) => this.#
|
|
180
|
+
cancelWorker: async (workerId, reason) => this.#drains.cancelWorkerTree(workerId, reason),
|
|
181
|
+
cancelDescendants: async (workerId, reason) => this.#drains.cancelDescendants(workerId, reason),
|
|
221
182
|
noticeNotify: (workspaceId, payload) => this.notifyNotice(workspaceId, payload),
|
|
222
183
|
});
|
|
184
|
+
this.#drains = new DrainSupervisor({
|
|
185
|
+
db,
|
|
186
|
+
lifecycle: this.#lifecycle,
|
|
187
|
+
injectPrompt: (workerId, prompt, openPaths) => this.#engine.inject(workerId, prompt, openPaths),
|
|
188
|
+
assertInjectionCompatibility: async ({ workerId, loopId, providerSpec, childProviderSpec, turnCeiling, flags, }) => {
|
|
189
|
+
await this.#assertFoldPosture(workerId, flags, loopId);
|
|
190
|
+
await this.#assertLoopProvider(loopId, providerSpec);
|
|
191
|
+
if (childProviderSpec !== undefined) {
|
|
192
|
+
await this.#assertLoopChildProvider(loopId, childProviderSpec);
|
|
193
|
+
}
|
|
194
|
+
await this.#assertLoopMaxTurns(loopId, turnCeiling?.source === "explicit" ? turnCeiling.effective : undefined);
|
|
195
|
+
},
|
|
196
|
+
reconcilePrompts: (workerId, endedLoopId) => this.#reconcileOrphanedPrompts(workerId, endedLoopId),
|
|
197
|
+
runLoop: async ({ workspaceId, workerId, loopId, maxTurns, prompt, systemPrompt, signal, onDispatch, }) => {
|
|
198
|
+
const { provider, childProvider } = await this.#providersForLoop(loopId);
|
|
199
|
+
return this.#engine.runLoop({
|
|
200
|
+
provider,
|
|
201
|
+
childProvider,
|
|
202
|
+
workspaceId,
|
|
203
|
+
workerId,
|
|
204
|
+
loopId,
|
|
205
|
+
maxTurns,
|
|
206
|
+
messages: [
|
|
207
|
+
{ role: "system", content: systemPrompt },
|
|
208
|
+
{ role: "user", content: prompt },
|
|
209
|
+
],
|
|
210
|
+
origin: "model",
|
|
211
|
+
signal,
|
|
212
|
+
onDispatch,
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
loopUsage: (loopId) => this.#engine.loopUsage(loopId),
|
|
216
|
+
loopAttributions: (loopId) => this.#engine.loopAttributions(loopId),
|
|
217
|
+
takeParkDeadline: (loopId) => {
|
|
218
|
+
const deadline = this.#engine.parkDeadlines.get(loopId);
|
|
219
|
+
this.#engine.parkDeadlines.delete(loopId);
|
|
220
|
+
return deadline;
|
|
221
|
+
},
|
|
222
|
+
settleSearch: (target, status) => this.#engine.searchGate.settle(target, status),
|
|
223
|
+
cancelSubscription: (subscriptionId) => this.#engine.cancelSubscription(subscriptionId),
|
|
224
|
+
hasActiveStreams: (workerId) => this.#workerHasActiveStreams(workerId),
|
|
225
|
+
readSystemPrompt: () => readFile(Paths.instructionsSystem, "utf8"),
|
|
226
|
+
emitLogEntry: async (workspaceId, logEntryId) => {
|
|
227
|
+
const entry = await LogEntry.fetchLogEntry(this.#db, logEntryId);
|
|
228
|
+
this.#broadcast({ workspaceId }, "log/entry", { entry });
|
|
229
|
+
},
|
|
230
|
+
emit: (workspaceId, method, params) => this.#broadcast({ workspaceId }, method, params),
|
|
231
|
+
});
|
|
223
232
|
// Wire proposal-pending events to the loop/proposal WS notification.
|
|
224
233
|
// Sessionid scopes the broadcast to clients on the same workspace.
|
|
225
234
|
this.#engine.onProposalPending((event) => {
|
|
@@ -398,8 +407,13 @@ export default class Daemon {
|
|
|
398
407
|
async #providerSpecForLoop(loopId) {
|
|
399
408
|
return (await this.#providerPolicyForLoop(loopId)).providerSpec;
|
|
400
409
|
}
|
|
401
|
-
async #
|
|
402
|
-
|
|
410
|
+
async #providersForLoop(loopId) {
|
|
411
|
+
const policy = await this.#providerPolicyForLoop(loopId);
|
|
412
|
+
const provider = await ProviderInstantiate.instantiateProvider(policy.providerSpec);
|
|
413
|
+
const childProvider = policy.childProviderSpec === null
|
|
414
|
+
? provider
|
|
415
|
+
: await ProviderInstantiate.instantiateProvider(policy.childProviderSpec);
|
|
416
|
+
return { provider, childProvider };
|
|
403
417
|
}
|
|
404
418
|
async #assertLoopProvider(loopId, requested) {
|
|
405
419
|
const selected = await this.#providerSpecForLoop(loopId);
|
|
@@ -608,9 +622,7 @@ export default class Daemon {
|
|
|
608
622
|
const isActive = active !== null && active.alias === a.alias;
|
|
609
623
|
return {
|
|
610
624
|
alias: a.alias, provider: a.provider, model: a.model, active: isActive,
|
|
611
|
-
|
|
612
|
-
// optional virtual pressure; known for the active alias, null elsewhere.
|
|
613
|
-
promptBudget: isActive && this.#provider !== null ? this.#engine.promptBudgetFor(this.#provider) : null,
|
|
625
|
+
inputCapacity: isActive && this.#provider !== null ? this.#provider.inputCapacity : null,
|
|
614
626
|
};
|
|
615
627
|
}),
|
|
616
628
|
};
|
|
@@ -853,18 +865,16 @@ export default class Daemon {
|
|
|
853
865
|
contentOffset: c.contentOffset,
|
|
854
866
|
contentLength: c.contentLength,
|
|
855
867
|
mimetype: c.mimetype,
|
|
856
|
-
|
|
868
|
+
weight: c.weight,
|
|
857
869
|
state: c.state,
|
|
858
870
|
};
|
|
859
871
|
}
|
|
860
|
-
const tagRows = await this.#db.crud_read_tags.all({ entry_id: row.id });
|
|
861
872
|
return entryReadResult({
|
|
862
873
|
status: 200,
|
|
863
874
|
entry: {
|
|
864
875
|
entryId: row.id,
|
|
865
876
|
target: location.target,
|
|
866
877
|
channels,
|
|
867
|
-
tags: tagRows.map((tag) => tag.tag),
|
|
868
878
|
},
|
|
869
879
|
});
|
|
870
880
|
}
|
|
@@ -922,12 +932,13 @@ export default class Daemon {
|
|
|
922
932
|
if (typeof namespaceOwner !== "string" || namespaceOwner.trim().length === 0) {
|
|
923
933
|
throw new Error("registerRuntime: namespaceOwner must be a non-empty string");
|
|
924
934
|
}
|
|
925
|
-
|
|
935
|
+
const runtime = RuntimeDeclaration.assert(decl, namespaceOwner);
|
|
936
|
+
this.#engine.registerRuntime(runtime.name, {
|
|
926
937
|
executor,
|
|
927
938
|
namespaceOwner: { kind: "module", name: namespaceOwner },
|
|
928
|
-
glyph:
|
|
929
|
-
|
|
930
|
-
documentation:
|
|
939
|
+
glyph: runtime.glyph ?? "",
|
|
940
|
+
invocation: runtime.invocation,
|
|
941
|
+
documentation: runtime.documentation ?? "",
|
|
931
942
|
available: availability.available,
|
|
932
943
|
detail: availability.detail,
|
|
933
944
|
}, scheme);
|
|
@@ -975,6 +986,7 @@ export default class Daemon {
|
|
|
975
986
|
if (this.#started)
|
|
976
987
|
throw new Error("daemon already started");
|
|
977
988
|
this.#started = true;
|
|
989
|
+
this.#drains.start();
|
|
978
990
|
// Mimetypes owns its own discovery scan over @plurnk/plurnk-mimetypes-*
|
|
979
991
|
// packages; pre-warm it so first index render doesn't pay the cost.
|
|
980
992
|
await this.#mimetypes.ready();
|
|
@@ -1019,7 +1031,8 @@ export default class Daemon {
|
|
|
1019
1031
|
}
|
|
1020
1032
|
async #recoverLifecycle() {
|
|
1021
1033
|
await this.#db.recovery_fail_active_loops.run({});
|
|
1022
|
-
await this.#db.
|
|
1034
|
+
await this.#db.recovery_settle_open_provider_requests.run({});
|
|
1035
|
+
await this.#db.recovery_fail_open_model_calls.run({});
|
|
1023
1036
|
await this.#db.recovery_fail_ownerless_proposals.run({});
|
|
1024
1037
|
await this.#db.recovery_error_orphan_subscription_channels.run({});
|
|
1025
1038
|
await this.#db.recovery_fail_orphan_subscriptions.run({});
|
|
@@ -1027,12 +1040,12 @@ export default class Daemon {
|
|
|
1027
1040
|
await this.#branchBatches.recover();
|
|
1028
1041
|
const orphanSources = await this.#db.recovery_orphan_prompt_sources.all({});
|
|
1029
1042
|
for (const source of orphanSources) {
|
|
1030
|
-
await this.#reconcileOrphanedPrompts(source.worker_id, source.loop_id);
|
|
1043
|
+
await this.#drains.reconcileOrphanedPrompts(source.worker_id, source.loop_id);
|
|
1031
1044
|
}
|
|
1032
1045
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
1033
1046
|
const queued = await this.#db.recovery_queued_workers.all({});
|
|
1034
1047
|
for (const row of queued) {
|
|
1035
|
-
const started = await this.#ensureDrain({
|
|
1048
|
+
const started = await this.#drains.ensureDrain({
|
|
1036
1049
|
workspaceId: row.workspace_id,
|
|
1037
1050
|
workerId: row.worker_id,
|
|
1038
1051
|
systemPrompt,
|
|
@@ -1043,7 +1056,7 @@ export default class Daemon {
|
|
|
1043
1056
|
}
|
|
1044
1057
|
const parked = await this.#db.recovery_parked_workers.all({});
|
|
1045
1058
|
for (const row of parked) {
|
|
1046
|
-
await this.#schedulePollWake(row.workspace_id, row.worker_id, systemPrompt);
|
|
1059
|
+
await this.#drains.schedulePollWake(row.workspace_id, row.worker_id, systemPrompt);
|
|
1047
1060
|
}
|
|
1048
1061
|
}
|
|
1049
1062
|
async stop() {
|
|
@@ -1057,8 +1070,8 @@ export default class Daemon {
|
|
|
1057
1070
|
.toReversed()
|
|
1058
1071
|
.map((module) => Promise.resolve().then(() => module.close())));
|
|
1059
1072
|
this.#moduleClosers = [];
|
|
1060
|
-
// Drain order: (1)
|
|
1061
|
-
// strike paths don't keep going, (2) await
|
|
1073
|
+
// Drain order: (1) tell the supervisor to abort worker scopes so
|
|
1074
|
+
// strike paths don't keep going, (2) await its active drains
|
|
1062
1075
|
// to completion, (3) drain streaming schemes' background work
|
|
1063
1076
|
// (exec spawn cleanup, channel writes). Only THEN close the DB
|
|
1064
1077
|
// upstream — drain queries hit the DB right up until they exit.
|
|
@@ -1073,24 +1086,9 @@ export default class Daemon {
|
|
|
1073
1086
|
this.#engine.cancelAllProposals("daemon_stopping");
|
|
1074
1087
|
this.#engine.cancelDerivations(derivationAbort);
|
|
1075
1088
|
this.#branchBatches.beginStop();
|
|
1076
|
-
|
|
1077
|
-
if (!scope.signal.aborted)
|
|
1078
|
-
scope.abort("daemon_stopping");
|
|
1079
|
-
}
|
|
1080
|
-
for (const t of this.#pollTimers.values())
|
|
1081
|
-
clearTimeout(t); // drop pending hibernation poll-wakes
|
|
1082
|
-
this.#pollBackoff.clear();
|
|
1083
|
-
this.#pollTimers.clear();
|
|
1084
|
-
// Cancel park-deadline timers before DB close; otherwise a late #wakeParkedWorker would run after
|
|
1085
|
-
// stop/db-close if left pending — an unhandled rejection (SqlRite closed) that abnormally
|
|
1086
|
-
// exits the worker under load. Symmetric with the poll-wakes above; both must be reaped.
|
|
1087
|
-
for (const t of this.#parkTimers.values())
|
|
1088
|
-
clearTimeout(t);
|
|
1089
|
-
this.#parkTimers.clear();
|
|
1089
|
+
this.#drains.beginStop("daemon_stopping");
|
|
1090
1090
|
await this.#branchBatches.idle();
|
|
1091
|
-
const
|
|
1092
|
-
await Promise.allSettled(drainPromises);
|
|
1093
|
-
await Promise.allSettled([...this.#drainExitTasks]);
|
|
1091
|
+
const [drainResult] = await Promise.allSettled([this.#drains.idle()]);
|
|
1094
1092
|
const closeResults = await moduleClose;
|
|
1095
1093
|
const [streamingResult] = await Promise.allSettled([this.#drainStreamingSchemes()]);
|
|
1096
1094
|
const [derivationResult] = await Promise.allSettled([
|
|
@@ -1100,7 +1098,19 @@ export default class Daemon {
|
|
|
1100
1098
|
? await Promise.allSettled([this.#mimetypes.dispose()])
|
|
1101
1099
|
: [];
|
|
1102
1100
|
const [schemeResult] = await Promise.allSettled([this.#schemes.close()]);
|
|
1103
|
-
|
|
1101
|
+
// Streaming and scheme closure are the last producers of synchronous
|
|
1102
|
+
// conclusion notifications. Join the supervisor-owned async tails only
|
|
1103
|
+
// after those producers settle, before the caller may close SQLite.
|
|
1104
|
+
const [wakeResult] = await Promise.allSettled([this.#drains.idle()]);
|
|
1105
|
+
const closeErrors = [
|
|
1106
|
+
...closeResults,
|
|
1107
|
+
drainResult,
|
|
1108
|
+
streamingResult,
|
|
1109
|
+
derivationResult,
|
|
1110
|
+
...mimetypeResults,
|
|
1111
|
+
schemeResult,
|
|
1112
|
+
wakeResult,
|
|
1113
|
+
]
|
|
1104
1114
|
.filter((result) => result.status === "rejected")
|
|
1105
1115
|
.flatMap((result) => result.reason instanceof AggregateError
|
|
1106
1116
|
? [...result.reason.errors]
|
|
@@ -1129,18 +1139,6 @@ export default class Daemon {
|
|
|
1129
1139
|
notifyNotice(workspaceId, payload) {
|
|
1130
1140
|
this.#broadcast({ workspaceId }, "notice/event", payload);
|
|
1131
1141
|
}
|
|
1132
|
-
/**
|
|
1133
|
-
* Inject a prompt into a worker. Two paths:
|
|
1134
|
-
* - Active drain: writes the next prompt:///<loop>/<N> entry via
|
|
1135
|
-
* Engine.inject. The current loop publishes it at its next
|
|
1136
|
-
* turn. Returns immediately with {action: "injected_next_turn"}.
|
|
1137
|
-
* - No active drain: enqueues a fresh loop with the prompt at
|
|
1138
|
-
* status=100, starts a drain. Returns the drain promise so the
|
|
1139
|
-
* caller can await full completion.
|
|
1140
|
-
*
|
|
1141
|
-
* Both `runLoop` and wake-on-completion go through this method
|
|
1142
|
-
* ({§actor-boundary-passive-wake}).
|
|
1143
|
-
*/
|
|
1144
1142
|
// {§methods-loop-run-fold-consistency} — a folded prompt cannot reconfigure its loop.
|
|
1145
1143
|
async #assertFoldPosture(workerId, flags, loopId) {
|
|
1146
1144
|
if (flags === undefined || Object.keys(flags).length === 0)
|
|
@@ -1161,815 +1159,56 @@ export default class Daemon {
|
|
|
1161
1159
|
});
|
|
1162
1160
|
}
|
|
1163
1161
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
// Active loop (status=102)? Fold the wake/prompt into its next turn.
|
|
1167
|
-
// engine.inject returns null when no loop is currently executing, so
|
|
1168
|
-
// we enqueue a fresh loop below and ensure a drain claims it.
|
|
1169
|
-
if (this.#activeDrains.has(workerId)) {
|
|
1170
|
-
const active = await this.#db.drain_current_loop_for_worker.get({ worker_id: workerId });
|
|
1171
|
-
if (active !== undefined) {
|
|
1172
|
-
await this.#assertFoldPosture(workerId, args.flags, active.id); // compare with the exact durable loop
|
|
1173
|
-
await this.#assertLoopProvider(active.id, args.providerSpec);
|
|
1174
|
-
if (args.childProviderSpec !== undefined)
|
|
1175
|
-
await this.#assertLoopChildProvider(active.id, args.childProviderSpec);
|
|
1176
|
-
await this.#assertLoopMaxTurns(active.id, args.turnCeiling?.source === "explicit" ? args.turnCeiling.effective : undefined);
|
|
1177
|
-
}
|
|
1178
|
-
const result = await this.#engine.inject(workerId, prompt, args.openPaths ?? []);
|
|
1179
|
-
if (result !== null) {
|
|
1180
|
-
return { action: "injected_next_turn", loopId: result.loopId, turnSeq: result.turnSeq };
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
// {§worker-lifecycle-wake-requeue-not-terminal} — a worker parked at 202 resumes that loop in place:
|
|
1184
|
-
// is a wake edge like a stream/child conclusion, not a fresh loop that orphans the parked one
|
|
1185
|
-
// (which would leave the worker non-quiescent forever). engine.inject writes the message as the
|
|
1186
|
-
// slept loop's next-turn prompt (the directed message — distinct from the env door, which
|
|
1187
|
-
// resumes promptless); then re-queue + drain it. {§worker-lifecycle-wake-liveness}.
|
|
1188
|
-
if (!this.#activeDrains.has(workerId)) {
|
|
1189
|
-
const slept = await this.#db.drain_find_slept_loop.get({ worker_id: workerId });
|
|
1190
|
-
if (slept !== undefined) {
|
|
1191
|
-
await this.#assertFoldPosture(workerId, args.flags, slept.id); // resume drops nothing silently
|
|
1192
|
-
await this.#assertLoopProvider(slept.id, args.providerSpec);
|
|
1193
|
-
if (args.childProviderSpec !== undefined)
|
|
1194
|
-
await this.#assertLoopChildProvider(slept.id, args.childProviderSpec);
|
|
1195
|
-
await this.#assertLoopMaxTurns(slept.id, args.turnCeiling?.source === "explicit" ? args.turnCeiling.effective : undefined);
|
|
1196
|
-
const injected = await this.#engine.inject(workerId, prompt, args.openPaths ?? []);
|
|
1197
|
-
await this.#lifecycle.wake(slept.id);
|
|
1198
|
-
const started = await this.#ensureDrain({
|
|
1199
|
-
workspaceId, workerId, systemPrompt: args.systemPrompt,
|
|
1200
|
-
});
|
|
1201
|
-
return { action: "injected_next_turn", loopId: slept.id, ...(injected?.turnSeq !== undefined ? { turnSeq: injected.turnSeq } : {}), ...(started ?? {}) };
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
const loopId = await this.#enqueueFreshLoop({
|
|
1205
|
-
workerId,
|
|
1206
|
-
prompt,
|
|
1207
|
-
providerSpec: args.providerSpec,
|
|
1208
|
-
childProviderSpec: args.childProviderSpec ?? null,
|
|
1209
|
-
maxTurns: args.turnCeiling?.effective,
|
|
1210
|
-
flags: args.flags,
|
|
1211
|
-
openPaths: args.openPaths,
|
|
1212
|
-
});
|
|
1213
|
-
// Guarantee a drain claims the loop we just enqueued. #ensureDrain runs its
|
|
1214
|
-
// check-and-start UNDER the per-worker drain lock ({§worker-lifecycle-single-drain}),
|
|
1215
|
-
// serialized against a draining sibling's teardown relinquish so the two can't
|
|
1216
|
-
// both register a drain (R4). A live drain re-claims the loop in its own
|
|
1217
|
-
// iteration or its lock-held exit re-claim, so it's never stranded.
|
|
1218
|
-
// firstLoopPromise is present only when THIS call started the drain — runLoop
|
|
1219
|
-
// keys its fast-path response on that.
|
|
1220
|
-
const started = await this.#ensureDrain({
|
|
1221
|
-
workspaceId, workerId, systemPrompt: args.systemPrompt,
|
|
1222
|
-
});
|
|
1223
|
-
return { action: "enqueued_new_loop", loopId, ...(started ?? {}) };
|
|
1224
|
-
}
|
|
1225
|
-
async #enqueueFreshLoop(args) {
|
|
1226
|
-
// {§worker-lifecycle-single-drain}: sequence allocation and insertion
|
|
1227
|
-
// are one queue mutation; another accepted prompt cannot claim the gap.
|
|
1228
|
-
return this.#withDrainLock(args.workerId, async () => {
|
|
1229
|
-
const seqRow = await this.#db.loop_run_next_sequence.get({
|
|
1230
|
-
worker_id: args.workerId,
|
|
1231
|
-
});
|
|
1232
|
-
if (seqRow === undefined)
|
|
1233
|
-
throw new Error("enqueueFreshLoop: next-sequence query returned no row");
|
|
1234
|
-
const loopRow = await this.#db.drain_enqueue_loop.get({
|
|
1235
|
-
worker_id: args.workerId,
|
|
1236
|
-
sequence: seqRow.next,
|
|
1237
|
-
prompt: args.prompt,
|
|
1238
|
-
provider_spec: JSON.stringify(args.providerSpec),
|
|
1239
|
-
child_provider_spec: JSON.stringify(args.childProviderSpec),
|
|
1240
|
-
max_turns: args.maxTurns ?? Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "50"),
|
|
1241
|
-
});
|
|
1242
|
-
if (loopRow === undefined)
|
|
1243
|
-
throw new Error("enqueueFreshLoop: loop enqueue returned no row");
|
|
1244
|
-
if (args.flags !== undefined) {
|
|
1245
|
-
await this.#db.engine_set_loop_flags.run({
|
|
1246
|
-
loop_id: loopRow.id,
|
|
1247
|
-
flags: JSON.stringify({ ...DEFAULT_LOOP_FLAGS, ...args.flags }),
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1250
|
-
if (args.openPaths !== undefined && args.openPaths.length > 0) {
|
|
1251
|
-
await this.#db.engine_set_loop_open_paths.run({
|
|
1252
|
-
loop_id: loopRow.id,
|
|
1253
|
-
open_paths: JSON.stringify(args.openPaths),
|
|
1254
|
-
});
|
|
1255
|
-
}
|
|
1256
|
-
return loopRow.id;
|
|
1257
|
-
});
|
|
1162
|
+
inject(args) {
|
|
1163
|
+
return this.#drains.inject(args);
|
|
1258
1164
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
* drain_claim_next_loop (atomic 100→102 flip), executes each via
|
|
1262
|
-
* Engine.runLoop, and re-checks. Stream-aware: when the queue is empty
|
|
1263
|
-
* but the worker has active subscriptions, the drain parks on a
|
|
1264
|
-
* #drainPokes signal — wake-on-completion → inject() wakes it. Drain
|
|
1265
|
-
* exits when queue is empty AND no active subscriptions remain.
|
|
1266
|
-
*
|
|
1267
|
-
* Returns both `firstLoopPromise` (resolves once the first loop the
|
|
1268
|
-
* drain processes completes — used by runLoop to give the caller a
|
|
1269
|
-
* fast response containing their loop's result) and `drainPromise`
|
|
1270
|
-
* (resolves only when the whole drain finishes, queue+subs settled).
|
|
1271
|
-
*/
|
|
1272
|
-
#startDrain(opts) {
|
|
1273
|
-
const { workspaceId, workerId, systemPrompt } = opts;
|
|
1274
|
-
// The drain runs under the worker's cancellation scope (shared with the
|
|
1275
|
-
// execs its loops spawn), so loop.cancel/shutdown abort it as a unit.
|
|
1276
|
-
const controller = this.#workerSignal(workerId);
|
|
1277
|
-
const handle = {
|
|
1278
|
-
controller, promise: Promise.resolve(),
|
|
1279
|
-
};
|
|
1280
|
-
let resolveFirst = () => { };
|
|
1281
|
-
let rejectFirst = () => { };
|
|
1282
|
-
const firstLoopPromise = new Promise((res, rej) => {
|
|
1283
|
-
resolveFirst = res;
|
|
1284
|
-
rejectFirst = rej;
|
|
1285
|
-
});
|
|
1286
|
-
let firstSettled = false;
|
|
1287
|
-
const claim = () => this.#db.drain_claim_next_loop.get({ worker_id: workerId });
|
|
1288
|
-
const drainPromise = (async () => {
|
|
1289
|
-
let loopsDrained = 0;
|
|
1290
|
-
let lastResult = null;
|
|
1291
|
-
let currentLoopId = null; // the loop being drained — for abort→499 settlement
|
|
1292
|
-
try {
|
|
1293
|
-
while (true) {
|
|
1294
|
-
controller.signal.throwIfAborted();
|
|
1295
|
-
let loopRow = await claim();
|
|
1296
|
-
if (loopRow === undefined) {
|
|
1297
|
-
// Queue empty → teardown UNDER the per-worker drain lock (R4 / I1),
|
|
1298
|
-
// serialized against #ensureDrain so a concurrent inject can't
|
|
1299
|
-
// start a 2nd drain in the gap. Re-claim while holding the lock;
|
|
1300
|
-
// relinquish the registry slot only if it's empty too. A loop
|
|
1301
|
-
// that raced in is returned and run — we stay registered, so
|
|
1302
|
-
// there's no transient delete for #ensureDrain to catch.
|
|
1303
|
-
loopRow = await this.#withDrainLock(workerId, async () => {
|
|
1304
|
-
const claimed = await claim();
|
|
1305
|
-
if (claimed === undefined && this.#activeDrains.get(workerId) === handle) {
|
|
1306
|
-
this.#activeDrains.delete(workerId);
|
|
1307
|
-
}
|
|
1308
|
-
return claimed;
|
|
1309
|
-
});
|
|
1310
|
-
if (loopRow === undefined)
|
|
1311
|
-
break;
|
|
1312
|
-
}
|
|
1313
|
-
currentLoopId = loopRow.id;
|
|
1314
|
-
// {§methods-loop-run-model} — provider identity belongs to the claimed loop, not the
|
|
1315
|
-
// drain that happened to claim it. A drain can consume multiple
|
|
1316
|
-
// queued loops; resolve each durable selection at this boundary.
|
|
1317
|
-
const provider = await this.#providerForLoop(loopRow.id);
|
|
1318
|
-
const onDispatch = (logEntryId) => {
|
|
1319
|
-
// {§methods-event-subscribe} — a log-broadcast failure must never crash the drain.
|
|
1320
|
-
void (async () => {
|
|
1321
|
-
const entry = await LogEntry.fetchLogEntry(this.#db, logEntryId);
|
|
1322
|
-
this.#broadcast({ workspaceId }, "log/entry", { entry });
|
|
1323
|
-
})().catch((e) => console.error("log/entry broadcast failed:", e instanceof Error ? e.message : String(e)));
|
|
1324
|
-
};
|
|
1325
|
-
const result = await observed(// {§observability-boundary}
|
|
1326
|
-
"loop.run", { workspaceId, workerId, "loop.id": loopRow.id }, async (span) => {
|
|
1327
|
-
const loopResult = await this.#engine.runLoop({
|
|
1328
|
-
provider, workspaceId, workerId, loopId: loopRow.id, maxTurns: loopRow.max_turns,
|
|
1329
|
-
messages: [
|
|
1330
|
-
{ role: "system", content: systemPrompt },
|
|
1331
|
-
{ role: "user", content: loopRow.prompt },
|
|
1332
|
-
],
|
|
1333
|
-
origin: "model",
|
|
1334
|
-
onDispatch,
|
|
1335
|
-
signal: controller.signal,
|
|
1336
|
-
});
|
|
1337
|
-
span.setAttribute("status", loopResult.result.status);
|
|
1338
|
-
recordCounter(LOOP_TERMINALS, { status: loopResult.result.status });
|
|
1339
|
-
return loopResult;
|
|
1340
|
-
});
|
|
1341
|
-
if (result.result.status === 202) {
|
|
1342
|
-
// The loop slept via SEND[202] — suspended, not terminated. Leave it at 202
|
|
1343
|
-
// (resumable); no loop/terminated, no orphan-reconcile. A stream conclusion
|
|
1344
|
-
// (#handleWakeWorker) re-queues it; and if it holds a polled stream, a poll timer
|
|
1345
|
-
// wakes it every P to inspect ({§exec-poll}). {§worker-lifecycle-wake-liveness}.
|
|
1346
|
-
void this.#schedulePollWake(workspaceId, workerId, systemPrompt).catch((err) => console.error("poll-wake scheduling failed:", err instanceof Error ? err.message : String(err)));
|
|
1347
|
-
// {§send-premature-terminate}/SEND[202]<T> — the park deadline:
|
|
1348
|
-
// dispatcher recorded the marker's seconds; a bounded park is woken at T
|
|
1349
|
-
// regardless of arrivals, so a park always has a next turn. -1 (indefinite:
|
|
1350
|
-
// the butler, a [300] ask) schedules nothing — irc/inject/conclusions wake it.
|
|
1351
|
-
// In-memory: a daemon restart drops pending deadlines.
|
|
1352
|
-
if (currentLoopId !== null) {
|
|
1353
|
-
const deadline = this.#engine.parkDeadlines.get(currentLoopId);
|
|
1354
|
-
this.#engine.parkDeadlines.delete(currentLoopId);
|
|
1355
|
-
const prior = this.#parkTimers.get(workerId);
|
|
1356
|
-
if (prior !== undefined) {
|
|
1357
|
-
clearTimeout(prior);
|
|
1358
|
-
this.#parkTimers.delete(workerId);
|
|
1359
|
-
}
|
|
1360
|
-
if (deadline !== undefined && deadline > 0) {
|
|
1361
|
-
const t = setTimeout(() => {
|
|
1362
|
-
this.#parkTimers.delete(workerId);
|
|
1363
|
-
void this.#wakeParkedWorker(workspaceId, workerId, systemPrompt).catch((err) => console.error("park-deadline wake failed:", err instanceof Error ? err.message : String(err)));
|
|
1364
|
-
}, deadline * 1000);
|
|
1365
|
-
t.unref();
|
|
1366
|
-
this.#parkTimers.set(workerId, t);
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
// Honor an OWED wake ({§worker-lifecycle-child-wake}): a child/stream concluded while
|
|
1370
|
-
// this worker was mid-turn, before it slept — resume in place rather than park blind,
|
|
1371
|
-
// so a worker hibernation always returns. The loop is 202 here; reset to
|
|
1372
|
-
// claimable and the drain re-runs it on the next claim below.
|
|
1373
|
-
if (this.#owedWakes.delete(workerId)) {
|
|
1374
|
-
await this.#lifecycle.wake(loopRow.id);
|
|
1375
|
-
currentLoopId = null;
|
|
1376
|
-
continue;
|
|
1377
|
-
}
|
|
1378
|
-
// The loop is blocked at 202 on a live obligation ({§wait-obligation-matrix});
|
|
1379
|
-
// that obligation's conclusion is its wake edge (the owed-wake above covers the
|
|
1380
|
-
// conclude-before-block race). An idle wait never reaches here — it concluded at dispatch.
|
|
1381
|
-
currentLoopId = null;
|
|
1382
|
-
continue;
|
|
1383
|
-
}
|
|
1384
|
-
this.#owedWakes.delete(workerId); // the loop concluded (non-202) — no park to honor a held wake at
|
|
1385
|
-
const [usage, attributions, turnIds] = await Promise.all([
|
|
1386
|
-
this.#engine.loopUsage(loopRow.id),
|
|
1387
|
-
this.#engine.loopAttributions(loopRow.id),
|
|
1388
|
-
this.#lifecycle.turnIds(loopRow.id),
|
|
1389
|
-
]);
|
|
1390
|
-
this.#broadcast({ workspaceId }, "loop/terminated", {
|
|
1391
|
-
workerId,
|
|
1392
|
-
loopId: loopRow.id,
|
|
1393
|
-
result: result.result,
|
|
1394
|
-
hitMaxTurns: result.hitMaxTurns,
|
|
1395
|
-
turnIds,
|
|
1396
|
-
usage,
|
|
1397
|
-
attributions,
|
|
1398
|
-
});
|
|
1399
|
-
loopsDrained++;
|
|
1400
|
-
const loopResult = {
|
|
1401
|
-
loopId: loopRow.id,
|
|
1402
|
-
turnIds,
|
|
1403
|
-
result: result.result,
|
|
1404
|
-
hitMaxTurns: result.hitMaxTurns,
|
|
1405
|
-
usage,
|
|
1406
|
-
attributions,
|
|
1407
|
-
};
|
|
1408
|
-
lastResult = loopResult;
|
|
1409
|
-
if (!firstSettled) {
|
|
1410
|
-
firstSettled = true;
|
|
1411
|
-
resolveFirst(loopResult);
|
|
1412
|
-
}
|
|
1413
|
-
// A next-turn prompt this loop ended before consuming (a
|
|
1414
|
-
// wake conclusion or a runLoop-while-active prompt) is promoted to
|
|
1415
|
-
// a fresh queued loop so it's never silently dropped.
|
|
1416
|
-
await this.#reconcileOrphanedPrompts(workerId, loopRow.id);
|
|
1417
|
-
currentLoopId = null;
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
catch (err) {
|
|
1421
|
-
if (controller.signal.aborted) {
|
|
1422
|
-
// {§methods-loop-cancel} — loop.cancel / shutdown aborted the live drain. A cancellation
|
|
1423
|
-
// is the loop's TERMINAL state (499), delivered via loop/terminated (runLoop no
|
|
1424
|
-
// longer blocks to return it). A genuine error rejects firstLoopPromise.
|
|
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 = [];
|
|
1437
|
-
const message = ErrorDetail.preview(controller.signal.reason ?? "user_cancelled")
|
|
1438
|
-
|| "no reason was supplied";
|
|
1439
|
-
if (currentLoopId !== null) {
|
|
1440
|
-
// {§methods-loop-cancel}/{§worker-lifecycle-terminal-result} —
|
|
1441
|
-
// persist the exact 499 cancellation result before broadcasting it.
|
|
1442
|
-
const cancelled = await this.#lifecycle.finish(currentLoopId, Results.failure("lifecycle:cancel", "loop-cancelled", 499, `The loop was cancelled: ${message}.`, {}, {
|
|
1443
|
-
reason: message,
|
|
1444
|
-
stage: "loop",
|
|
1445
|
-
retryable: false,
|
|
1446
|
-
}), { terminatedBy: "cancel" });
|
|
1447
|
-
[usage, attributions] = await Promise.all([
|
|
1448
|
-
this.#engine.loopUsage(currentLoopId),
|
|
1449
|
-
this.#engine.loopAttributions(currentLoopId),
|
|
1450
|
-
]);
|
|
1451
|
-
if (cancelled !== null) {
|
|
1452
|
-
this.#broadcast({ workspaceId }, "loop/terminated", {
|
|
1453
|
-
workerId,
|
|
1454
|
-
loopId: currentLoopId,
|
|
1455
|
-
result: cancelled,
|
|
1456
|
-
hitMaxTurns: false,
|
|
1457
|
-
turnIds: await this.#lifecycle.turnIds(currentLoopId),
|
|
1458
|
-
usage,
|
|
1459
|
-
attributions,
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
if (!firstSettled) {
|
|
1464
|
-
firstSettled = true;
|
|
1465
|
-
resolveFirst({
|
|
1466
|
-
loopId: currentLoopId ?? 0,
|
|
1467
|
-
turnIds: [],
|
|
1468
|
-
result: currentLoopId === null
|
|
1469
|
-
? Results.failure("lifecycle:cancel", "loop-cancelled", 499, `The loop was cancelled: ${message}.`, {}, {
|
|
1470
|
-
reason: message,
|
|
1471
|
-
stage: "loop",
|
|
1472
|
-
retryable: false,
|
|
1473
|
-
})
|
|
1474
|
-
: await this.#lifecycle.result(currentLoopId)
|
|
1475
|
-
?? Results.failure("lifecycle:cancel", "loop-cancelled", 499, `The loop was cancelled: ${message}.`, {}, {
|
|
1476
|
-
reason: message,
|
|
1477
|
-
stage: "loop",
|
|
1478
|
-
retryable: false,
|
|
1479
|
-
}),
|
|
1480
|
-
hitMaxTurns: false,
|
|
1481
|
-
usage,
|
|
1482
|
-
});
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
else {
|
|
1486
|
-
// {§worker-lifecycle-terminal-result} — a non-abort drain
|
|
1487
|
-
// failure becomes an exact durable 500 and terminal notification;
|
|
1488
|
-
// daemon diagnostics retain the complete caught error.
|
|
1489
|
-
console.error(`drain error (workspace ${workspaceId}, worker ${workerId}, loop ${currentLoopId ?? "?"}):`, err);
|
|
1490
|
-
if (currentLoopId !== null) {
|
|
1491
|
-
const failure = err instanceof OperationFailureError
|
|
1492
|
-
? err.result
|
|
1493
|
-
: Results.failure("daemon:drain", "loop-threw", 500, "The loop failed outside its operation result contract.", {}, {
|
|
1494
|
-
stage: "loop",
|
|
1495
|
-
retryable: false,
|
|
1496
|
-
});
|
|
1497
|
-
const settled = await this.#lifecycle.finish(currentLoopId, failure)
|
|
1498
|
-
?? await this.#lifecycle.result(currentLoopId);
|
|
1499
|
-
if (settled === null) {
|
|
1500
|
-
throw new Error(`drain could not settle loop ${currentLoopId}`, { cause: err });
|
|
1501
|
-
}
|
|
1502
|
-
const [usage, attributions] = await Promise.all([
|
|
1503
|
-
this.#engine.loopUsage(currentLoopId),
|
|
1504
|
-
this.#engine.loopAttributions(currentLoopId),
|
|
1505
|
-
]);
|
|
1506
|
-
this.#broadcast({ workspaceId }, "loop/terminated", {
|
|
1507
|
-
workerId,
|
|
1508
|
-
loopId: currentLoopId,
|
|
1509
|
-
result: settled,
|
|
1510
|
-
hitMaxTurns: false,
|
|
1511
|
-
turnIds: await this.#lifecycle.turnIds(currentLoopId),
|
|
1512
|
-
usage,
|
|
1513
|
-
attributions,
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
|
-
if (!firstSettled) {
|
|
1517
|
-
firstSettled = true;
|
|
1518
|
-
rejectFirst(err);
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
throw err;
|
|
1522
|
-
}
|
|
1523
|
-
finally {
|
|
1524
|
-
if (!firstSettled) {
|
|
1525
|
-
firstSettled = true;
|
|
1526
|
-
rejectFirst(new Error("drain exited without producing a result"));
|
|
1527
|
-
}
|
|
1528
|
-
if (this.#activeDrains.get(workerId) === handle)
|
|
1529
|
-
this.#activeDrains.delete(workerId);
|
|
1530
|
-
}
|
|
1531
|
-
return { loopsDrained, lastResult };
|
|
1532
|
-
})();
|
|
1533
|
-
handle.promise = drainPromise;
|
|
1534
|
-
this.#activeDrains.set(workerId, handle);
|
|
1535
|
-
// Topology join ({§worker-loop-lifecycle}): when this drain exits having CONCLUDED the worker, wake its parent
|
|
1536
|
-
// if parked. Runs after the drain fully tears down (settled promise) so the quiescence check sees
|
|
1537
|
-
// final state; speculative (#onDrainExit no-ops unless the worker concluded AND the parent is parked).
|
|
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) => {
|
|
1541
|
-
console.error(`parent wake after worker ${workerId} settlement failed:`, err);
|
|
1542
|
-
}).finally(() => {
|
|
1543
|
-
this.#drainExitTasks.delete(drainExitTask);
|
|
1544
|
-
});
|
|
1545
|
-
// Swallow unhandled rejections (drain aborts with no awaiter); the
|
|
1546
|
-
// error already surfaced via firstLoopPromise or was logged inside.
|
|
1547
|
-
drainPromise.catch(() => { });
|
|
1548
|
-
firstLoopPromise.catch(() => { });
|
|
1549
|
-
return { firstLoopPromise, drainPromise };
|
|
1550
|
-
}
|
|
1551
|
-
// Per-worker drain-transition lock (R4 / {§worker-lifecycle-single-drain}). #ensureDrain's
|
|
1552
|
-
// start and a drain's teardown relinquish both run under it, serialized, so the two
|
|
1553
|
-
// can't interleave and register two drains for one worker. The critical section is the
|
|
1554
|
-
// registry decision only (never a loop's work) — a sub-ms hop at drain boundaries.
|
|
1555
|
-
// A promise-chain mutex: each caller awaits the prior holder; the tail self-prunes
|
|
1556
|
-
// when idle so the Map stays bounded to workers mid-transition.
|
|
1557
|
-
#withDrainLock(workerId, fn) {
|
|
1558
|
-
const prev = this.#drainLocks.get(workerId) ?? Promise.resolve();
|
|
1559
|
-
const run = prev.then(fn, fn);
|
|
1560
|
-
const tail = run.catch(() => { });
|
|
1561
|
-
this.#drainLocks.set(workerId, tail);
|
|
1562
|
-
void tail.then(() => { if (this.#drainLocks.get(workerId) === tail)
|
|
1563
|
-
this.#drainLocks.delete(workerId); });
|
|
1564
|
-
return run;
|
|
1565
|
-
}
|
|
1566
|
-
// The drain guarantee, serialized per worker via #withDrainLock so it can't race a
|
|
1567
|
-
// sibling drain's teardown relinquish into a double-drain (R4). A live drain
|
|
1568
|
-
// (registered, NOT aborting) will claim the just-enqueued loop in its own iteration
|
|
1569
|
-
// or its lock-held exit re-claim → return null. A registered-but-ABORTING drain is
|
|
1570
|
-
// in teardown and won't claim, so we don't defer to it — start fresh, or the loop
|
|
1571
|
-
// strands on a cancel/resume race (I6 no-lost-loop). Otherwise start one.
|
|
1572
|
-
#ensureDrain(opts) {
|
|
1573
|
-
return this.#withDrainLock(opts.workerId, async () => {
|
|
1574
|
-
const existing = this.#activeDrains.get(opts.workerId);
|
|
1575
|
-
if (existing !== undefined && !existing.controller.signal.aborted)
|
|
1576
|
-
return null;
|
|
1577
|
-
return this.#startDrain(opts);
|
|
1578
|
-
});
|
|
1579
|
-
}
|
|
1580
|
-
// After a loop terminates, promote every next-turn frame it never consumed
|
|
1581
|
-
// into one source-keyed queued loop. The first frame occupies the loop seed;
|
|
1582
|
-
// later frames retain separate prompt entries and publish in the same turn.
|
|
1583
|
-
// Re-entry and boot recovery complete that same queued identity.
|
|
1165
|
+
// Durable prompt promotion remains daemon policy; DrainSupervisor invokes
|
|
1166
|
+
// it under the same worker lock as enqueue and drain teardown.
|
|
1584
1167
|
async #reconcileOrphanedPrompts(workerId, endedLoopId) {
|
|
1585
|
-
await this.#
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
orphan_source_loop_id: endedLoopId,
|
|
1605
|
-
});
|
|
1606
|
-
if (recovery === undefined)
|
|
1607
|
-
throw new Error("reconcileOrphanedPrompts: enqueue returned no row");
|
|
1608
|
-
if (recovery.status !== 100)
|
|
1609
|
-
return;
|
|
1610
|
-
const moved = await this.#db.drain_rehome_orphaned_prompt_frames.all({
|
|
1611
|
-
owner_id: workerId,
|
|
1612
|
-
source_loop_id: endedLoopId,
|
|
1613
|
-
source_pattern: `${prefix}%`,
|
|
1614
|
-
source_prefix_len: prefix.length,
|
|
1615
|
-
target_prefix: promptLoopPrefix(recovery.sequence),
|
|
1616
|
-
});
|
|
1617
|
-
if (moved.length !== frames.length) {
|
|
1618
|
-
throw new Error(`reconcileOrphanedPrompts: expected to re-home ${frames.length} frames, moved ${moved.length}`);
|
|
1619
|
-
}
|
|
1168
|
+
const endedSeq = (await this.#db.engine_loop_sequence.get({ loop_id: endedLoopId }))?.sequence ?? endedLoopId;
|
|
1169
|
+
const prefix = promptLoopPrefix(endedSeq);
|
|
1170
|
+
const frames = await this.#db.drain_orphaned_prompts_for_loop.all({ loop_id: endedLoopId, owner_id: workerId, pattern: `${prefix}%`, prefix_len: prefix.length });
|
|
1171
|
+
const first = frames[0];
|
|
1172
|
+
if (first === undefined)
|
|
1173
|
+
return;
|
|
1174
|
+
const seqRow = await this.#db.loop_run_next_sequence.get({ worker_id: workerId });
|
|
1175
|
+
if (seqRow === undefined)
|
|
1176
|
+
throw new Error("reconcileOrphanedPrompts: next-sequence query returned no row");
|
|
1177
|
+
const recovery = await this.#db.drain_enqueue_orphan_recovery_loop.get({
|
|
1178
|
+
worker_id: workerId,
|
|
1179
|
+
sequence: seqRow.next,
|
|
1180
|
+
prompt: first.body,
|
|
1181
|
+
flags: first.flags,
|
|
1182
|
+
provider_spec: first.provider_spec,
|
|
1183
|
+
child_provider_spec: first.child_provider_spec,
|
|
1184
|
+
max_turns: first.max_turns,
|
|
1185
|
+
open_paths: first.open_paths ?? "[]",
|
|
1186
|
+
orphan_source_loop_id: endedLoopId,
|
|
1620
1187
|
});
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
const cancelled = await this.#lifecycle.cancelTree(workerId, reason, includeRoot);
|
|
1635
|
-
for (const targetWorkerId of cancelled.workerIds) {
|
|
1636
|
-
const pollTimer = this.#pollTimers.get(targetWorkerId);
|
|
1637
|
-
if (pollTimer !== undefined) {
|
|
1638
|
-
clearTimeout(pollTimer);
|
|
1639
|
-
this.#pollTimers.delete(targetWorkerId);
|
|
1640
|
-
}
|
|
1641
|
-
const parkTimer = this.#parkTimers.get(targetWorkerId);
|
|
1642
|
-
if (parkTimer !== undefined) {
|
|
1643
|
-
clearTimeout(parkTimer);
|
|
1644
|
-
this.#parkTimers.delete(targetWorkerId);
|
|
1645
|
-
}
|
|
1646
|
-
this.#pollBackoff.delete(targetWorkerId);
|
|
1647
|
-
this.#owedWakes.delete(targetWorkerId);
|
|
1648
|
-
const scope = this.#workerAborts.get(targetWorkerId);
|
|
1649
|
-
if (scope !== undefined && !scope.signal.aborted)
|
|
1650
|
-
scope.abort(reason);
|
|
1651
|
-
}
|
|
1652
|
-
await Promise.all(cancelled.workerIds.map(async (targetWorkerId) => this.#reapWorkerStreams(targetWorkerId)));
|
|
1653
|
-
for (const { loopId, workerId: targetWorkerId, result } of cancelled.loops) {
|
|
1654
|
-
const row = await this.#db.drain_get_worker_workspace.get({ worker_id: targetWorkerId });
|
|
1655
|
-
if (row === undefined)
|
|
1656
|
-
continue;
|
|
1657
|
-
const [usage, attributions] = await Promise.all([
|
|
1658
|
-
this.#engine.loopUsage(loopId),
|
|
1659
|
-
this.#engine.loopAttributions(loopId),
|
|
1660
|
-
]);
|
|
1661
|
-
this.#broadcast({ workspaceId: row.workspace_id }, "loop/terminated", {
|
|
1662
|
-
workerId: targetWorkerId,
|
|
1663
|
-
loopId,
|
|
1664
|
-
result,
|
|
1665
|
-
hitMaxTurns: false,
|
|
1666
|
-
turnIds: await this.#lifecycle.turnIds(loopId),
|
|
1667
|
-
usage,
|
|
1668
|
-
attributions,
|
|
1669
|
-
});
|
|
1188
|
+
if (recovery === undefined)
|
|
1189
|
+
throw new Error("reconcileOrphanedPrompts: enqueue returned no row");
|
|
1190
|
+
if (recovery.status !== 100)
|
|
1191
|
+
return;
|
|
1192
|
+
const moved = await this.#db.drain_rehome_orphaned_prompt_frames.all({
|
|
1193
|
+
owner_id: workerId,
|
|
1194
|
+
source_loop_id: endedLoopId,
|
|
1195
|
+
source_pattern: `${prefix}%`,
|
|
1196
|
+
source_prefix_len: prefix.length,
|
|
1197
|
+
target_prefix: promptLoopPrefix(recovery.sequence),
|
|
1198
|
+
});
|
|
1199
|
+
if (moved.length !== frames.length) {
|
|
1200
|
+
throw new Error(`reconcileOrphanedPrompts: expected to re-home ${frames.length} frames, moved ${moved.length}`);
|
|
1670
1201
|
}
|
|
1671
1202
|
}
|
|
1672
|
-
async #cancelWorkerTree(workerId, reason) {
|
|
1673
|
-
await this.#cancelTree(workerId, reason, true);
|
|
1674
|
-
}
|
|
1675
|
-
/**
|
|
1676
|
-
* Cancel the worker's in-flight work (loop.cancel). One abort, one scope: the
|
|
1677
|
-
* worker signal stops the running loop's turn generation AND tears down every
|
|
1678
|
-
* stream linked to it — a background exec that outlived its loop, or even a
|
|
1679
|
-
* spawn that registers after this abort (it self-aborts against the aborted
|
|
1680
|
-
* signal). Returns cancelled iff there was process-local work; durable
|
|
1681
|
-
* unresolved loops in the worker tree are terminalized independently.
|
|
1682
|
-
*/
|
|
1683
1203
|
cancelDrain(workerId, reason = "user_cancelled") {
|
|
1684
|
-
|
|
1685
|
-
const hadWork = hadDrain || this.#workerHasActiveStreams(workerId);
|
|
1686
|
-
// A cancel is deliberate — kill any pending hibernation poll-wake so it can't resurrect the worker.
|
|
1687
|
-
const pollTimer = this.#pollTimers.get(workerId);
|
|
1688
|
-
if (pollTimer !== undefined) {
|
|
1689
|
-
clearTimeout(pollTimer);
|
|
1690
|
-
this.#pollTimers.delete(workerId);
|
|
1691
|
-
}
|
|
1692
|
-
// Stop the active drain's turn-generation (its loop closes 499). The worker
|
|
1693
|
-
// signal is the optimization path — the fast, listener-driven reap.
|
|
1694
|
-
// Durable structured cancellation: one recursive transition claims the
|
|
1695
|
-
// worker and every unresolved descendant, then reaps each process-local scope.
|
|
1696
|
-
void this.#cancelWorkerTree(workerId, reason).catch((err) => {
|
|
1697
|
-
console.error(`cancelTree(${workerId}) failed:`, err);
|
|
1698
|
-
});
|
|
1699
|
-
return hadWork;
|
|
1204
|
+
return this.#drains.cancel(workerId, reason);
|
|
1700
1205
|
}
|
|
1701
|
-
//
|
|
1702
|
-
//
|
|
1703
|
-
// worker signal. Duck-typed like #drainStreamingSchemes.
|
|
1206
|
+
// Process-local stream activity is supplied to the drain owner through the
|
|
1207
|
+
// scheme registry rather than exposing that registry across the boundary.
|
|
1704
1208
|
#workerHasActiveStreams(workerId) {
|
|
1705
1209
|
const exec = this.#schemes.get("exec");
|
|
1706
1210
|
return exec?.hasActiveSpawns?.(workerId) ?? false;
|
|
1707
1211
|
}
|
|
1708
|
-
// The contract-routed reap ({§worker-lifecycle-total-reap}): durable rows enumerate
|
|
1709
|
-
// every open subscription; the live registry invokes its exact callable owner.
|
|
1710
|
-
// The worker signal is only the fast path. An exec mid-spawn or a background exec
|
|
1711
|
-
// from a past loop is caught regardless of listener timing. Idempotent — a stream
|
|
1712
|
-
// the signal already reaped shares the same registry cancellation.
|
|
1713
|
-
async #reapWorkerStreams(workerId) {
|
|
1714
|
-
const open = await ChannelWrite.findOpenSubscriptionsForWorker(this.#db, workerId);
|
|
1715
|
-
await Promise.all(open.map(({ id }) => this.#engine.cancelSubscription(id)));
|
|
1716
|
-
}
|
|
1717
|
-
/**
|
|
1718
|
-
* Wake-on-completion handler. Streaming schemes call this when a
|
|
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.
|
|
1722
|
-
*
|
|
1723
|
-
* Skipped on result.status=499 (aborted): the model already knows about
|
|
1724
|
-
* its own SEND[499], and a forcefully-cancelled loop's spawn-abort must
|
|
1725
|
-
* not resurrect the worker.
|
|
1726
|
-
*/
|
|
1727
|
-
async #handleWakeWorker(payload) {
|
|
1728
|
-
const { entryOwnerId, ...wake } = payload;
|
|
1729
|
-
const conclusion = { ...wake, workerId: entryOwnerId };
|
|
1730
|
-
// {§search-gate} — settle the dedup registration: promote on a 200 conclusion, drop on
|
|
1731
|
-
// failure (a dead search must never serve as a duplicate). No-op for non-search streams.
|
|
1732
|
-
this.#engine.searchGate.settle(payload.target.replace(/^[a-z+.-]+:\/\//, "/").replace(/^\/+/, "/"), payload.result.status);
|
|
1733
|
-
// Aborted streams don't wake — the abort was deliberate.
|
|
1734
|
-
if (payload.result.status === 499) {
|
|
1735
|
-
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1736
|
-
...conclusion, wakeAction: "skipped-aborted",
|
|
1737
|
-
});
|
|
1738
|
-
return;
|
|
1739
|
-
}
|
|
1740
|
-
// No resurrection ({§worker-lifecycle-no-resurrection}): a non-499 completion whose
|
|
1741
|
-
// worker was cancelled (idle + its scope aborted) must not start a fresh drain —
|
|
1742
|
-
// the cancel was deliberate. The deliverable is already in the channel/log and
|
|
1743
|
-
// surfaces as a `collect` environment delta ({§env-delta}) if the worker is read or
|
|
1744
|
-
// resumed; we just don't inject a turn. (An active worker folds the wake into its
|
|
1745
|
-
// next turn via inject below; a resumed worker is active, never aborted, so it is
|
|
1746
|
-
// unaffected.)
|
|
1747
|
-
const scope = this.#workerAborts.get(payload.workerId);
|
|
1748
|
-
if (scope?.signal.aborted === true && !this.#activeDrains.has(payload.workerId)) {
|
|
1749
|
-
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1750
|
-
...conclusion, wakeAction: "skipped-cancelled",
|
|
1751
|
-
});
|
|
1752
|
-
return;
|
|
1753
|
-
}
|
|
1754
|
-
try {
|
|
1755
|
-
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
1756
|
-
// A slept (202) loop means the worker parked via SEND[202] → resume it in place: re-queue
|
|
1757
|
-
// it (202→100) so the drain re-claims and CONTINUES it (seq>1 → no re-foist). Checked
|
|
1758
|
-
// FIRST: the slept status is the worker's true disposition regardless of a draining
|
|
1759
|
-
// sibling mid-teardown (the #ensureDrain lock serializes the re-claim). No fresh loop,
|
|
1760
|
-
// no summary-as-prompt — the resumed loop reads the concluded stream's own state from
|
|
1761
|
-
// the manifest. {§worker-lifecycle-wake-liveness}.
|
|
1762
|
-
const slept = await this.#db.drain_find_slept_loop.get({ worker_id: payload.workerId });
|
|
1763
|
-
if (slept !== undefined) {
|
|
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));
|
|
1769
|
-
});
|
|
1770
|
-
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1771
|
-
...conclusion, wakeAction: "resumed-loop", wakeLoopId: slept.id,
|
|
1772
|
-
});
|
|
1773
|
-
return;
|
|
1774
|
-
}
|
|
1775
|
-
// No slept loop. A live loop surfaces the concluded stream ambiently via the
|
|
1776
|
-
// environment-observation injector ({§exec-stream}) on its next turn — there is no prompt
|
|
1777
|
-
// to inject and NO task to overwrite. The obsolete "automated environment update"
|
|
1778
|
-
// synthesis (which clobbered the model's actual goal) is retired; just tell the client.
|
|
1779
|
-
if (this.#activeDrains.has(payload.workerId)) {
|
|
1780
|
-
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1781
|
-
...conclusion, wakeAction: "no-op-active-loop",
|
|
1782
|
-
});
|
|
1783
|
-
return;
|
|
1784
|
-
}
|
|
1785
|
-
// No slept loop, no active drain — nothing to resume (e.g. a SEND[200]-done worker whose
|
|
1786
|
-
// streams were swept). Surface the conclusion without opening a loop.
|
|
1787
|
-
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1788
|
-
...conclusion, wakeAction: "no-loop",
|
|
1789
|
-
});
|
|
1790
|
-
}
|
|
1791
|
-
catch (err) {
|
|
1792
|
-
console.error("wake-on-completion setup failed:", err instanceof Error ? err.message : String(err));
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
/**
|
|
1796
|
-
* grammar 0.74.20 EXEC `<T,P>` — schedule a hibernation poll-wake. Called when a loop parks at
|
|
1797
|
-
* a park; if the worker holds an open polled stream, arm a timer for its tightest cadence P that
|
|
1798
|
-
* resumes the slept loop so the model inspects progress. While the loop is ACTIVE there is no
|
|
1799
|
-
* poll work — ambient folded stream deltas already surface progress ({§exec-stream}); the wake
|
|
1800
|
-
* matters only across hibernation. A wake-edge-less 202 (no polled stream) gets no timer. {§exec-poll}
|
|
1801
|
-
*/
|
|
1802
|
-
async #schedulePollWake(workspaceId, workerId, systemPrompt) {
|
|
1803
|
-
const existing = this.#pollTimers.get(workerId);
|
|
1804
|
-
if (existing !== undefined) {
|
|
1805
|
-
clearTimeout(existing);
|
|
1806
|
-
this.#pollTimers.delete(workerId);
|
|
1807
|
-
}
|
|
1808
|
-
const row = await this.#db.drain_worker_min_poll.get({ worker_id: workerId });
|
|
1809
|
-
if ((row?.open_count ?? 0) === 0) {
|
|
1810
|
-
this.#pollBackoff.delete(workerId);
|
|
1811
|
-
return;
|
|
1812
|
-
}
|
|
1813
|
-
const pollSec = row?.poll_seconds ?? null;
|
|
1814
|
-
// {§exec-poll} — a positive explicit cadence wins, zero opts out,
|
|
1815
|
-
// and an absent cadence uses the worker's exponential-backoff step.
|
|
1816
|
-
let delayMs;
|
|
1817
|
-
if (pollSec !== null && pollSec > 0) {
|
|
1818
|
-
this.#pollBackoff.delete(workerId);
|
|
1819
|
-
delayMs = pollSec * 1000;
|
|
1820
|
-
}
|
|
1821
|
-
else if (pollSec === 0) {
|
|
1822
|
-
this.#pollBackoff.delete(workerId);
|
|
1823
|
-
return; // explicit opt-out
|
|
1824
|
-
}
|
|
1825
|
-
else {
|
|
1826
|
-
// An open stream without an explicit cadence uses the stream polling floor.
|
|
1827
|
-
// Child joins never enter this branch: durable child settlement is their only wake edge.
|
|
1828
|
-
const base = Number(process.env.PLURNK_SERVICE_EXEC_POLL_SEC ?? "60");
|
|
1829
|
-
const turns = Number(process.env.PLURNK_SERVICE_EXEC_POLL_TURNS ?? "8");
|
|
1830
|
-
const step = this.#pollBackoff.get(workerId) ?? 0;
|
|
1831
|
-
delayMs = execPollBackoffMs(step, base, turns);
|
|
1832
|
-
this.#pollBackoff.set(workerId, step + 1);
|
|
1833
|
-
}
|
|
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();
|
|
1837
|
-
const timer = setTimeout(() => {
|
|
1838
|
-
this.#pollTimers.delete(workerId);
|
|
1839
|
-
void this.#wakeParkedWorker(workspaceId, workerId, systemPrompt);
|
|
1840
|
-
}, Math.max(delayMs, optimisticWaitMs));
|
|
1841
|
-
timer.unref();
|
|
1842
|
-
this.#pollTimers.set(workerId, timer);
|
|
1843
|
-
}
|
|
1844
|
-
/** Resume `workerId`'s slept (202) loop in place — the same 202→100 resume #handleWakeWorker uses, minus a
|
|
1845
|
-
* wake payload. The shared wake primitive: a poll cadence ({§exec-poll}), a watched stream concluding,
|
|
1846
|
-
* or a child worker finishing ({§worker-loop-lifecycle} topology join) all call this. A no-op if the worker was
|
|
1847
|
-
* cancelled or isn't actually parked (no slept loop) — so calling it speculatively is safe. */
|
|
1848
|
-
async #wakeParkedWorker(workspaceId, workerId, systemPrompt, oweIfActive = true) {
|
|
1849
|
-
if (!this.#started)
|
|
1850
|
-
return;
|
|
1851
|
-
const scope = this.#workerAborts.get(workerId);
|
|
1852
|
-
if (scope?.signal.aborted === true && !this.#activeDrains.has(workerId))
|
|
1853
|
-
return; // cancelled — no resurrection
|
|
1854
|
-
const slept = await this.#db.drain_find_slept_loop.get({ worker_id: workerId });
|
|
1855
|
-
if (slept === undefined) {
|
|
1856
|
-
// Not parked. If a drain is still ACTIVE, the worker is mid-turn and about to park — the
|
|
1857
|
-
// conclusion that fired this wake arrived before the 202 committed (the conclude-before-park
|
|
1858
|
-
// race). Owe the wake: the drain honors it at park so a worker hibernation never deadlocks.
|
|
1859
|
-
// (No active drain → already concluded/running; nothing to wake.)
|
|
1860
|
-
if (this.#activeDrains.has(workerId)) {
|
|
1861
|
-
if (oweIfActive)
|
|
1862
|
-
this.#owedWakes.add(workerId);
|
|
1863
|
-
}
|
|
1864
|
-
return;
|
|
1865
|
-
}
|
|
1866
|
-
const woke = await this.#lifecycle.wake(slept.id);
|
|
1867
|
-
if (!woke)
|
|
1868
|
-
return;
|
|
1869
|
-
const started = await this.#ensureDrain({
|
|
1870
|
-
workspaceId, workerId, systemPrompt,
|
|
1871
|
-
});
|
|
1872
|
-
started?.drainPromise?.catch((err) => {
|
|
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
|
-
}
|
|
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
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
/** A worker's drain exited. If the worker truly CONCLUDED — no 202-blocked loop, no open stream — then
|
|
1956
|
-
* wake its PARENT in place if the parent is blocked on the join (the structured-concurrency join — a
|
|
1957
|
-
* child finishing is the wake edge for a parent that waited on it, {§worker-lifecycle-child-wake}). A worker
|
|
1958
|
-
* blocked at 202, or still holding a stream, is NOT concluded — its own wake edges drive it, not this.
|
|
1959
|
-
* The parent reads the child's deliverable from its own log (the {§worker-scheme-collect} delta) on
|
|
1960
|
-
* resume — control edge here, never an injected prompt. Recurses up via the parent's own drain-exit. */
|
|
1961
|
-
async #onDrainExit(workspaceId, workerId, systemPrompt) {
|
|
1962
|
-
const slept = await this.#db.drain_find_slept_loop.get({ worker_id: workerId });
|
|
1963
|
-
if (slept !== undefined)
|
|
1964
|
-
return; // parked at 202 — not concluded, the worker is still alive
|
|
1965
|
-
const openSubs = await this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId });
|
|
1966
|
-
if (openSubs.length > 0)
|
|
1967
|
-
return; // a stream still runs — its conclusion re-evaluates, not this exit
|
|
1968
|
-
const parent = await this.#db.worker_parent_id.get({ worker_id: workerId });
|
|
1969
|
-
if (parent?.parent_worker_id == null)
|
|
1970
|
-
return; // a root worker — nobody to wake
|
|
1971
|
-
await this.#settleCompletionWake(workspaceId, parent.parent_worker_id, systemPrompt);
|
|
1972
|
-
}
|
|
1973
1212
|
// {§methods-event-subscribe} — subscriber failures are transport-local:
|
|
1974
1213
|
// log them at this boundary and never re-enter engine control flow.
|
|
1975
1214
|
#emitTo(workspaceId, method, params) {
|