@plurnk/plurnk-service 1.7.0 → 1.8.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 +29 -42
- package/INSTALL.md +49 -10
- package/README.md +11 -1
- package/SPEC.md +631 -279
- package/digest-sql/channel-dispositions/dispositions.sql +15 -0
- package/digest-sql/channel-state/state.sql +6 -0
- package/digest-sql/curation/curation.sql +5 -0
- package/digest-sql/derivations/derivations.sql +4 -0
- package/digest-sql/embeddings/embeddings.sql +4 -0
- package/digest-sql/entry-dispositions/dispositions.sql +15 -0
- package/digest-sql/entry-state/state.sql +6 -0
- package/digest-sql/token-counts/token-counts.sql +2 -0
- package/dist/Paths.d.ts +2 -5
- package/dist/Paths.d.ts.map +1 -1
- package/dist/Paths.js +9 -21
- package/dist/Paths.js.map +1 -1
- package/dist/build-info.json +1 -1
- package/dist/content/read-projector.d.ts.map +1 -1
- package/dist/content/read-projector.js +4 -3
- package/dist/content/read-projector.js.map +1 -1
- package/dist/core/BudgetReadout.d.ts.map +1 -1
- package/dist/core/BudgetReadout.js +12 -22
- package/dist/core/BudgetReadout.js.map +1 -1
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +2 -1
- package/dist/core/Dispatcher.d.ts +7 -6
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +34 -88
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/Engine.d.ts +11 -8
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +45 -27
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +112 -107
- package/dist/core/ExecutorRegistry.d.ts +8 -2
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +6 -3
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/HostPaths.d.ts +29 -0
- package/dist/core/HostPaths.d.ts.map +1 -0
- package/dist/core/HostPaths.js +74 -0
- package/dist/core/HostPaths.js.map +1 -0
- package/dist/core/LegacyHome.d.ts +10 -0
- package/dist/core/LegacyHome.d.ts.map +1 -0
- package/dist/core/LegacyHome.js +170 -0
- package/dist/core/LegacyHome.js.map +1 -0
- package/dist/core/LogBody.d.ts +1 -1
- package/dist/core/LogBody.d.ts.map +1 -1
- package/dist/core/LogBody.js +2 -2
- package/dist/core/LogBody.js.map +1 -1
- package/dist/core/LogEntryProjection.js +1 -1
- package/dist/core/LogEntryProjection.js.map +1 -1
- package/dist/core/LogVisibility.d.ts +28 -0
- package/dist/core/LogVisibility.d.ts.map +1 -0
- package/dist/core/LogVisibility.js +215 -0
- package/dist/core/LogVisibility.js.map +1 -0
- package/dist/core/LoopLifecycle.d.ts +1 -0
- package/dist/core/LoopLifecycle.d.ts.map +1 -1
- package/dist/core/LoopLifecycle.js +8 -0
- package/dist/core/LoopLifecycle.js.map +1 -1
- package/dist/core/LoopLifecycle.sql +10 -0
- package/dist/core/OperatorConfig.d.ts +6 -0
- package/dist/core/OperatorConfig.d.ts.map +1 -0
- package/dist/core/OperatorConfig.js +92 -0
- package/dist/core/OperatorConfig.js.map +1 -0
- package/dist/core/OverflowTurn.d.ts +11 -0
- package/dist/core/OverflowTurn.d.ts.map +1 -0
- package/dist/core/OverflowTurn.js +77 -0
- package/dist/core/OverflowTurn.js.map +1 -0
- package/dist/core/Owner.js +2 -2
- package/dist/core/Owner.js.map +1 -1
- package/dist/core/PacketBuilder.d.ts +3 -15
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +48 -121
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProblemLog.js +1 -1
- package/dist/core/ProblemLog.js.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +3 -23
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts +9 -5
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +99 -104
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/ResourceMutations.d.ts.map +1 -1
- package/dist/core/ResourceMutations.js +5 -3
- package/dist/core/ResourceMutations.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts +0 -1
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +7 -11
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/ToolResources.d.ts +19 -0
- package/dist/core/ToolResources.d.ts.map +1 -0
- package/dist/core/ToolResources.js +122 -0
- package/dist/core/ToolResources.js.map +1 -0
- package/dist/core/Turn.d.ts +27 -0
- package/dist/core/Turn.d.ts.map +1 -0
- package/dist/core/Turn.js +43 -0
- package/dist/core/Turn.js.map +1 -0
- package/dist/core/Turn.sql +72 -0
- package/dist/core/TurnOps.d.ts +7 -0
- package/dist/core/TurnOps.d.ts.map +1 -0
- package/dist/core/TurnOps.js +63 -0
- package/dist/core/TurnOps.js.map +1 -0
- package/dist/core/TurnRunner.d.ts +12 -4
- package/dist/core/TurnRunner.d.ts.map +1 -1
- package/dist/core/TurnRunner.js +1045 -898
- package/dist/core/TurnRunner.js.map +1 -1
- package/dist/core/WorkerName.d.ts +1 -1
- package/dist/core/WorkerName.d.ts.map +1 -1
- package/dist/core/env-defaults.d.ts.map +1 -1
- package/dist/core/env-defaults.js +7 -5
- package/dist/core/env-defaults.js.map +1 -1
- package/dist/core/fork.d.ts.map +1 -1
- package/dist/core/fork.js +15 -4
- package/dist/core/fork.js.map +1 -1
- package/dist/core/fork.sql +57 -27
- package/dist/core/git-env.d.ts +0 -1
- package/dist/core/git-env.d.ts.map +1 -1
- package/dist/core/git-env.js +0 -3
- package/dist/core/git-env.js.map +1 -1
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +10 -26
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +13 -14
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/operation-target-groups.d.ts +3 -0
- package/dist/core/operation-target-groups.d.ts.map +1 -0
- package/dist/core/operation-target-groups.js +53 -0
- package/dist/core/operation-target-groups.js.map +1 -0
- package/dist/core/packet-inject.d.ts +0 -1
- package/dist/core/packet-inject.d.ts.map +1 -1
- package/dist/core/packet-inject.js +6 -13
- package/dist/core/packet-inject.js.map +1 -1
- package/dist/core/packet-wire.d.ts +4 -0
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +208 -44
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/plurnk-uri.d.ts +1 -1
- package/dist/core/plurnk-uri.d.ts.map +1 -1
- package/dist/core/plurnk-uri.js +3 -3
- package/dist/core/plurnk-uri.js.map +1 -1
- package/dist/core/teaching.d.ts.map +1 -1
- package/dist/core/teaching.js +4 -5
- package/dist/core/teaching.js.map +1 -1
- package/dist/core/worker-settings.d.ts +9 -0
- package/dist/core/worker-settings.d.ts.map +1 -0
- package/dist/core/worker-settings.js +25 -0
- package/dist/core/worker-settings.js.map +1 -0
- package/dist/core/workspace-settings.d.ts +0 -11
- package/dist/core/workspace-settings.d.ts.map +1 -1
- package/dist/core/workspace-settings.js +3 -47
- package/dist/core/workspace-settings.js.map +1 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +164 -92
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/digest.sql +8 -2
- package/dist/observe/genai.d.ts +9 -0
- package/dist/observe/genai.d.ts.map +1 -0
- package/dist/observe/genai.js +31 -0
- package/dist/observe/genai.js.map +1 -0
- package/dist/observe/spans.d.ts +3 -3
- package/dist/observe/spans.d.ts.map +1 -1
- package/dist/observe/spans.js +20 -5
- package/dist/observe/spans.js.map +1 -1
- package/dist/schemes/EffectPolicy.d.ts +3 -0
- package/dist/schemes/EffectPolicy.d.ts.map +1 -1
- package/dist/schemes/EffectPolicy.js +38 -6
- package/dist/schemes/EffectPolicy.js.map +1 -1
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +55 -13
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/Log.d.ts +6 -2
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +73 -22
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +25 -7
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +1 -2
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/QuestionTool.d.ts +10 -0
- package/dist/schemes/QuestionTool.d.ts.map +1 -0
- package/dist/schemes/QuestionTool.js +81 -0
- package/dist/schemes/QuestionTool.js.map +1 -0
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +4 -1
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +20 -3
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +19 -1
- package/dist/schemes/_entry-find.d.ts +5 -3
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +102 -22
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-find.sql +8 -6
- package/dist/schemes/_entry-graph.sql +2 -6
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +12 -1
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_search-index.d.ts.map +1 -1
- package/dist/schemes/_search-index.js +37 -25
- package/dist/schemes/_search-index.js.map +1 -1
- package/dist/server/Daemon.d.ts +58 -7
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +551 -113
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DrainSupervisor.d.ts +12 -9
- package/dist/server/DrainSupervisor.d.ts.map +1 -1
- package/dist/server/DrainSupervisor.js +59 -10
- package/dist/server/DrainSupervisor.js.map +1 -1
- package/dist/server/client-input.d.ts +5 -2
- package/dist/server/client-input.d.ts.map +1 -1
- package/dist/server/client-input.js +53 -34
- package/dist/server/client-input.js.map +1 -1
- package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
- package/dist/server/dispatch-as-plurnk.js +28 -6
- package/dist/server/dispatch-as-plurnk.js.map +1 -1
- package/dist/server/drain.sql +26 -11
- package/dist/server/envelope.d.ts +1 -1
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +4 -4
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +25 -1
- package/dist/server/lifecycle-recovery.sql +11 -1
- package/dist/server/logEntry.d.ts +2 -1
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +3 -2
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/logEntry.sql +7 -0
- package/dist/server/loop-model.d.ts +2 -2
- package/dist/server/loop-model.d.ts.map +1 -1
- package/dist/server/loop-model.js +23 -33
- package/dist/server/loop-model.js.map +1 -1
- package/dist/server/loopDocs.d.ts.map +1 -1
- package/dist/server/loopDocs.js +61 -10
- package/dist/server/loopDocs.js.map +1 -1
- package/dist/server/loopDocs.sql +4 -2
- package/dist/server/model-catalog.d.ts +3 -0
- package/dist/server/model-catalog.d.ts.map +1 -0
- package/dist/server/model-catalog.js +83 -0
- package/dist/server/model-catalog.js.map +1 -0
- package/dist/server/model-route.d.ts +7 -0
- package/dist/server/model-route.d.ts.map +1 -0
- package/dist/server/model-route.js +47 -0
- package/dist/server/model-route.js.map +1 -0
- package/dist/server/module-discovery.d.ts +13 -0
- package/dist/server/module-discovery.d.ts.map +1 -0
- package/dist/server/module-discovery.js +78 -0
- package/dist/server/module-discovery.js.map +1 -0
- package/dist/server/skillDocs.d.ts +9 -0
- package/dist/server/skillDocs.d.ts.map +1 -0
- package/dist/server/skillDocs.js +212 -0
- package/dist/server/skillDocs.js.map +1 -0
- package/dist/server/skillDocs.sql +14 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +182 -123
- package/dist/service.js.map +1 -1
- package/migrations/001_schema.sql +424 -65
- package/package.json +37 -37
- package/dist/core/ExecutableTools.d.ts +0 -11
- package/dist/core/ExecutableTools.d.ts.map +0 -1
- package/dist/core/ExecutableTools.js +0 -49
- package/dist/core/ExecutableTools.js.map +0 -1
- package/dist/core/JournalTurn.d.ts +0 -9
- package/dist/core/JournalTurn.d.ts.map +0 -1
- package/dist/core/JournalTurn.js +0 -14
- package/dist/core/JournalTurn.js.map +0 -1
- package/dist/core/JournalTurn.sql +0 -10
- package/dist/core/git-iso.d.ts +0 -13
- package/dist/core/git-iso.d.ts.map +0 -1
- package/dist/core/git-iso.js +0 -267
- package/dist/core/git-iso.js.map +0 -1
- package/dist/core/search-gate.d.ts +0 -17
- package/dist/core/search-gate.d.ts.map +0 -1
- package/dist/core/search-gate.js +0 -77
- package/dist/core/search-gate.js.map +0 -1
- package/dist/digest/Digest.sql +0 -1
package/dist/server/Daemon.js
CHANGED
|
@@ -8,27 +8,34 @@ import ExecutorRegistry from "../core/ExecutorRegistry.js";
|
|
|
8
8
|
import SchemeRegistry from "../core/SchemeRegistry.js";
|
|
9
9
|
import { Mimetypes } from "@plurnk/plurnk-mimetypes";
|
|
10
10
|
import { RuntimeDeclaration } from "@plurnk/plurnk-execs";
|
|
11
|
+
import { projectModelRoute, routeForSpec, specForRoute } from "./model-route.js";
|
|
12
|
+
import { discoverDaemonModules } from "./module-discovery.js";
|
|
13
|
+
import WorkerSettingsReader from "../core/worker-settings.js";
|
|
14
|
+
import EffectPolicy from "../schemes/EffectPolicy.js";
|
|
15
|
+
import QuestionTool, { questionRuntimeDecl } from "../schemes/QuestionTool.js";
|
|
11
16
|
import DrainSupervisor from "./DrainSupervisor.js";
|
|
12
17
|
import { parsePath, Validator, } from "@plurnk/plurnk-contracts";
|
|
13
18
|
import LogEntry from "./logEntry.js";
|
|
14
19
|
import Envelope from "./envelope.js";
|
|
15
20
|
import ClientInput from "./client-input.js";
|
|
16
|
-
import
|
|
21
|
+
import Turn from "../core/Turn.js";
|
|
17
22
|
import LoopDocs from "./loopDocs.js";
|
|
23
|
+
import SkillDocs from "./skillDocs.js";
|
|
18
24
|
import GitMembership from "../core/git-membership.js";
|
|
19
25
|
import Fork from "../core/fork.js";
|
|
20
26
|
import WorkerName from "../core/WorkerName.js";
|
|
21
27
|
import LoopLifecycle from "../core/LoopLifecycle.js";
|
|
22
28
|
import { promptLoopPrefix } from "../core/plurnk-uri.js";
|
|
23
29
|
import { contentWeight } from "../core/content-weight.js";
|
|
24
|
-
import { parseAliasesFromEnv,
|
|
30
|
+
import { parseAliasesFromEnv, resolveActiveRoute, UnsupportedReasoningPolicyError, } from "@plurnk/plurnk-providers";
|
|
25
31
|
import ProviderInstantiate from "../core/ProviderInstantiate.js";
|
|
26
|
-
import {
|
|
32
|
+
import { resolveLoopRoute } from "./loop-model.js";
|
|
27
33
|
import LoopFlagsReader from "../core/LoopFlagsReader.js";
|
|
28
34
|
import Results, { OperationFailureError } from "../core/results.js";
|
|
29
35
|
import WorkspaceGate from "../core/WorkspaceGate.js";
|
|
30
36
|
import BranchBatches from "./BranchBatches.js";
|
|
31
37
|
import { observed, observedSync } from "../observe/spans.js";
|
|
38
|
+
import { listModelCatalog } from "./model-catalog.js";
|
|
32
39
|
const clientActionFailure = (error) => {
|
|
33
40
|
if (error instanceof OperationFailureError)
|
|
34
41
|
return error.result;
|
|
@@ -40,6 +47,9 @@ const clientActionFailure = (error) => {
|
|
|
40
47
|
};
|
|
41
48
|
const daemonFailure = (owner, code, status, detail, extensions = {}) => new OperationFailureError(Results.failure(owner, code, status, detail, {}, extensions));
|
|
42
49
|
const entryReadResult = (result) => Validator.assertEntryReadResult(result);
|
|
50
|
+
const modelRouteLabel = (route) => route.alias === undefined
|
|
51
|
+
? `model route '${route.provider}/${route.model}'`
|
|
52
|
+
: `provider alias '${route.alias}' (${route.provider}/${route.model})`;
|
|
43
53
|
export default class Daemon {
|
|
44
54
|
#db;
|
|
45
55
|
#engine;
|
|
@@ -83,7 +93,7 @@ export default class Daemon {
|
|
|
83
93
|
defaultMimetype: "text/markdown",
|
|
84
94
|
discoverOptions: { cwd: this.#discoveryCwd },
|
|
85
95
|
});
|
|
86
|
-
const bootSpec =
|
|
96
|
+
const bootSpec = resolveActiveRoute();
|
|
87
97
|
if (this.#provider !== null && bootSpec !== null) {
|
|
88
98
|
ProviderInstantiate.registerInstance(this.#provider, bootSpec);
|
|
89
99
|
}
|
|
@@ -110,10 +120,19 @@ export default class Daemon {
|
|
|
110
120
|
}))?.id;
|
|
111
121
|
if (workerId === undefined)
|
|
112
122
|
throw new Error("Branch worker insert returned no row");
|
|
123
|
+
// {§worker-model-selection} — lineage inheritance by value: the branch
|
|
124
|
+
// child begins with the spawning loop's effective spawn model.
|
|
125
|
+
await this.#db.worker_generation_policy_update.run({
|
|
126
|
+
id: workerId,
|
|
127
|
+
model_route_id: await routeForSpec(this.#db, providerSpec),
|
|
128
|
+
spawn_model_route_id: null,
|
|
129
|
+
reasoning_policy: parentPolicy.reasoningPolicy,
|
|
130
|
+
});
|
|
113
131
|
const loopId = await this.#drains.enqueueFreshLoop({
|
|
114
132
|
workerId,
|
|
115
133
|
prompt,
|
|
116
134
|
providerSpec,
|
|
135
|
+
reasoningPolicy: parentPolicy.reasoningPolicy,
|
|
117
136
|
childProviderSpec: parentPolicy.childProviderSpec,
|
|
118
137
|
flags,
|
|
119
138
|
});
|
|
@@ -153,20 +172,44 @@ export default class Daemon {
|
|
|
153
172
|
// independently (the sister is its own worker). {§machine-processes}
|
|
154
173
|
injectWorker: async ({ workspaceId, workerId, prompt, flags, parentLoopId }) => {
|
|
155
174
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
175
|
+
let providerSpec;
|
|
176
|
+
let childProviderSpec;
|
|
177
|
+
let reasoningPolicy;
|
|
178
|
+
if (parentLoopId === undefined) {
|
|
179
|
+
// {§worker-model-selection} — the voice door addresses an
|
|
180
|
+
// existing worker. Its own durable generation policy is the
|
|
181
|
+
// receiver's identity; the daemon default and sender do not
|
|
182
|
+
// overwrite it merely because a new loop must be minted.
|
|
183
|
+
const targetPolicy = await this.#resolveWorkerModel(workerId, undefined);
|
|
184
|
+
if (targetPolicy === null) {
|
|
185
|
+
throw new Error(`injectWorker: worker ${workerId} has no resolvable model route`);
|
|
186
|
+
}
|
|
187
|
+
providerSpec = targetPolicy.providerSpec;
|
|
188
|
+
reasoningPolicy = targetPolicy.reasoningPolicy;
|
|
189
|
+
childProviderSpec = await this.#resolveWorkerSpawnModel(workerId, undefined);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const parentPolicy = await this.#providerPolicyForLoop(parentLoopId);
|
|
193
|
+
providerSpec = parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
|
|
194
|
+
reasoningPolicy = parentPolicy.reasoningPolicy;
|
|
195
|
+
// {§worker-model-selection} — lineage inheritance by value:
|
|
196
|
+
// the child begins with the spawning loop's effective spawn
|
|
197
|
+
// model and no separate override of its own.
|
|
198
|
+
childProviderSpec = null;
|
|
199
|
+
await this.#db.worker_generation_policy_update.run({
|
|
200
|
+
id: workerId,
|
|
201
|
+
model_route_id: await routeForSpec(this.#db, providerSpec),
|
|
202
|
+
spawn_model_route_id: null,
|
|
203
|
+
reasoning_policy: reasoningPolicy,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
164
206
|
const { action, loopId } = await this.inject({
|
|
165
207
|
workspaceId,
|
|
166
208
|
workerId,
|
|
167
209
|
prompt,
|
|
168
210
|
providerSpec,
|
|
169
|
-
|
|
211
|
+
reasoningPolicy,
|
|
212
|
+
childProviderSpec,
|
|
170
213
|
systemPrompt,
|
|
171
214
|
...(flags === undefined ? {} : { flags }),
|
|
172
215
|
});
|
|
@@ -175,7 +218,15 @@ export default class Daemon {
|
|
|
175
218
|
branchWorker: async (args) => this.#branchBatches.enqueue(args),
|
|
176
219
|
branchCompletionGate: async (workerId) => this.#branchBatches.completionGate(workerId),
|
|
177
220
|
acquireWorkspaceTurn: async (workspaceId, workerId) => this.#workspaceGate.acquireTurn(workspaceId, workerId),
|
|
178
|
-
|
|
221
|
+
// {§skills-materialization} — filesystem installers operate out of
|
|
222
|
+
// band. Refresh under the workspace turn gate before packet assembly
|
|
223
|
+
// so the first subsequent model turn sees their exact result.
|
|
224
|
+
workspaceTurnStarting: async ({ workspaceId }) => {
|
|
225
|
+
await SkillDocs.refreshIfChanged(this.#engine, this.#db, workspaceId);
|
|
226
|
+
},
|
|
227
|
+
workspaceTurnCompleted: async ({ turnId }) => {
|
|
228
|
+
await this.#branchBatches.sealTurn(turnId);
|
|
229
|
+
},
|
|
179
230
|
// worker:// KILL (terminate) — cancel the addressed worker subtree and
|
|
180
231
|
// tear down its held streams before the operation completes.
|
|
181
232
|
cancelWorker: async (workerId, reason) => this.#drains.cancelWorkerTree(workerId, reason),
|
|
@@ -186,9 +237,14 @@ export default class Daemon {
|
|
|
186
237
|
db,
|
|
187
238
|
lifecycle: this.#lifecycle,
|
|
188
239
|
injectPrompt: (workerId, prompt, openPaths) => this.#engine.inject(workerId, prompt, openPaths),
|
|
189
|
-
assertInjectionCompatibility: async ({ workerId, loopId, providerSpec, childProviderSpec, turnCeiling, flags, }) => {
|
|
240
|
+
assertInjectionCompatibility: async ({ workerId, loopId, providerSpec, providerSpecExplicit, reasoningPolicy, childProviderSpec, turnCeiling, flags, }) => {
|
|
190
241
|
await this.#assertFoldPosture(workerId, flags, loopId);
|
|
191
|
-
|
|
242
|
+
// An omitted selector keeps the loop's durable provider; only an
|
|
243
|
+
// explicit selection is checked against it (a deliberate switch).
|
|
244
|
+
if (providerSpecExplicit !== false) {
|
|
245
|
+
await this.#assertLoopProvider(loopId, providerSpec);
|
|
246
|
+
}
|
|
247
|
+
await this.#assertLoopReasoningPolicy(loopId, reasoningPolicy);
|
|
192
248
|
if (childProviderSpec !== undefined) {
|
|
193
249
|
await this.#assertLoopChildProvider(loopId, childProviderSpec);
|
|
194
250
|
}
|
|
@@ -208,7 +264,6 @@ export default class Daemon {
|
|
|
208
264
|
{ role: "system", content: systemPrompt },
|
|
209
265
|
{ role: "user", content: prompt },
|
|
210
266
|
],
|
|
211
|
-
origin: "model",
|
|
212
267
|
signal,
|
|
213
268
|
onDispatch,
|
|
214
269
|
});
|
|
@@ -220,7 +275,6 @@ export default class Daemon {
|
|
|
220
275
|
this.#engine.parkDeadlines.delete(loopId);
|
|
221
276
|
return deadline;
|
|
222
277
|
},
|
|
223
|
-
settleSearch: (target, status) => this.#engine.searchGate.settle(target, status),
|
|
224
278
|
cancelSubscription: (subscriptionId) => this.#engine.cancelSubscription(subscriptionId),
|
|
225
279
|
hasActiveStreams: (workerId) => this.#workerHasActiveStreams(workerId),
|
|
226
280
|
readSystemPrompt: () => readFile(Paths.instructionsSystem, "utf8"),
|
|
@@ -282,34 +336,25 @@ export default class Daemon {
|
|
|
282
336
|
const prompt = ClientInput.assertPrompt("runLoop", args.prompt);
|
|
283
337
|
const requestedMaxTurns = ClientInput.assertMaxTurns("runLoop", args.maxTurns);
|
|
284
338
|
const openPaths = ClientInput.assertOpenPaths("runLoop", args.openPaths);
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
const childAlias = ClientInput.assertOptionalChildAlias("runLoop", args.childAlias);
|
|
288
|
-
const childModel = ClientInput.assertOptionalSelector("runLoop", "childModel", args.childModel);
|
|
289
|
-
if (childAlias === null && childModel !== undefined) {
|
|
290
|
-
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 });
|
|
291
|
-
}
|
|
339
|
+
const selector = ClientInput.assertOptionalSelector("runLoop", "selector", args.selector);
|
|
340
|
+
const childSelector = ClientInput.assertOptionalChildSelector("runLoop", args.childSelector);
|
|
292
341
|
const flags = ClientInput.normalizeLoopFlags("runLoop", args.flags);
|
|
293
|
-
// {§
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
342
|
+
// {§worker-model-selection} — the worker owns the model. An explicit selector
|
|
343
|
+
// persists onto the worker; an omitted selector resolves the worker's durable model
|
|
344
|
+
// (seeded once from the daemon default). The loop then snapshots the resolved route.
|
|
345
|
+
// A continuation's omitted selector must still keep the loop's durable provider.
|
|
346
|
+
const selectorExplicit = selector !== undefined;
|
|
347
|
+
const generationPolicy = await this.#resolveWorkerModel(workerId, selector);
|
|
348
|
+
if (generationPolicy === null) {
|
|
349
|
+
throw new OperationFailureError(Results.failure("daemon:provider", "not-configured", 501, "No provider is configured for this worker.", {}, {
|
|
300
350
|
stage: "provider-selection",
|
|
301
351
|
recovery: "Select a configured model provider.",
|
|
302
352
|
retryable: false,
|
|
303
353
|
}));
|
|
304
354
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
: childAlias;
|
|
309
|
-
const childSelection = configuredChildAlias === null
|
|
310
|
-
|| (configuredChildAlias === undefined && childModel === undefined)
|
|
311
|
-
? null
|
|
312
|
-
: await this.#resolveLoopProvider(configuredChildAlias, childModel);
|
|
355
|
+
const { providerSpec: selection, reasoningPolicy } = generationPolicy;
|
|
356
|
+
// {§methods-loop-run-child-provider} — the worker's persistent spawn override.
|
|
357
|
+
const childSelection = await this.#resolveWorkerSpawnModel(workerId, childSelector);
|
|
313
358
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
314
359
|
// {§machine-processes} — the model NEVER runs in a client-origin worker (its packets would carry
|
|
315
360
|
// client-action rows). The module resolves the model worker via ensureModelWorker and passes it (or a
|
|
@@ -350,72 +395,182 @@ export default class Daemon {
|
|
|
350
395
|
...(openPaths !== undefined ? { openPaths } : {}),
|
|
351
396
|
turnCeiling,
|
|
352
397
|
providerSpec: selection,
|
|
398
|
+
providerSpecExplicit: selectorExplicit,
|
|
399
|
+
reasoningPolicy,
|
|
353
400
|
childProviderSpec: childSelection,
|
|
354
401
|
systemPrompt,
|
|
355
402
|
});
|
|
356
403
|
return { status: 100, action, loopId, ...(turnSeq !== undefined ? { turnSeq } : {}) };
|
|
357
404
|
}
|
|
358
|
-
// {§methods-loop-run-model} — resolve
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const requested = resolveLoopAlias(alias, model, parseAliasesFromEnv());
|
|
405
|
+
// {§methods-loop-run-model} — resolve one alias-or-route selector to a cached
|
|
406
|
+
// Provider; absent uses the boot default. An unknown alias or malformed exact
|
|
407
|
+
// route throws legibly rather than silently running the wrong model.
|
|
408
|
+
async #resolveLoopProvider(selector, reasoningPolicy) {
|
|
409
|
+
const requested = resolveLoopRoute(selector, parseAliasesFromEnv());
|
|
364
410
|
if (requested === null && this.#provider === null)
|
|
365
411
|
return null;
|
|
366
|
-
const spec = requested ??
|
|
412
|
+
const spec = requested ?? resolveActiveRoute();
|
|
367
413
|
if (spec === null) {
|
|
368
|
-
throw daemonFailure("daemon:provider", "active-
|
|
414
|
+
throw daemonFailure("daemon:provider", "active-model-unresolved", 500, "The active provider has no resolvable model route.", { stage: "provider-selection", retryable: false });
|
|
369
415
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
416
|
+
await this.#providerForPolicy(spec, reasoningPolicy);
|
|
417
|
+
return spec;
|
|
418
|
+
}
|
|
419
|
+
// Resolve eagerly so runLoop fails before enqueue when the selected route
|
|
420
|
+
// and durable reasoning policy cannot compose. The drain later retrieves
|
|
421
|
+
// this cached handle from the loop's immutable snapshot.
|
|
422
|
+
async #providerForPolicy(spec, reasoningPolicy) {
|
|
373
423
|
try {
|
|
374
|
-
await ProviderInstantiate.instantiateProvider(spec);
|
|
424
|
+
const provider = await ProviderInstantiate.instantiateProvider(spec, process.env, reasoningPolicy);
|
|
425
|
+
ProviderInstantiate.validateGrammarConfiguration(provider, process.env, reasoningPolicy);
|
|
426
|
+
return provider;
|
|
375
427
|
}
|
|
376
428
|
catch (cause) {
|
|
377
429
|
if (cause instanceof OperationFailureError)
|
|
378
430
|
throw cause;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
431
|
+
if (cause instanceof UnsupportedReasoningPolicyError) {
|
|
432
|
+
throw daemonFailure("daemon:provider", "reasoning-policy-unsupported", 409, `${modelRouteLabel(spec)} does not support reasoning policy '${cause.policy}'.`, {
|
|
433
|
+
...(spec.alias === undefined ? {} : { alias: spec.alias }),
|
|
434
|
+
provider: spec.provider,
|
|
435
|
+
model: spec.model,
|
|
436
|
+
reasoningPolicy: cause.policy,
|
|
437
|
+
supportedReasoningPolicies: cause.supported,
|
|
438
|
+
stage: "provider-selection",
|
|
439
|
+
recovery: "Select one of the provider's supported reasoning policies.",
|
|
440
|
+
retryable: false,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
console.error(`${modelRouteLabel(spec)} could not be instantiated:`, cause);
|
|
444
|
+
throw daemonFailure("daemon:provider", "provider-unavailable", 503, `${modelRouteLabel(spec)} is unavailable.`, {
|
|
445
|
+
...(spec.alias === undefined ? {} : { alias: spec.alias }),
|
|
382
446
|
provider: spec.provider,
|
|
383
447
|
model: spec.model,
|
|
384
448
|
stage: "provider-selection",
|
|
385
449
|
retryable: false,
|
|
386
450
|
});
|
|
387
451
|
}
|
|
388
|
-
return spec;
|
|
389
452
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
453
|
+
// {§worker-model-selection} — a model worker owns one durable model. An explicit
|
|
454
|
+
// selector persists onto the worker; an omitted selector resolves the worker's
|
|
455
|
+
// durable model, seeded once from the daemon default. A deliberately modelless
|
|
456
|
+
// daemon leaves the worker unset until an explicit selection arrives.
|
|
457
|
+
async #resolveWorkerModel(workerId, selector) {
|
|
458
|
+
const worker = await this.#db.worker_generation_policy_read.get({ id: workerId });
|
|
459
|
+
if (worker === undefined)
|
|
460
|
+
throw new Error(`worker ${workerId}: model route row missing`);
|
|
461
|
+
if (selector !== undefined) {
|
|
462
|
+
const spec = await this.#resolveLoopProvider(selector, worker.reasoning_policy ?? undefined);
|
|
463
|
+
if (spec === null)
|
|
464
|
+
return null;
|
|
465
|
+
const reasoningPolicy = worker.reasoning_policy
|
|
466
|
+
?? ProviderInstantiate.configuredReasoningPolicy(spec);
|
|
467
|
+
if (worker.spawn_model_route_id !== null) {
|
|
468
|
+
const spawnSpec = await specForRoute(this.#db, worker.spawn_model_route_id);
|
|
469
|
+
if (spawnSpec === null)
|
|
470
|
+
throw new Error(`worker ${workerId}: spawn model route is missing`);
|
|
471
|
+
await this.#providerForPolicy(spawnSpec, reasoningPolicy);
|
|
472
|
+
}
|
|
473
|
+
await this.#db.worker_generation_policy_update.run({
|
|
474
|
+
id: workerId,
|
|
475
|
+
model_route_id: await routeForSpec(this.#db, spec),
|
|
476
|
+
spawn_model_route_id: worker.spawn_model_route_id,
|
|
477
|
+
reasoning_policy: reasoningPolicy,
|
|
478
|
+
});
|
|
479
|
+
return { providerSpec: spec, reasoningPolicy };
|
|
394
480
|
}
|
|
395
|
-
|
|
396
|
-
|
|
481
|
+
if (worker.model_route_id !== null) {
|
|
482
|
+
if (worker.reasoning_policy === null) {
|
|
483
|
+
throw new Error(`worker ${workerId}: durable model has no reasoning policy`);
|
|
484
|
+
}
|
|
485
|
+
const spec = await specForRoute(this.#db, worker.model_route_id);
|
|
486
|
+
if (spec === null)
|
|
487
|
+
throw new Error(`worker ${workerId}: model route is missing`);
|
|
488
|
+
await this.#providerForPolicy(spec, worker.reasoning_policy);
|
|
489
|
+
return { providerSpec: spec, reasoningPolicy: worker.reasoning_policy };
|
|
397
490
|
}
|
|
398
|
-
if (
|
|
491
|
+
if (this.#provider === null)
|
|
399
492
|
return null;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
493
|
+
const spec = resolveActiveRoute();
|
|
494
|
+
if (spec !== null) {
|
|
495
|
+
const reasoningPolicy = ProviderInstantiate.configuredReasoningPolicy(spec);
|
|
496
|
+
await this.#providerForPolicy(spec, reasoningPolicy);
|
|
497
|
+
if (worker.spawn_model_route_id !== null) {
|
|
498
|
+
const spawnSpec = await specForRoute(this.#db, worker.spawn_model_route_id);
|
|
499
|
+
if (spawnSpec === null)
|
|
500
|
+
throw new Error(`worker ${workerId}: spawn model route is missing`);
|
|
501
|
+
await this.#providerForPolicy(spawnSpec, reasoningPolicy);
|
|
502
|
+
}
|
|
503
|
+
await this.#db.worker_generation_policy_update.run({
|
|
504
|
+
id: workerId,
|
|
505
|
+
model_route_id: await routeForSpec(this.#db, spec),
|
|
506
|
+
spawn_model_route_id: worker.spawn_model_route_id,
|
|
507
|
+
reasoning_policy: reasoningPolicy,
|
|
508
|
+
});
|
|
509
|
+
return { providerSpec: spec, reasoningPolicy };
|
|
405
510
|
}
|
|
406
|
-
return
|
|
511
|
+
return null;
|
|
407
512
|
}
|
|
513
|
+
// {§worker-model-selection} — the persistent spawn override. An explicit child
|
|
514
|
+
// selector persists onto the worker (null clears it back to inherit); an omitted
|
|
515
|
+
// selector resolves the persisted override, seeded once from the operator's
|
|
516
|
+
// PLURNK_MODEL_CHILD default.
|
|
517
|
+
async #resolveWorkerSpawnModel(workerId, childSelector) {
|
|
518
|
+
const worker = await this.#db.worker_generation_policy_read.get({ id: workerId });
|
|
519
|
+
if (worker === undefined)
|
|
520
|
+
throw new Error(`worker ${workerId}: model route row missing`);
|
|
521
|
+
if (childSelector !== undefined) {
|
|
522
|
+
const spec = childSelector === null
|
|
523
|
+
? null
|
|
524
|
+
: await this.#resolveLoopProvider(childSelector, worker.reasoning_policy ?? undefined);
|
|
525
|
+
await this.#db.worker_generation_policy_update.run({
|
|
526
|
+
id: workerId,
|
|
527
|
+
model_route_id: worker.model_route_id,
|
|
528
|
+
spawn_model_route_id: spec === null ? null : await routeForSpec(this.#db, spec),
|
|
529
|
+
reasoning_policy: worker.reasoning_policy,
|
|
530
|
+
});
|
|
531
|
+
return spec;
|
|
532
|
+
}
|
|
533
|
+
if (worker.spawn_model_route_id !== null) {
|
|
534
|
+
const spec = await specForRoute(this.#db, worker.spawn_model_route_id);
|
|
535
|
+
if (spec === null)
|
|
536
|
+
throw new Error(`worker ${workerId}: spawn model route is missing`);
|
|
537
|
+
if (worker.reasoning_policy !== null) {
|
|
538
|
+
await this.#providerForPolicy(spec, worker.reasoning_policy);
|
|
539
|
+
}
|
|
540
|
+
return spec;
|
|
541
|
+
}
|
|
542
|
+
const configured = process.env.PLURNK_MODEL_CHILD;
|
|
543
|
+
if (configured === undefined || configured.length === 0)
|
|
544
|
+
return null;
|
|
545
|
+
const spec = await this.#resolveLoopProvider(configured, worker.reasoning_policy ?? undefined);
|
|
546
|
+
if (spec !== null) {
|
|
547
|
+
await this.#db.worker_generation_policy_update.run({
|
|
548
|
+
id: workerId,
|
|
549
|
+
model_route_id: worker.model_route_id,
|
|
550
|
+
spawn_model_route_id: await routeForSpec(this.#db, spec),
|
|
551
|
+
reasoning_policy: worker.reasoning_policy,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return spec;
|
|
555
|
+
}
|
|
556
|
+
// {§worker-model-selection} — the loop's durable snapshot is the immutable route ids,
|
|
557
|
+
// resolved through model_routes at the claim boundary; never re-resolved through the
|
|
558
|
+
// alias cascade (a changed alias declaration must not rewrite history).
|
|
408
559
|
async #providerPolicyForLoop(loopId) {
|
|
409
|
-
const row = await this.#db.
|
|
560
|
+
const row = await this.#db.drain_loop_generation_policy.get({ loop_id: loopId });
|
|
410
561
|
if (row === undefined)
|
|
411
562
|
throw new Error(`loop ${loopId}: provider selection row is missing`);
|
|
412
|
-
const providerSpec = this.#
|
|
563
|
+
const providerSpec = await specForRoute(this.#db, row.model_route_id);
|
|
413
564
|
if (providerSpec === null) {
|
|
414
|
-
throw new Error(`loop ${loopId}: persisted provider selection is missing
|
|
565
|
+
throw new Error(`loop ${loopId}: persisted provider selection is missing — refusing boot-default substitution`);
|
|
566
|
+
}
|
|
567
|
+
if (row.reasoning_policy === null) {
|
|
568
|
+
throw new Error(`loop ${loopId}: persisted reasoning policy is missing`);
|
|
415
569
|
}
|
|
416
570
|
return {
|
|
417
571
|
providerSpec,
|
|
418
|
-
childProviderSpec: this.#
|
|
572
|
+
childProviderSpec: await specForRoute(this.#db, row.spawn_model_route_id),
|
|
573
|
+
reasoningPolicy: row.reasoning_policy,
|
|
419
574
|
};
|
|
420
575
|
}
|
|
421
576
|
async #providerSpecForLoop(loopId) {
|
|
@@ -423,20 +578,20 @@ export default class Daemon {
|
|
|
423
578
|
}
|
|
424
579
|
async #providersForLoop(loopId) {
|
|
425
580
|
const policy = await this.#providerPolicyForLoop(loopId);
|
|
426
|
-
const provider = await
|
|
581
|
+
const provider = await this.#providerForPolicy(policy.providerSpec, policy.reasoningPolicy);
|
|
427
582
|
const childProvider = policy.childProviderSpec === null
|
|
428
583
|
? provider
|
|
429
|
-
: await
|
|
584
|
+
: await this.#providerForPolicy(policy.childProviderSpec, policy.reasoningPolicy);
|
|
430
585
|
return { provider, childProvider };
|
|
431
586
|
}
|
|
432
587
|
async #assertLoopProvider(loopId, requested) {
|
|
433
588
|
const selected = await this.#providerSpecForLoop(loopId);
|
|
434
589
|
if (JSON.stringify(selected) !== JSON.stringify(requested)) {
|
|
435
|
-
throw daemonFailure("daemon:provider", "loop-provider-conflict", 409, `Loop ${loopId} uses
|
|
590
|
+
throw daemonFailure("daemon:provider", "loop-provider-conflict", 409, `Loop ${loopId} uses ${modelRouteLabel(selected)}, not ${modelRouteLabel(requested)}.`, {
|
|
436
591
|
loopId,
|
|
437
|
-
selectedAlias: selected.alias,
|
|
592
|
+
...(selected.alias === undefined ? {} : { selectedAlias: selected.alias }),
|
|
438
593
|
selectedModel: `${selected.provider}/${selected.model}`,
|
|
439
|
-
requestedAlias: requested.alias,
|
|
594
|
+
...(requested.alias === undefined ? {} : { requestedAlias: requested.alias }),
|
|
440
595
|
requestedModel: `${requested.provider}/${requested.model}`,
|
|
441
596
|
stage: "loop-injection",
|
|
442
597
|
recovery: "Cancel or conclude the loop before selecting another provider.",
|
|
@@ -444,13 +599,28 @@ export default class Daemon {
|
|
|
444
599
|
});
|
|
445
600
|
}
|
|
446
601
|
}
|
|
602
|
+
async #assertLoopReasoningPolicy(loopId, requested) {
|
|
603
|
+
const selected = (await this.#providerPolicyForLoop(loopId)).reasoningPolicy;
|
|
604
|
+
if (selected !== requested) {
|
|
605
|
+
throw daemonFailure("daemon:provider", "loop-reasoning-policy-conflict", 409, `Loop ${loopId} uses reasoning policy '${selected}', not '${requested}'.`, {
|
|
606
|
+
loopId,
|
|
607
|
+
selectedReasoningPolicy: selected,
|
|
608
|
+
requestedReasoningPolicy: requested,
|
|
609
|
+
stage: "loop-injection",
|
|
610
|
+
recovery: "Cancel or conclude the loop before selecting another reasoning policy.",
|
|
611
|
+
retryable: false,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
447
615
|
async #assertLoopChildProvider(loopId, requested) {
|
|
448
616
|
const selected = (await this.#providerPolicyForLoop(loopId)).childProviderSpec;
|
|
449
617
|
if (JSON.stringify(selected) !== JSON.stringify(requested)) {
|
|
450
618
|
throw daemonFailure("daemon:provider", "loop-child-provider-conflict", 409, `Loop ${loopId} already has a different child provider policy.`, {
|
|
451
619
|
loopId,
|
|
452
620
|
selectedChildAlias: selected?.alias ?? null,
|
|
621
|
+
selectedChildModel: selected === null ? null : `${selected.provider}/${selected.model}`,
|
|
453
622
|
requestedChildAlias: requested?.alias ?? null,
|
|
623
|
+
requestedChildModel: requested === null ? null : `${requested.provider}/${requested.model}`,
|
|
454
624
|
stage: "loop-injection",
|
|
455
625
|
recovery: "Cancel or conclude the loop before changing its child provider policy.",
|
|
456
626
|
retryable: false,
|
|
@@ -476,10 +646,180 @@ export default class Daemon {
|
|
|
476
646
|
}
|
|
477
647
|
// {§methods-model-worker} — the workspace's model worker (created on first use), distinct from the client
|
|
478
648
|
// worker so the model's packets never carry client-action rows. The module binds its threads to this.
|
|
479
|
-
|
|
480
|
-
|
|
649
|
+
// Optional worker settings ({§worker-settings}) ride the client's per-run declaration: merged in on
|
|
650
|
+
// creation AND on every subsequent ensure, so a client can change its mind between loops.
|
|
651
|
+
async ensureModelWorker(workspaceId, settings) {
|
|
652
|
+
const checked = ClientInput.assertId("worker.ensure-model", "workspaceId", workspaceId);
|
|
653
|
+
const created = await Envelope.ensureModelWorker(this.#db, checked);
|
|
654
|
+
if (settings !== undefined)
|
|
655
|
+
await this.#mergeWorkerSettings(created, settings);
|
|
656
|
+
return created;
|
|
657
|
+
}
|
|
658
|
+
// {§worker-settings} — merge known keys into the worker's behavioral-rules bag.
|
|
659
|
+
// Validated at the boundary; unprovided keys keep their durable value.
|
|
660
|
+
async #mergeWorkerSettings(workerId, settings) {
|
|
661
|
+
const current = await WorkerSettingsReader.read(this.#db, workerId);
|
|
662
|
+
const merged = {
|
|
663
|
+
requestUserInput: settings.requestUserInput ?? current.requestUserInput,
|
|
664
|
+
};
|
|
665
|
+
await this.#db.worker_settings_update.run({ id: workerId, settings: JSON.stringify(merged) });
|
|
666
|
+
}
|
|
667
|
+
// {§worker-settings} — project the worker's behavioral rules for a client-interface surface.
|
|
668
|
+
async readWorkerSettings(args) {
|
|
669
|
+
const workspaceId = ClientInput.assertId("worker.settings.get", "workspaceId", args.workspaceId);
|
|
670
|
+
const workerId = ClientInput.assertId("worker.settings.get", "workerId", args.workerId);
|
|
671
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
672
|
+
const settings = await WorkerSettingsReader.read(this.#db, workerId);
|
|
673
|
+
return { requestUserInput: settings.requestUserInput };
|
|
674
|
+
}
|
|
675
|
+
// {§worker-settings} — persist known behavioral-rule keys; unknown keys are
|
|
676
|
+
// rejected at the input boundary, never persisted.
|
|
677
|
+
async setWorkerSettings(args) {
|
|
678
|
+
const workspaceId = ClientInput.assertId("worker.settings.set", "workspaceId", args.workspaceId);
|
|
679
|
+
const workerId = ClientInput.assertId("worker.settings.set", "workerId", args.workerId);
|
|
680
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
681
|
+
ClientInput.parseWorkerSettings(args.settings);
|
|
682
|
+
await this.#mergeWorkerSettings(workerId, args.settings);
|
|
683
|
+
const settings = await WorkerSettingsReader.read(this.#db, workerId);
|
|
684
|
+
return { requestUserInput: settings.requestUserInput };
|
|
481
685
|
}
|
|
482
|
-
// {§
|
|
686
|
+
// {§worker-model-selection} — project a worker's durable model and spawn override
|
|
687
|
+
// for a client-interface get/set surface. Ownership is checked at this boundary.
|
|
688
|
+
async readWorkerModel(args) {
|
|
689
|
+
const workspaceId = ClientInput.assertId("worker.model.get", "workspaceId", args.workspaceId);
|
|
690
|
+
const workerId = ClientInput.assertId("worker.model.get", "workerId", args.workerId);
|
|
691
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
692
|
+
const row = await this.#db.worker_generation_policy_read.get({ id: workerId });
|
|
693
|
+
if (row === undefined)
|
|
694
|
+
throw new Error(`worker ${workerId}: model route row missing`);
|
|
695
|
+
const modelSpec = await specForRoute(this.#db, row.model_route_id);
|
|
696
|
+
const spawnModelSpec = await specForRoute(this.#db, row.spawn_model_route_id);
|
|
697
|
+
return {
|
|
698
|
+
model: modelSpec === null ? null : projectModelRoute(modelSpec),
|
|
699
|
+
spawnModel: spawnModelSpec === null ? null : projectModelRoute(spawnModelSpec),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
// {§worker-model-selection} — persist an explicit model selection onto the worker
|
|
703
|
+
// and return the resolved spec. An unresolvable selector fails loud here.
|
|
704
|
+
async setWorkerModel(args) {
|
|
705
|
+
const workspaceId = ClientInput.assertId("worker.model.set", "workspaceId", args.workspaceId);
|
|
706
|
+
const workerId = ClientInput.assertId("worker.model.set", "workerId", args.workerId);
|
|
707
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
708
|
+
await this.#assertWorkerSelectable(workerId);
|
|
709
|
+
const selector = ClientInput.assertSelector("worker.model.set", "selector", args.selector);
|
|
710
|
+
const policy = await this.#resolveWorkerModel(workerId, selector);
|
|
711
|
+
if (policy === null) {
|
|
712
|
+
throw new OperationFailureError(Results.failure("daemon:provider", "not-configured", 501, "No provider is configured for this worker.", {}, { stage: "provider-selection", recovery: "Select a configured model provider.", retryable: false }));
|
|
713
|
+
}
|
|
714
|
+
return projectModelRoute(policy.providerSpec);
|
|
715
|
+
}
|
|
716
|
+
async #reasoningSupportForWorker(workerId, row, providerSpec, policy) {
|
|
717
|
+
const provider = await this.#providerForPolicy(providerSpec, policy);
|
|
718
|
+
if (row.spawn_model_route_id === null)
|
|
719
|
+
return provider.supportedReasoningPolicies;
|
|
720
|
+
const spawnSpec = await specForRoute(this.#db, row.spawn_model_route_id);
|
|
721
|
+
if (spawnSpec === null)
|
|
722
|
+
throw new Error(`worker ${workerId}: spawn model route is missing`);
|
|
723
|
+
const spawnProvider = await this.#providerForPolicy(spawnSpec, policy);
|
|
724
|
+
return provider.supportedReasoningPolicies.filter((candidate) => spawnProvider.supportedReasoningPolicies.includes(candidate));
|
|
725
|
+
}
|
|
726
|
+
// {§worker-reasoning-policy} — the worker's reasoning policy is durable and
|
|
727
|
+
// provider-relative. A model-less worker remains explicitly uninitialized.
|
|
728
|
+
async readWorkerReasoning(args) {
|
|
729
|
+
const workspaceId = ClientInput.assertId("worker.reasoning.get", "workspaceId", args.workspaceId);
|
|
730
|
+
const workerId = ClientInput.assertId("worker.reasoning.get", "workerId", args.workerId);
|
|
731
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
732
|
+
await this.#resolveWorkerModel(workerId, undefined);
|
|
733
|
+
const row = await this.#db.worker_generation_policy_read.get({ id: workerId });
|
|
734
|
+
if (row === undefined)
|
|
735
|
+
throw new Error(`worker ${workerId}: generation policy row missing`);
|
|
736
|
+
if (row.model_route_id === null) {
|
|
737
|
+
if (row.reasoning_policy !== null) {
|
|
738
|
+
throw new Error(`worker ${workerId}: reasoning policy exists without a model route`);
|
|
739
|
+
}
|
|
740
|
+
return { policy: null, supportedPolicies: [] };
|
|
741
|
+
}
|
|
742
|
+
if (row.reasoning_policy === null) {
|
|
743
|
+
throw new Error(`worker ${workerId}: durable model has no reasoning policy`);
|
|
744
|
+
}
|
|
745
|
+
const spec = await specForRoute(this.#db, row.model_route_id);
|
|
746
|
+
if (spec === null)
|
|
747
|
+
throw new Error(`worker ${workerId}: model route is missing`);
|
|
748
|
+
return {
|
|
749
|
+
policy: row.reasoning_policy,
|
|
750
|
+
supportedPolicies: await this.#reasoningSupportForWorker(workerId, row, spec, row.reasoning_policy),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
// {§worker-reasoning-policy} — mutate between loops, validate against both
|
|
754
|
+
// the worker model and its optional spawn model, then persist atomically.
|
|
755
|
+
async setWorkerReasoning(args) {
|
|
756
|
+
const workspaceId = ClientInput.assertId("worker.reasoning.set", "workspaceId", args.workspaceId);
|
|
757
|
+
const workerId = ClientInput.assertId("worker.reasoning.set", "workerId", args.workerId);
|
|
758
|
+
let policy;
|
|
759
|
+
try {
|
|
760
|
+
policy = Validator.assertReasoningPolicy(args.policy);
|
|
761
|
+
}
|
|
762
|
+
catch (cause) {
|
|
763
|
+
throw daemonFailure("daemon:input", "reasoning-policy-invalid", 400, cause instanceof Error ? cause.message : "The reasoning policy is invalid.", { field: "policy", retryable: false });
|
|
764
|
+
}
|
|
765
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
766
|
+
await this.#assertWorkerSelectable(workerId);
|
|
767
|
+
await this.#resolveWorkerModel(workerId, undefined);
|
|
768
|
+
const row = await this.#db.worker_generation_policy_read.get({ id: workerId });
|
|
769
|
+
if (row === undefined)
|
|
770
|
+
throw new Error(`worker ${workerId}: generation policy row missing`);
|
|
771
|
+
const spec = await specForRoute(this.#db, row.model_route_id);
|
|
772
|
+
if (spec === null) {
|
|
773
|
+
throw daemonFailure("daemon:provider", "worker-model-unset", 409, `Worker ${workerId} has no model against which to validate a reasoning policy.`, {
|
|
774
|
+
workerId,
|
|
775
|
+
stage: "provider-selection",
|
|
776
|
+
recovery: "Select a model before selecting its reasoning policy.",
|
|
777
|
+
retryable: false,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
const supportedPolicies = await this.#reasoningSupportForWorker(workerId, row, spec, policy);
|
|
781
|
+
await this.#db.worker_generation_policy_update.run({
|
|
782
|
+
id: workerId,
|
|
783
|
+
model_route_id: row.model_route_id,
|
|
784
|
+
spawn_model_route_id: row.spawn_model_route_id,
|
|
785
|
+
reasoning_policy: policy,
|
|
786
|
+
});
|
|
787
|
+
return { policy, supportedPolicies };
|
|
788
|
+
}
|
|
789
|
+
// {§worker-model-selection} — persist the worker's spawn override; a null
|
|
790
|
+
// selector means inherit (clears the override).
|
|
791
|
+
async setWorkerSpawnModel(args) {
|
|
792
|
+
const workspaceId = ClientInput.assertId("worker.child.set", "workspaceId", args.workspaceId);
|
|
793
|
+
const workerId = ClientInput.assertId("worker.child.set", "workerId", args.workerId);
|
|
794
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
795
|
+
await this.#assertWorkerSelectable(workerId);
|
|
796
|
+
const selector = ClientInput.assertChildSelector("worker.child.set", args.selector);
|
|
797
|
+
const spec = await this.#resolveWorkerSpawnModel(workerId, selector);
|
|
798
|
+
return spec === null ? null : projectModelRoute(spec);
|
|
799
|
+
}
|
|
800
|
+
// {§worker-model-selection} — a selection must not mutate underneath active
|
|
801
|
+
// work: a live or parked loop is a precise 409, never a silent retroactive
|
|
802
|
+
// switch of the immutable loop snapshot.
|
|
803
|
+
async #assertWorkerSelectable(workerId) {
|
|
804
|
+
if (await this.#drains.hasLiveWork(workerId)) {
|
|
805
|
+
throw daemonFailure("daemon:worker", "worker-loop-active", 409, `Worker ${workerId} has a live or parked loop; select a model after concluding or cancelling it.`, {
|
|
806
|
+
workerId,
|
|
807
|
+
stage: "model-selection",
|
|
808
|
+
recovery: "Conclude or cancel the active loop before selecting a model.",
|
|
809
|
+
retryable: false,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async #assertWorkerOwned(workspaceId, workerId) {
|
|
814
|
+
const owner = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
|
|
815
|
+
if (owner === undefined) {
|
|
816
|
+
throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
|
|
817
|
+
}
|
|
818
|
+
if (owner.workspace_id !== workspaceId) {
|
|
819
|
+
throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, { workerId, workspaceId, actualWorkspaceId: owner.workspace_id });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// {§methods-op-mirror} — execute parsed ops on behalf of a client as a
|
|
483
823
|
// client-origin turn (the log is core's, a client op is a first-class citizen), dispatched through
|
|
484
824
|
// the engine, then emitted as log/entry on the event source. One seam op backs the whole op_*
|
|
485
825
|
// family (read/edit/copy/find/fold/look/move/open/send/exec); the module parses at its edge with the
|
|
@@ -499,10 +839,10 @@ export default class Daemon {
|
|
|
499
839
|
throw error;
|
|
500
840
|
}
|
|
501
841
|
}
|
|
502
|
-
// The client-interface action contract: one AG-UI action owns one
|
|
503
|
-
// regardless of how many statements op.parse produced.
|
|
504
|
-
// keep
|
|
505
|
-
//
|
|
842
|
+
// The client-interface action contract: one AG-UI action owns one administrative
|
|
843
|
+
// loop, regardless of how many statements op.parse produced. Each statement is one
|
|
844
|
+
// ordinary operation turn; a proposal may keep that turn and loop open across
|
|
845
|
+
// interrupt/resume until settlement.
|
|
506
846
|
async dispatchClientAction(args) {
|
|
507
847
|
const workspaceId = ClientInput.assertId("operation.dispatch-batch", "workspaceId", args.workspaceId);
|
|
508
848
|
const workerId = ClientInput.assertId("operation.dispatch-batch", "workerId", args.workerId);
|
|
@@ -527,18 +867,38 @@ export default class Daemon {
|
|
|
527
867
|
const { workspaceId, workerId, loopId, statement } = args;
|
|
528
868
|
const release = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
|
|
529
869
|
try {
|
|
530
|
-
const { id: turnId } = await
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
origin: "client", onDispatch: (logEntryId) => { entryIds.push(logEntryId); },
|
|
870
|
+
const { id: turnId } = await Turn.open(this.#db, {
|
|
871
|
+
loopId,
|
|
872
|
+
producer: "client",
|
|
873
|
+
kind: "operation",
|
|
535
874
|
});
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const
|
|
539
|
-
|
|
875
|
+
let turnOpen = true;
|
|
876
|
+
try {
|
|
877
|
+
const entryIds = [];
|
|
878
|
+
const result = await this.#engine.dispatch({
|
|
879
|
+
statement, workspaceId, workerId, loopId, turnId, sequence: 1,
|
|
880
|
+
origin: "client", onDispatch: (logEntryId) => { entryIds.push(logEntryId); },
|
|
881
|
+
});
|
|
882
|
+
await Turn.complete(this.#db, turnId, result.status);
|
|
883
|
+
turnOpen = false;
|
|
884
|
+
await this.#branchBatches.sealTurn(turnId);
|
|
885
|
+
for (const logEntryId of entryIds) {
|
|
886
|
+
const entry = await LogEntry.fetchLogEntry(this.#db, logEntryId);
|
|
887
|
+
this.#broadcast({ workspaceId }, "log/entry", { entry });
|
|
888
|
+
}
|
|
889
|
+
return result;
|
|
890
|
+
}
|
|
891
|
+
catch (cause) {
|
|
892
|
+
if (turnOpen) {
|
|
893
|
+
try {
|
|
894
|
+
await Turn.complete(this.#db, turnId, 500);
|
|
895
|
+
}
|
|
896
|
+
catch (completionCause) {
|
|
897
|
+
throw new AggregateError([cause, completionCause], `client operation turn ${turnId} failed and could not be completed`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
throw cause;
|
|
540
901
|
}
|
|
541
|
-
return result;
|
|
542
902
|
}
|
|
543
903
|
finally {
|
|
544
904
|
release();
|
|
@@ -630,7 +990,7 @@ export default class Daemon {
|
|
|
630
990
|
// {§methods} — the module's render surface beyond the journal. Thin delegations
|
|
631
991
|
// into core's envelope / membership / provider machinery; the module fans the results into its own views.
|
|
632
992
|
listProviders() {
|
|
633
|
-
const active =
|
|
993
|
+
const active = resolveActiveRoute();
|
|
634
994
|
return {
|
|
635
995
|
aliases: parseAliasesFromEnv().map((a) => {
|
|
636
996
|
const isActive = active !== null && active.alias === a.alias;
|
|
@@ -641,6 +1001,11 @@ export default class Daemon {
|
|
|
641
1001
|
}),
|
|
642
1002
|
};
|
|
643
1003
|
}
|
|
1004
|
+
// {§model-catalog} The catalog is a bounded, worldless local projection.
|
|
1005
|
+
// It performs no provider I/O and never changes the worker's selection.
|
|
1006
|
+
listModels(query) {
|
|
1007
|
+
return listModelCatalog(query);
|
|
1008
|
+
}
|
|
644
1009
|
// {§client-display-capabilities} Core composes the installed family
|
|
645
1010
|
// declarations; interface modules expose this contracts-owned wire without
|
|
646
1011
|
// inventing presentation policy. `exec` is operation machinery, not an
|
|
@@ -717,6 +1082,7 @@ export default class Daemon {
|
|
|
717
1082
|
await provider.hydrate(envelope.workspaceId);
|
|
718
1083
|
}
|
|
719
1084
|
await LoopDocs.materialize(this.#engine, this.#db, envelope.workspaceId);
|
|
1085
|
+
await SkillDocs.materialize(this.#engine, this.#db, envelope.workspaceId);
|
|
720
1086
|
void this.#engine.warmWorkspaceDerivations(envelope.workspaceId).catch(() => { });
|
|
721
1087
|
this.#broadcast("all", "workspace/created", { id: envelope.workspaceId, name: envelope.workspaceName, projectRoot: envelope.projectRoot });
|
|
722
1088
|
return envelope;
|
|
@@ -915,6 +1281,9 @@ export default class Daemon {
|
|
|
915
1281
|
}
|
|
916
1282
|
}
|
|
917
1283
|
const worker = await Envelope.createModelWorker(this.#db, workspaceId, name);
|
|
1284
|
+
if (args.settings !== undefined) {
|
|
1285
|
+
await this.#mergeWorkerSettings(worker.id, args.settings);
|
|
1286
|
+
}
|
|
918
1287
|
return { workerId: worker.id, workerName: worker.name };
|
|
919
1288
|
}
|
|
920
1289
|
// {§worker-scheme-fork} — branch a worker's log while sharing the workspace world.
|
|
@@ -951,6 +1320,7 @@ export default class Daemon {
|
|
|
951
1320
|
if (this.#capabilitiesPublished) {
|
|
952
1321
|
for (const workspace of await Envelope.listWorkspaces(this.#db)) {
|
|
953
1322
|
await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
|
|
1323
|
+
await SkillDocs.materialize(this.#engine, this.#db, workspace.id);
|
|
954
1324
|
}
|
|
955
1325
|
}
|
|
956
1326
|
}
|
|
@@ -963,6 +1333,7 @@ export default class Daemon {
|
|
|
963
1333
|
await this.#schemes.ready();
|
|
964
1334
|
for (const workspace of await Envelope.listWorkspaces(this.#db)) {
|
|
965
1335
|
await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
|
|
1336
|
+
await SkillDocs.materialize(this.#engine, this.#db, workspace.id);
|
|
966
1337
|
}
|
|
967
1338
|
}
|
|
968
1339
|
}
|
|
@@ -977,8 +1348,11 @@ export default class Daemon {
|
|
|
977
1348
|
executor,
|
|
978
1349
|
namespaceOwner: { kind: "module", name: namespaceOwner },
|
|
979
1350
|
glyph: runtime.glyph ?? "",
|
|
1351
|
+
summary: runtime.summary,
|
|
980
1352
|
invocation: runtime.invocation,
|
|
981
|
-
|
|
1353
|
+
details: runtime.details ?? "",
|
|
1354
|
+
...(runtime.resourcesPath === undefined ? {} : { resourcesPath: runtime.resourcesPath }),
|
|
1355
|
+
...(runtime.expandTools === undefined ? {} : { expandTools: runtime.expandTools }),
|
|
982
1356
|
available: availability.available,
|
|
983
1357
|
detail: availability.detail,
|
|
984
1358
|
},
|
|
@@ -1091,6 +1465,7 @@ export default class Daemon {
|
|
|
1091
1465
|
rollbackRuntimes = commitRuntimes();
|
|
1092
1466
|
if (this.#capabilitiesPublished) {
|
|
1093
1467
|
await LoopDocs.materialize(this.#engine, this.#db, checkedWorkspaceId);
|
|
1468
|
+
await SkillDocs.materialize(this.#engine, this.#db, checkedWorkspaceId);
|
|
1094
1469
|
}
|
|
1095
1470
|
}
|
|
1096
1471
|
catch (cause) {
|
|
@@ -1118,6 +1493,7 @@ export default class Daemon {
|
|
|
1118
1493
|
if (this.#capabilitiesPublished) {
|
|
1119
1494
|
try {
|
|
1120
1495
|
await LoopDocs.materialize(this.#engine, this.#db, checkedWorkspaceId);
|
|
1496
|
+
await SkillDocs.materialize(this.#engine, this.#db, checkedWorkspaceId);
|
|
1121
1497
|
}
|
|
1122
1498
|
catch (rollbackCause) {
|
|
1123
1499
|
rollbackErrors.push(rollbackCause);
|
|
@@ -1158,6 +1534,23 @@ export default class Daemon {
|
|
|
1158
1534
|
// shell is the default runtime, so its executor must boot usable.
|
|
1159
1535
|
const executors = await ExecutorRegistry.build({ defaultRuntime: "sh", cwd: this.#discoveryCwd });
|
|
1160
1536
|
this.#engine.setExecutors(executors);
|
|
1537
|
+
// {§question-tool} — the native request-user-input runtime, process-wide;
|
|
1538
|
+
// per-worker admission gates its doc visibility and dispatch ({§worker-settings}).
|
|
1539
|
+
await this.#engine.registerRuntimes([{
|
|
1540
|
+
tag: "question",
|
|
1541
|
+
entry: {
|
|
1542
|
+
executor: new QuestionTool({ runtime: "question", glyph: "❓" }),
|
|
1543
|
+
namespaceOwner: { kind: "module", name: "core" },
|
|
1544
|
+
glyph: "❓",
|
|
1545
|
+
summary: questionRuntimeDecl.summary,
|
|
1546
|
+
invocation: questionRuntimeDecl.invocation,
|
|
1547
|
+
details: questionRuntimeDecl.details ?? "",
|
|
1548
|
+
available: true,
|
|
1549
|
+
detail: "in-process",
|
|
1550
|
+
},
|
|
1551
|
+
}]);
|
|
1552
|
+
// {§effect-policy-tunable} — invalid operator policy fails boot, not the first EXEC.
|
|
1553
|
+
EffectPolicy.validateConfiguration();
|
|
1161
1554
|
// {§exec} — mint a scheme per runtime tag so exec output entries address by tag
|
|
1162
1555
|
// authority (sh:///l/t/s). The "exec" scheme stays for the EXEC op dispatch.
|
|
1163
1556
|
this.#schemes.registerRuntimeSchemes(executors);
|
|
@@ -1165,6 +1558,16 @@ export default class Daemon {
|
|
|
1165
1558
|
// (agnostic, by plurnk.kind:"scheme"). They light up http://, etc. with
|
|
1166
1559
|
// no further engine change — #run wraps their context in SchemeCtxImpl ({§plugin-discovery}).
|
|
1167
1560
|
await this.#schemes.discoverExternal(this.#discoveryCwd);
|
|
1561
|
+
// {§module-discovery} — third-party daemon-module composition: trusted
|
|
1562
|
+
// packages declaring `plurnk.kind: "module"` register beside the
|
|
1563
|
+
// service's explicit composition before any module setup runs.
|
|
1564
|
+
const discoveredModules = await discoverDaemonModules({ cwd: this.#discoveryCwd });
|
|
1565
|
+
for (const packageName of discoveredModules.skipped) {
|
|
1566
|
+
console.warn(`module discovery: '${packageName}' is discovered but untrusted (PLURNK_PLUGINS_TRUSTED_ONLY); not registered`);
|
|
1567
|
+
}
|
|
1568
|
+
for (const module of discoveredModules.modules) {
|
|
1569
|
+
this.#modules.push(module);
|
|
1570
|
+
}
|
|
1168
1571
|
const setupSeam = this;
|
|
1169
1572
|
for (const module of this.#modules) {
|
|
1170
1573
|
if (module.close !== undefined)
|
|
@@ -1182,6 +1585,7 @@ export default class Daemon {
|
|
|
1182
1585
|
// consume this workspace state but never republish it.
|
|
1183
1586
|
for (const workspace of await Envelope.listWorkspaces(this.#db)) {
|
|
1184
1587
|
await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
|
|
1588
|
+
await SkillDocs.materialize(this.#engine, this.#db, workspace.id);
|
|
1185
1589
|
}
|
|
1186
1590
|
this.#capabilitiesPublished = true;
|
|
1187
1591
|
await this.#recoverLifecycle();
|
|
@@ -1198,6 +1602,7 @@ export default class Daemon {
|
|
|
1198
1602
|
await this.#db.recovery_fail_active_loops.run({});
|
|
1199
1603
|
await this.#db.recovery_settle_open_provider_requests.run({});
|
|
1200
1604
|
await this.#db.recovery_fail_open_model_calls.run({});
|
|
1605
|
+
await this.#db.recovery_fail_open_turns.run({});
|
|
1201
1606
|
await this.#db.recovery_fail_ownerless_proposals.run({});
|
|
1202
1607
|
await this.#db.recovery_remove_ownerless_client_interactions.run({});
|
|
1203
1608
|
await this.#db.recovery_error_orphan_subscription_channels.run({});
|
|
@@ -1225,10 +1630,34 @@ export default class Daemon {
|
|
|
1225
1630
|
await this.#drains.schedulePollWake(row.workspace_id, row.worker_id, systemPrompt);
|
|
1226
1631
|
}
|
|
1227
1632
|
}
|
|
1633
|
+
// {§crash-only-stop} — the settle deadline. Default 30s; an operator can
|
|
1634
|
+
// raise it for slow hosts or lower it for tests. 0 is refused (an unbounded
|
|
1635
|
+
// stop is exactly the wedge this exists to prevent).
|
|
1636
|
+
static #stopDeadlineMs() {
|
|
1637
|
+
const raw = Number(process.env.PLURNK_SERVICE_STOP_TIMEOUT_MS ?? 30_000);
|
|
1638
|
+
if (!Number.isSafeInteger(raw) || raw <= 0) {
|
|
1639
|
+
throw new Error(`PLURNK_SERVICE_STOP_TIMEOUT_MS must be a positive integer; got ${JSON.stringify(process.env.PLURNK_SERVICE_STOP_TIMEOUT_MS)}.`);
|
|
1640
|
+
}
|
|
1641
|
+
return raw;
|
|
1642
|
+
}
|
|
1228
1643
|
async stop() {
|
|
1229
1644
|
if (!this.#started)
|
|
1230
1645
|
return;
|
|
1231
1646
|
this.#started = false;
|
|
1647
|
+
const stopDeadlineMs = Daemon.#stopDeadlineMs();
|
|
1648
|
+
const deadline = Date.now() + stopDeadlineMs;
|
|
1649
|
+
const settle = (label, wait) => new Promise((resolve) => {
|
|
1650
|
+
let settled = false;
|
|
1651
|
+
const finish = (result) => {
|
|
1652
|
+
if (settled)
|
|
1653
|
+
return;
|
|
1654
|
+
settled = true;
|
|
1655
|
+
clearTimeout(timer);
|
|
1656
|
+
resolve(result);
|
|
1657
|
+
};
|
|
1658
|
+
const timer = setTimeout(() => finish({ status: "rejected", reason: new Error(`stop deadline exceeded waiting for ${label}`) }), Math.max(0, deadline - Date.now()));
|
|
1659
|
+
Promise.resolve().then(wait).then((value) => finish({ status: "fulfilled", value }), (reason) => finish({ status: "rejected", reason }));
|
|
1660
|
+
});
|
|
1232
1661
|
// Stop accepting external work immediately, but do not await listener
|
|
1233
1662
|
// closure before cancelling active workers: an SSE connection may itself be
|
|
1234
1663
|
// waiting for the worker cancellation that follows.
|
|
@@ -1253,27 +1682,35 @@ export default class Daemon {
|
|
|
1253
1682
|
this.#engine.cancelDerivations(derivationAbort);
|
|
1254
1683
|
this.#branchBatches.beginStop();
|
|
1255
1684
|
this.#drains.beginStop("daemon_stopping");
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1685
|
+
// {§crash-only-stop} — the settle sequence is DEADLINE-BOUNDED: a child
|
|
1686
|
+
// that never closes (a wedged MCP server, a stuck stream) must not hang
|
|
1687
|
+
// the daemon forever. Past the deadline the waits are abandoned; the
|
|
1688
|
+
// process may exit with the WAL in place (SQLite recovers) rather than
|
|
1689
|
+
// leak as a live-but-wedged tree.
|
|
1690
|
+
const branchResult = await settle("branch batches idle", () => this.#branchBatches.idle());
|
|
1691
|
+
const drainResult = await settle("drains idle", () => this.#drains.idle());
|
|
1692
|
+
const moduleResult = await settle("modules close", async () => {
|
|
1693
|
+
const results = await moduleClose;
|
|
1694
|
+
if (results.some((r) => r.status === "rejected"))
|
|
1695
|
+
throw new AggregateError(results.filter((r) => r.status === "rejected").map((r) => r.reason), "module close failed");
|
|
1696
|
+
});
|
|
1697
|
+
const streamingResult = await settle("streaming schemes idle", () => this.#drainStreamingSchemes());
|
|
1698
|
+
const derivationResult = await settle("derivation drain", () => this.#engine.drainDerivations(derivationAbort));
|
|
1699
|
+
const mimetypeResult = this.#ownsMimetypes
|
|
1700
|
+
? await settle("mimetypes dispose", () => this.#mimetypes.dispose())
|
|
1701
|
+
: null;
|
|
1702
|
+
const schemeResult = await settle("schemes close", () => this.#schemes.close());
|
|
1267
1703
|
// Streaming and scheme closure are the last producers of synchronous
|
|
1268
1704
|
// conclusion notifications. Join the supervisor-owned async tails only
|
|
1269
1705
|
// after those producers settle, before the caller may close SQLite.
|
|
1270
|
-
const
|
|
1706
|
+
const wakeResult = await settle("drains idle (wake)", () => this.#drains.idle());
|
|
1271
1707
|
const closeErrors = [
|
|
1272
|
-
|
|
1708
|
+
moduleResult,
|
|
1709
|
+
branchResult,
|
|
1273
1710
|
drainResult,
|
|
1274
1711
|
streamingResult,
|
|
1275
1712
|
derivationResult,
|
|
1276
|
-
...
|
|
1713
|
+
...(mimetypeResult === null ? [] : [mimetypeResult]),
|
|
1277
1714
|
schemeResult,
|
|
1278
1715
|
wakeResult,
|
|
1279
1716
|
]
|
|
@@ -1345,8 +1782,9 @@ export default class Daemon {
|
|
|
1345
1782
|
sequence: seqRow.next,
|
|
1346
1783
|
prompt: first.body,
|
|
1347
1784
|
flags: first.flags,
|
|
1348
|
-
|
|
1349
|
-
|
|
1785
|
+
model_route_id: first.model_route_id,
|
|
1786
|
+
spawn_model_route_id: first.spawn_model_route_id,
|
|
1787
|
+
reasoning_policy: first.reasoning_policy,
|
|
1350
1788
|
max_turns: first.max_turns,
|
|
1351
1789
|
open_paths: first.open_paths ?? "[]",
|
|
1352
1790
|
orphan_source_loop_id: endedLoopId,
|