@plurnk/plurnk-service 3.3.0 → 4.1.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 +4 -4
- package/INSTALL.md +6 -0
- package/README.md +2 -2
- package/SPEC.md +408 -179
- package/dist/build-info.json +1 -1
- package/dist/completions/_plurnk-service +1 -1
- package/dist/completions/plurnk-service.bash +1 -1
- package/dist/completions/plurnk-service.fish +2 -0
- package/dist/content/read-projector.js +1 -1
- package/dist/content/read-projector.js.map +1 -1
- package/dist/core/AdministrativeLoop.d.ts +1 -1
- package/dist/core/AdministrativeLoop.d.ts.map +1 -1
- package/dist/core/AdministrativeLoop.js +4 -1
- package/dist/core/AdministrativeLoop.js.map +1 -1
- package/dist/core/AdmittedTurnExecutor.d.ts +3 -1
- package/dist/core/AdmittedTurnExecutor.d.ts.map +1 -1
- package/dist/core/AdmittedTurnExecutor.js +12 -0
- package/dist/core/AdmittedTurnExecutor.js.map +1 -1
- package/dist/core/BareBatchRunner.js +2 -1
- package/dist/core/BareBatchRunner.js.map +1 -1
- package/dist/core/ChannelWrite.d.ts +10 -0
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +10 -0
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +14 -2
- package/dist/core/ClientInteractions.d.ts.map +1 -1
- package/dist/core/ClientInteractions.js +2 -1
- package/dist/core/ClientInteractions.js.map +1 -1
- package/dist/core/CoreSchemeServices.d.ts.map +1 -1
- package/dist/core/CoreSchemeServices.js +5 -0
- package/dist/core/CoreSchemeServices.js.map +1 -1
- package/dist/core/DataStatementRunner.js +4 -6
- package/dist/core/DataStatementRunner.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +4 -7
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +48 -68
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/Dispatcher.sql +2 -2
- package/dist/core/EditMutations.js +1 -0
- package/dist/core/EditMutations.js.map +1 -1
- package/dist/core/Engine.d.ts +8 -14
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +22 -24
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +6 -6
- package/dist/core/ExecutorRegistry.d.ts +0 -5
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +10 -18
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/FileAddress.js +1 -1
- package/dist/core/FileAddress.js.map +1 -1
- package/dist/core/FilePublication.js +10 -74
- package/dist/core/FilePublication.js.map +1 -1
- package/dist/core/FileSnapshot.js +36 -100
- package/dist/core/FileSnapshot.js.map +1 -1
- package/dist/core/HostPaths.d.ts +1 -0
- package/dist/core/HostPaths.d.ts.map +1 -1
- package/dist/core/HostPaths.js +11 -1
- package/dist/core/HostPaths.js.map +1 -1
- package/dist/core/KillHandler.js +3 -1
- package/dist/core/KillHandler.js.map +1 -1
- package/dist/core/LiveSubscription.js +1 -1
- package/dist/core/LiveSubscription.js.map +1 -1
- package/dist/core/LogBody.js +1 -1
- package/dist/core/LogBody.js.map +1 -1
- package/dist/core/LogWriter.d.ts +4 -2
- package/dist/core/LogWriter.d.ts.map +1 -1
- package/dist/core/LogWriter.js +8 -4
- package/dist/core/LogWriter.js.map +1 -1
- package/dist/core/LoopDriver.d.ts +3 -3
- package/dist/core/LoopDriver.d.ts.map +1 -1
- package/dist/core/LoopDriver.js +15 -13
- package/dist/core/LoopDriver.js.map +1 -1
- package/dist/core/LoopLifecycle.d.ts +5 -0
- package/dist/core/LoopLifecycle.d.ts.map +1 -1
- package/dist/core/LoopLifecycle.js +14 -1
- package/dist/core/LoopLifecycle.js.map +1 -1
- package/dist/core/LoopLifecycle.sql +8 -0
- package/dist/core/MembershipMaterialization.js +2 -2
- package/dist/core/MembershipMaterialization.js.map +1 -1
- package/dist/core/MessageResources.d.ts +3 -0
- package/dist/core/MessageResources.d.ts.map +1 -1
- package/dist/core/MessageResources.js +15 -1
- package/dist/core/MessageResources.js.map +1 -1
- package/dist/core/OperationHistory.d.ts +18 -0
- package/dist/core/OperationHistory.d.ts.map +1 -0
- package/dist/core/OperationHistory.js +55 -0
- package/dist/core/OperationHistory.js.map +1 -0
- package/dist/core/OperatorConfig.d.ts +1 -2
- package/dist/core/OperatorConfig.d.ts.map +1 -1
- package/dist/core/OperatorConfig.js +26 -92
- package/dist/core/OperatorConfig.js.map +1 -1
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +22 -56
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/PacketBuilder.sql +7 -8
- package/dist/core/ProblemLog.d.ts +1 -0
- package/dist/core/ProblemLog.d.ts.map +1 -1
- package/dist/core/ProblemLog.js +2 -2
- package/dist/core/ProblemLog.js.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +13 -8
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProposalPolicies.d.ts +4 -1
- package/dist/core/ProposalPolicies.d.ts.map +1 -1
- package/dist/core/ProposalPolicies.js +5 -2
- package/dist/core/ProposalPolicies.js.map +1 -1
- package/dist/core/ResourceMutations.js +2 -0
- package/dist/core/ResourceMutations.js.map +1 -1
- package/dist/core/ResourceSelector.js +1 -1
- package/dist/core/ResourceSelector.js.map +1 -1
- package/dist/core/SchemeRegistry.js +1 -1
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/ServiceHelp.js +1 -1
- package/dist/core/ServiceHelp.js.map +1 -1
- package/dist/core/ServiceTeardown.js +9 -12
- package/dist/core/ServiceTeardown.js.map +1 -1
- package/dist/core/StrikeRail.d.ts +2 -1
- package/dist/core/StrikeRail.d.ts.map +1 -1
- package/dist/core/StrikeRail.js +11 -6
- package/dist/core/StrikeRail.js.map +1 -1
- package/dist/core/TerminalResult.js +1 -1
- package/dist/core/TerminalResult.js.map +1 -1
- package/dist/core/TurnDispositionHandler.js +3 -2
- package/dist/core/TurnDispositionHandler.js.map +1 -1
- package/dist/core/TurnMaterialization.js +6 -4
- package/dist/core/TurnMaterialization.js.map +1 -1
- package/dist/core/TurnMaterialization.sql +1 -1
- package/dist/core/TurnOps.d.ts +0 -3
- package/dist/core/TurnOps.d.ts.map +1 -1
- package/dist/core/TurnOps.js +1 -27
- package/dist/core/TurnOps.js.map +1 -1
- package/dist/core/TurnRunner.d.ts +4 -2
- package/dist/core/TurnRunner.d.ts.map +1 -1
- package/dist/core/TurnRunner.js +61 -61
- package/dist/core/TurnRunner.js.map +1 -1
- package/dist/core/WorkerControlHandler.d.ts +2 -0
- package/dist/core/WorkerControlHandler.d.ts.map +1 -1
- package/dist/core/WorkerControlHandler.js +48 -16
- package/dist/core/WorkerControlHandler.js.map +1 -1
- package/dist/core/WorkerName.d.ts +0 -4
- package/dist/core/WorkerName.d.ts.map +1 -1
- package/dist/core/WorkerName.js +5 -30
- package/dist/core/WorkerName.js.map +1 -1
- package/dist/core/WorkerName.sql +4 -20
- package/dist/core/WorkerOwners.d.ts +1 -0
- package/dist/core/WorkerOwners.d.ts.map +1 -1
- package/dist/core/WorkerOwners.js +6 -0
- package/dist/core/WorkerOwners.js.map +1 -1
- package/dist/core/WorkerOwners.sql +7 -0
- package/dist/core/WorkspaceGate.d.ts +3 -1
- package/dist/core/WorkspaceGate.d.ts.map +1 -1
- package/dist/core/WorkspaceGate.js +59 -10
- package/dist/core/WorkspaceGate.js.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts +2 -0
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +12 -0
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/env-catalog.js +1 -1
- package/dist/core/env-catalog.js.map +1 -1
- package/dist/core/fork.js +1 -1
- package/dist/core/fork.js.map +1 -1
- package/dist/core/fork.sql +5 -4
- package/dist/core/mutation-types.js +4 -1
- package/dist/core/mutation-types.js.map +1 -1
- package/dist/core/namespace.js +1 -1
- package/dist/core/namespace.js.map +1 -1
- package/dist/core/packet-wire.d.ts +7 -5
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +38 -20
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/scheme-types.d.ts +4 -0
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/turn-signals.d.ts +4 -0
- package/dist/core/turn-signals.d.ts.map +1 -1
- package/dist/core/turn-signals.js +1 -0
- package/dist/core/turn-signals.js.map +1 -1
- package/dist/core/workspace-settings.js +1 -1
- package/dist/core/workspace-settings.js.map +1 -1
- package/dist/evidence/EvidenceReader.js +5 -5
- package/dist/evidence/EvidenceReader.js.map +1 -1
- package/dist/man/plurnk-service.1 +27 -6
- package/dist/schemes/Env.d.ts +7 -3
- package/dist/schemes/Env.d.ts.map +1 -1
- package/dist/schemes/Env.js +12 -6
- package/dist/schemes/Env.js.map +1 -1
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +33 -25
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +3 -3
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/ExecutionInput.js +1 -1
- package/dist/schemes/ExecutionInput.js.map +1 -1
- package/dist/schemes/File.js +15 -18
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +21 -36
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +0 -9
- package/dist/schemes/ResourceTreeScheme.js +2 -5
- package/dist/schemes/ResourceTreeScheme.js.map +1 -1
- package/dist/schemes/TurnSource.js +1 -1
- package/dist/schemes/TurnSource.js.map +1 -1
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +10 -3
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +2 -4
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +5 -3
- package/dist/schemes/_entry-find.js +3 -3
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +3 -3
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-manifest.sql +2 -0
- package/dist/schemes/_entry-ops.js +1 -1
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-send.js +1 -1
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/server/ClientReads.d.ts +1 -0
- package/dist/server/ClientReads.d.ts.map +1 -1
- package/dist/server/ClientReads.js +7 -2
- package/dist/server/ClientReads.js.map +1 -1
- package/dist/server/Credentials.d.ts +48 -0
- package/dist/server/Credentials.d.ts.map +1 -0
- package/dist/server/Credentials.js +203 -0
- package/dist/server/Credentials.js.map +1 -0
- package/dist/server/Daemon.d.ts +40 -41
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +272 -206
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DrainSupervisor.d.ts +5 -2
- package/dist/server/DrainSupervisor.d.ts.map +1 -1
- package/dist/server/DrainSupervisor.js +27 -31
- package/dist/server/DrainSupervisor.js.map +1 -1
- package/dist/server/EnvFunctionality.d.ts.map +1 -1
- package/dist/server/EnvFunctionality.js +2 -1
- package/dist/server/EnvFunctionality.js.map +1 -1
- package/dist/server/Functionality.d.ts +4 -2
- package/dist/server/Functionality.d.ts.map +1 -1
- package/dist/server/Functionality.js +110 -51
- package/dist/server/Functionality.js.map +1 -1
- package/dist/server/FunctionalityManager.js +1 -1
- package/dist/server/FunctionalityManager.js.map +1 -1
- package/dist/server/HttpListener.d.ts +1 -1
- package/dist/server/HttpListener.d.ts.map +1 -1
- package/dist/server/HttpListener.js.map +1 -1
- package/dist/server/MembersFunctionality.d.ts.map +1 -1
- package/dist/server/MembersFunctionality.js +4 -2
- package/dist/server/MembersFunctionality.js.map +1 -1
- package/dist/server/ModuleHost.d.ts +2 -1
- package/dist/server/ModuleHost.d.ts.map +1 -1
- package/dist/server/PlurnkSkill.js +4 -7
- package/dist/server/PlurnkSkill.js.map +1 -1
- package/dist/server/Rollback.js +1 -1
- package/dist/server/Rollback.js.map +1 -1
- package/dist/server/WorkerModelResolver.js +1 -0
- package/dist/server/WorkerModelResolver.js.map +1 -1
- package/dist/server/WorkerReferences.d.ts +14 -0
- package/dist/server/WorkerReferences.d.ts.map +1 -0
- package/dist/server/WorkerReferences.js +24 -0
- package/dist/server/WorkerReferences.js.map +1 -0
- package/dist/server/WorkerReferences.sql +31 -0
- package/dist/server/WorkspaceCapabilities.d.ts +2 -0
- package/dist/server/WorkspaceCapabilities.d.ts.map +1 -1
- package/dist/server/WorkspaceCapabilities.js +35 -18
- package/dist/server/WorkspaceCapabilities.js.map +1 -1
- package/dist/server/WorkspacePlugins.js +1 -0
- package/dist/server/WorkspacePlugins.js.map +1 -1
- package/dist/server/WorkspaceResidency.d.ts +3 -1
- package/dist/server/WorkspaceResidency.d.ts.map +1 -1
- package/dist/server/WorkspaceResidency.js +12 -5
- package/dist/server/WorkspaceResidency.js.map +1 -1
- package/dist/server/client-input.d.ts +2 -0
- package/dist/server/client-input.d.ts.map +1 -1
- package/dist/server/client-input.js +28 -0
- package/dist/server/client-input.js.map +1 -1
- package/dist/server/drain.sql +37 -40
- package/dist/server/envelope.d.ts +5 -9
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +21 -84
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +17 -17
- package/dist/server/lifecycle-recovery.sql +5 -15
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +6 -0
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/logEntry.sql +94 -6
- package/dist/server/module-discovery.d.ts +1 -1
- package/dist/server/module-discovery.d.ts.map +1 -1
- package/dist/server/module-discovery.js +1 -0
- package/dist/server/module-discovery.js.map +1 -1
- package/dist/server/seam-loop.sql +15 -7
- package/dist/server/seam-proposal-list.sql +10 -4
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +7 -1
- package/dist/service.js.map +1 -1
- package/docs/members.md +6 -4
- package/migrations/002_workers.sql +2 -2
- package/migrations/006_log.sql +1 -1
- package/migrations/018_worker_owners.sql +1 -1
- package/migrations/029_default_worker.sql +9 -0
- package/migrations/030_client_workers.sql +105 -0
- package/migrations/031_message_openers.sql +83 -0
- package/migrations/032_worker_references.sql +29 -0
- package/package.json +23 -22
- package/plurnk.nginx.conf +71 -0
- package/plurnk.service +2 -0
package/dist/server/Daemon.js
CHANGED
|
@@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises";
|
|
|
4
4
|
import { resolve, dirname } from "node:path";
|
|
5
5
|
import { Share } from "@plurnk/plurnk-digest";
|
|
6
6
|
import EvidenceReader from "@plurnk/plurnk-service/evidence";
|
|
7
|
+
import ChannelWrite, {} from "../core/ChannelWrite.js";
|
|
7
8
|
import Paths from "../Paths.js";
|
|
8
9
|
import Engine from "../core/Engine.js";
|
|
9
10
|
import ExecutorRegistry from "../core/ExecutorRegistry.js";
|
|
@@ -15,14 +16,16 @@ import { projectModelRoute, routeForSpec, specForRoute } from "./model-route.js"
|
|
|
15
16
|
import { discoverDaemonModules } from "./module-discovery.js";
|
|
16
17
|
import EffectPolicy from "../schemes/EffectPolicy.js";
|
|
17
18
|
import QuestionTool, { questionRuntimeDecl } from "../schemes/QuestionTool.js";
|
|
18
|
-
import DrainSupervisor from "./DrainSupervisor.js";
|
|
19
|
+
import DrainSupervisor, {} from "./DrainSupervisor.js";
|
|
19
20
|
import MessageAbandonment from "./MessageAbandonment.js";
|
|
20
21
|
import Retention, { retentionPolicy } from "./Retention.js";
|
|
21
22
|
import TurnDispositionHandler from "../core/TurnDispositionHandler.js";
|
|
22
23
|
import { Validator } from "@plurnk/plurnk-contracts";
|
|
23
24
|
import LogEntry from "./logEntry.js";
|
|
24
25
|
import HttpListener from "./HttpListener.js";
|
|
26
|
+
import Credentials from "./Credentials.js";
|
|
25
27
|
import Envelope, { projectWorkerRow } from "./envelope.js";
|
|
28
|
+
import WorkerReferences from "./WorkerReferences.js";
|
|
26
29
|
import ClientInput from "./client-input.js";
|
|
27
30
|
import Turn from "../core/Turn.js";
|
|
28
31
|
import WorkspacePlugins from "./WorkspacePlugins.js";
|
|
@@ -60,6 +63,13 @@ import { daemonFailure, modelRouteLabel } from "./daemon-results.js";
|
|
|
60
63
|
import WorkerModelResolver from "./WorkerModelResolver.js";
|
|
61
64
|
import ClientReads from "./ClientReads.js";
|
|
62
65
|
import ConfigurationDiagnostics from "./ConfigurationDiagnostics.js";
|
|
66
|
+
// {§connection-lifecycle} A client operation cancelled mid-way settles its loop with what happened.
|
|
67
|
+
const clientOperationCancelled = (reason) => {
|
|
68
|
+
const said = typeof reason === "string" ? reason : "cancelled";
|
|
69
|
+
return Results.failure("daemon:client", "operation-cancelled", 499, `The client operation was cancelled: ${said}.`, {}, {
|
|
70
|
+
reason: said, stage: "client-action", retryable: false,
|
|
71
|
+
});
|
|
72
|
+
};
|
|
63
73
|
const clientActionFailure = (error) => {
|
|
64
74
|
if (error instanceof OperationFailureError)
|
|
65
75
|
return error.result;
|
|
@@ -71,6 +81,7 @@ const clientActionFailure = (error) => {
|
|
|
71
81
|
};
|
|
72
82
|
export default class Daemon {
|
|
73
83
|
static validateConfiguration() {
|
|
84
|
+
Credentials.posture();
|
|
74
85
|
FileCreationPolicy.serviceScope();
|
|
75
86
|
EffectPolicy.validateConfiguration();
|
|
76
87
|
ProposalPolicies.read();
|
|
@@ -99,6 +110,13 @@ export default class Daemon {
|
|
|
99
110
|
// {§http-host} — the one listener, bound by the service before durable admission; null in
|
|
100
111
|
// tests that never open a transport.
|
|
101
112
|
#http;
|
|
113
|
+
// {§credential-authority} {§methods-credentials} — the one authority over credentials presented to
|
|
114
|
+
// the listener, holding the operator's secret under the posture `PLURNK_AUTH` names.
|
|
115
|
+
credentials;
|
|
116
|
+
// The file beside the database where a minted operator secret is written for same-user clients;
|
|
117
|
+
// null for a daemon started without its database path.
|
|
118
|
+
#secretPath;
|
|
119
|
+
#secretMinted;
|
|
102
120
|
#discoveryCwd;
|
|
103
121
|
#started = false; // {§module-lifecycle}: one discovery/module boot; no listener
|
|
104
122
|
#modules = [];
|
|
@@ -107,6 +125,11 @@ export default class Daemon {
|
|
|
107
125
|
#mounted = new Set();
|
|
108
126
|
#moduleLifetimes = [];
|
|
109
127
|
#moduleActions = new Map();
|
|
128
|
+
// {§connection-lifecycle} Each running client operation's loop, its own cancellation scope and its settlement.
|
|
129
|
+
#clientLoops = new Map();
|
|
130
|
+
// {§worker-lifecycle-total-reap} Every client operation's scope descends from this one, which shutdown aborts:
|
|
131
|
+
// an operation still dispatching, and every execution an operation started, ends with the daemon.
|
|
132
|
+
#clientOperations = new AbortController();
|
|
110
133
|
#residency;
|
|
111
134
|
#functionality;
|
|
112
135
|
// {§retention-policy} — the operator's retention, run on a cadence and at shutdown.
|
|
@@ -127,6 +150,10 @@ export default class Daemon {
|
|
|
127
150
|
this.#dbPath = dbPath;
|
|
128
151
|
this.#http = http;
|
|
129
152
|
this.#configuration = configuration;
|
|
153
|
+
const posture = Credentials.posture();
|
|
154
|
+
this.credentials = new Credentials({ operatorSecret: posture.secret });
|
|
155
|
+
this.#secretPath = dbPath === undefined ? null : Credentials.secretPath(dbPath);
|
|
156
|
+
this.#secretMinted = posture.minted;
|
|
130
157
|
this.#storage = new WorkspaceStorage(db, hostPaths);
|
|
131
158
|
this.#lifecycle = new LoopLifecycle(db);
|
|
132
159
|
this.#schemes = schemes ?? new SchemeRegistry();
|
|
@@ -165,13 +192,15 @@ export default class Daemon {
|
|
|
165
192
|
// {§functionality-coordinator} — the one owner above every family
|
|
166
193
|
// adapter; Core hosts it, modules register adapters through the seam.
|
|
167
194
|
this.#functionality = new Functionality({
|
|
195
|
+
signal: this.#residency.signal,
|
|
168
196
|
registerModuleAction: (registration) => this.registerModuleAction(registration),
|
|
169
197
|
registerWorkspaceCapabilityProvider: (owner, provider) => this.registerWorkspaceCapabilityProvider(owner, provider),
|
|
170
198
|
readWorkspaceModuleState: (workspaceId, owner) => this.readWorkspaceModuleState(workspaceId, owner),
|
|
171
199
|
replaceWorkspaceCapabilities: (replacement, options) => this.replaceWorkspaceCapabilities(replacement, options),
|
|
172
200
|
readWorkerModuleState: (workerId, owner) => this.#residency.readWorkerModuleState(workerId, owner),
|
|
173
201
|
replaceWorkerModuleState: (workerId, owner, state) => this.#residency.replaceWorkerModuleState(workerId, owner, state),
|
|
174
|
-
withWorkspaceGate: (workspaceId, owner, gate, run) => this.#residency.exclusively(workspaceId, owner, gate, run),
|
|
202
|
+
withWorkspaceGate: (workspaceId, owner, gate, run, signal) => this.#residency.exclusively(workspaceId, owner, gate, run, signal),
|
|
203
|
+
atWorkspaceBoundary: (workspaceId, run) => this.#workspaceGate.atBoundary(workspaceId, run),
|
|
175
204
|
retainWorkspace: (workspaceId) => this.#residency.retain(workspaceId),
|
|
176
205
|
preparationChanged: (workspaceId, preparation) => this.#broadcast({ workspaceId }, "workspace/preparation", { workspaceId, preparation })
|
|
177
206
|
});
|
|
@@ -181,8 +210,10 @@ export default class Daemon {
|
|
|
181
210
|
this.#functionality.register(this.#members);
|
|
182
211
|
// {§env-functionality} — Core's own family: the environment a Worker's commands run in.
|
|
183
212
|
// {§env-scheme} — the family's face: reads project the dispatching Worker's effective
|
|
184
|
-
// environment, writes go through the family's own admission at the Worker's layer.
|
|
185
|
-
|
|
213
|
+
// environment, writes go through the family's own admission at the Worker's layer. The runtime
|
|
214
|
+
// worker keeps none: its environment is the workspace's ({§actor-boundary-client-operations}).
|
|
215
|
+
this.#functionality.register(new EnvFunctionality(EnvFunctionality.defaultsReader(Paths.packageRoot, this.#nodeModulesPath), new EnvScheme((workspaceId, workerId) => EnvFunctionality.resolve(this.#db, workspaceId, workerId), (verb, params, identity) => this.#functionality.invoke("env", verb, params, identity, "operation"), async (workspaceId, workerId) => (await this.#db.runtime_worker_get.get({ workspace_id: workspaceId }))?.id === workerId
|
|
216
|
+
? "workspace" : "worker")));
|
|
186
217
|
this.#engine = new Engine({
|
|
187
218
|
db, lifecycle: this.#lifecycle, schemes: this.#schemes, mimetypes: this.#mimetypes,
|
|
188
219
|
// {§tokenomics-agnostic-ruler} — stored and catalog curation weights
|
|
@@ -200,13 +231,13 @@ export default class Daemon {
|
|
|
200
231
|
// daemon owns provider + the law-file system prompt; the worker scheme
|
|
201
232
|
// handler carries neither. Fire-and-forget: the returned drain runs
|
|
202
233
|
// independently (the sister is its own worker). {§machine-processes}
|
|
203
|
-
injectWorker: async ({ workspaceId, workerId, sourceLoopId, prompt, spawn, environment, attachments }) => {
|
|
234
|
+
injectWorker: async ({ workspaceId, workerId, sourceLoopId, prompt, spawn, environment, attachments, opener }) => {
|
|
204
235
|
await this.#assertModelWorker(workspaceId, workerId);
|
|
205
236
|
const sender = await this.#db.drain_message_source.get({ loop_id: sourceLoopId });
|
|
206
237
|
if (sender === undefined || sender.workspace_id !== workspaceId) {
|
|
207
238
|
throw new Error(`injectWorker: source loop ${sourceLoopId} does not belong to workspace ${workspaceId}`);
|
|
208
239
|
}
|
|
209
|
-
const source = await this.#
|
|
240
|
+
const source = await this.#messageSource(workspaceId, sourceLoopId, sender.worker_id, workerId);
|
|
210
241
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
211
242
|
let providerSpec;
|
|
212
243
|
let childProviderSpec;
|
|
@@ -239,14 +270,14 @@ export default class Daemon {
|
|
|
239
270
|
effort: effort,
|
|
240
271
|
effort_source: "default"
|
|
241
272
|
});
|
|
242
|
-
// {§env-option} — explicit child overrides replace copied values and enable them.
|
|
243
|
-
// Names were admitted at the operation; a refusal here is a defect and throws.
|
|
244
|
-
for (const [alias, value] of Object.entries(environment ?? {})) {
|
|
245
|
-
await this.#functionality.invoke("env", "replace", { alias, definition: { value } }, { workspaceId, workerId }, "operation");
|
|
246
|
-
await this.#functionality.invoke("env", "enable", { alias }, { workspaceId, workerId }, "operation");
|
|
247
|
-
}
|
|
248
273
|
}
|
|
249
|
-
|
|
274
|
+
// {§env-option} — a created worker's explicit overrides replace copied values and enable them.
|
|
275
|
+
// Names were admitted at the operation; a refusal here is a defect and throws.
|
|
276
|
+
for (const [alias, value] of Object.entries(environment ?? {})) {
|
|
277
|
+
await this.#functionality.invoke("env", "replace", { alias, definition: { value } }, { workspaceId, workerId }, "operation");
|
|
278
|
+
await this.#functionality.invoke("env", "enable", { alias }, { workspaceId, workerId }, "operation");
|
|
279
|
+
}
|
|
280
|
+
const { action, loopId, messageId } = await ContentUse.read(this.#db, async () => {
|
|
250
281
|
const delivered = await this.#prepareMessage(workspaceId, workerId, prompt, attachments);
|
|
251
282
|
return this.inject({
|
|
252
283
|
workspaceId,
|
|
@@ -255,15 +286,20 @@ export default class Daemon {
|
|
|
255
286
|
evidence: delivered.evidence,
|
|
256
287
|
sourceLoopId,
|
|
257
288
|
...(source === undefined ? {} : { source }),
|
|
289
|
+
...(opener === undefined ? {} : { opener }),
|
|
258
290
|
providerSpec,
|
|
259
291
|
effort,
|
|
260
292
|
childProviderSpec,
|
|
261
293
|
systemPrompt
|
|
262
294
|
});
|
|
263
295
|
});
|
|
264
|
-
|
|
296
|
+
// {§worker-obligations} — the message's durable address, as Open Work names it.
|
|
297
|
+
const message = await this.#db.drain_message_path.get({ id: messageId });
|
|
298
|
+
if (message === undefined)
|
|
299
|
+
throw new Error(`injectWorker: message ${messageId} has no address`);
|
|
300
|
+
return { action, loopId, message: message.path };
|
|
265
301
|
},
|
|
266
|
-
|
|
302
|
+
withWorkspaceTurn: (workspaceId, workerId, run, signal) => this.#workspaceGate.withTurn(workspaceId, workerId, run, signal),
|
|
267
303
|
// {§functionality-hotload} — skills and plugins change out of band.
|
|
268
304
|
// Republish under the workspace turn gate before packet assembly so
|
|
269
305
|
// the first subsequent model turn sees their exact result.
|
|
@@ -284,7 +320,7 @@ export default class Daemon {
|
|
|
284
320
|
// {§message-abandonment} — the daemon answers, 499, what an ended loop left unanswered.
|
|
285
321
|
completeAbandonedMessages: (workspaceId, workerId, loopId, status) => MessageAbandonment.complete(this.#engine, this.#db, workspaceId, workerId, loopId, status),
|
|
286
322
|
lifecycle: this.#lifecycle,
|
|
287
|
-
injectPrompt: (loopId, prompt, openPaths, source, evidence, messageAddress) => this.#engine.injectIntoLoop(loopId, prompt, openPaths, source, evidence, messageAddress),
|
|
323
|
+
injectPrompt: (loopId, prompt, openPaths, source, evidence, messageAddress, opener) => this.#engine.injectIntoLoop(loopId, prompt, openPaths, source, evidence, messageAddress, opener),
|
|
288
324
|
assertInjectionCompatibility: async ({ workerId, loopId, providerSpec, providerSpecExplicit, effort, childProviderSpec, turnCeiling }) => {
|
|
289
325
|
// An omitted selector keeps the loop's durable provider; only an
|
|
290
326
|
// explicit selection is checked against it (a deliberate switch).
|
|
@@ -420,6 +456,11 @@ export default class Daemon {
|
|
|
420
456
|
await this.#assertModelWorker(args.workspaceId, args.workerId);
|
|
421
457
|
return MessageResources.read(this.#db, args);
|
|
422
458
|
}
|
|
459
|
+
// {§message-idempotency}
|
|
460
|
+
async messageAdmission(args) {
|
|
461
|
+
const workspaceId = ClientInput.assertId("messageAdmission", "workspaceId", args.workspaceId);
|
|
462
|
+
return await MessageResources.admission(this.#db, workspaceId, args.address, args.body);
|
|
463
|
+
}
|
|
423
464
|
// {§methods-loop-run} — drive/steer a loop. The module supplies only workspace/worker/prompt;
|
|
424
465
|
// the provider and the law-file system prompt are core's and stay inside. Returns immediately — the
|
|
425
466
|
// loop runs async and its outcome arrives on the event source (loop/terminated). `cancelDrain` (public)
|
|
@@ -575,12 +616,6 @@ export default class Daemon {
|
|
|
575
616
|
});
|
|
576
617
|
}
|
|
577
618
|
}
|
|
578
|
-
// {§methods-model-worker} — the workspace's model worker (created on first use), distinct from the client
|
|
579
|
-
// worker so the model's packets never carry client-action rows. The module binds its threads to this.
|
|
580
|
-
async ensureModelWorker(workspaceId) {
|
|
581
|
-
const checked = ClientInput.assertId("worker.ensure-model", "workspaceId", workspaceId);
|
|
582
|
-
return await Envelope.ensureModelWorker(this.#db, checked);
|
|
583
|
-
}
|
|
584
619
|
async ensureRuntimeWorker(workspaceId) {
|
|
585
620
|
const checked = ClientInput.assertId("worker.ensure-runtime", "workspaceId", workspaceId);
|
|
586
621
|
if (await this.#db.envelope_get_workspace.get({ id: checked }) === undefined) {
|
|
@@ -757,7 +792,12 @@ export default class Daemon {
|
|
|
757
792
|
});
|
|
758
793
|
}
|
|
759
794
|
}
|
|
760
|
-
|
|
795
|
+
// {§actor-boundary-client-operations} A message speaks for its loop's actor: a person's operation for that
|
|
796
|
+
// person, as a thread message does; any other loop for its worker.
|
|
797
|
+
async #messageSource(workspaceId, sourceLoopId, sourceWorkerId, targetWorkerId) {
|
|
798
|
+
const person = await this.#db.envelope_client_operation_person.get({ loop_id: sourceLoopId });
|
|
799
|
+
if (person !== undefined)
|
|
800
|
+
return person.person;
|
|
761
801
|
if (sourceWorkerId === targetWorkerId)
|
|
762
802
|
return undefined;
|
|
763
803
|
const source = await this.#db.envelope_get_worker_by_id.get({ id: sourceWorkerId });
|
|
@@ -766,81 +806,93 @@ export default class Daemon {
|
|
|
766
806
|
}
|
|
767
807
|
return WorkerControlAddress.render(source.name);
|
|
768
808
|
}
|
|
769
|
-
// {§methods-op-mirror} — execute parsed ops on behalf of a client as a
|
|
770
|
-
// client-origin turn (the log is core's, a client op is a first-class citizen), dispatched through
|
|
771
|
-
// the engine, then emitted as log/entry on the event source. One seam op backs the whole op_*
|
|
772
|
-
// family (read/edit/copy/find/kill/look/move/send/exec); the module parses at its edge with the
|
|
773
|
-
// grammar package and hands over the statement, then fans the emitted entry out to its own clients.
|
|
774
809
|
// {§functionality-model-mutation} — await every queued Functionality
|
|
775
810
|
// publication (a Worker's accepted mutation publishes at its turn boundary).
|
|
776
811
|
async settleFunctionality(workspaceId) {
|
|
777
812
|
await this.#functionality.settle(workspaceId);
|
|
778
813
|
}
|
|
779
|
-
async dispatchAsClient(args) {
|
|
780
|
-
const workspaceId = ClientInput.assertId("operation.dispatch", "workspaceId", args.workspaceId);
|
|
781
|
-
const workerId = ClientInput.assertId("operation.dispatch", "workerId", args.workerId);
|
|
782
|
-
const { statement } = args;
|
|
783
|
-
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
784
|
-
const releaseCapabilities = await this.#residency.acquire(workspaceId);
|
|
785
|
-
try {
|
|
786
|
-
const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
|
|
787
|
-
try {
|
|
788
|
-
const result = await this.#dispatchClientStatement({ workspaceId, workerId, loopId: clientLoopId, statement });
|
|
789
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
|
|
790
|
-
return result;
|
|
791
|
-
}
|
|
792
|
-
catch (error) {
|
|
793
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
|
|
794
|
-
throw error;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
finally {
|
|
798
|
-
releaseCapabilities();
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
// The client-interface action contract: one AG-UI action owns one administrative
|
|
802
|
-
// loop, regardless of how many statements op.parse produced. Each statement is one
|
|
803
|
-
// ordinary operation turn; a proposal may keep that turn and loop open across
|
|
804
|
-
// interrupt/resume until settlement.
|
|
805
814
|
executorTags(workspaceId) {
|
|
806
815
|
return this.#engine.executorTags(workspaceId);
|
|
807
816
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
817
|
+
// {§connection-lifecycle} A person's client operation: one administrative loop in the workspace's runtime
|
|
818
|
+
// worker, attributed to its actor, reading `log:///` in the worker that person acts for
|
|
819
|
+
// ({§actor-boundary-client-operations}).
|
|
811
820
|
async dispatchClientAction(args) {
|
|
812
821
|
const workspaceId = ClientInput.assertId("operation.dispatch-batch", "workspaceId", args.workspaceId);
|
|
813
|
-
const
|
|
822
|
+
const actor = ClientInput.assertActor("operation.dispatch-batch", args.actor);
|
|
823
|
+
const contextWorkerId = args.contextWorkerId === undefined
|
|
824
|
+
? undefined
|
|
825
|
+
: ClientInput.assertId("operation.dispatch-batch", "contextWorkerId", args.contextWorkerId);
|
|
826
|
+
if (contextWorkerId !== undefined)
|
|
827
|
+
await this.#assertModelWorker(workspaceId, contextWorkerId);
|
|
814
828
|
const { statements } = args;
|
|
815
829
|
if (statements.length === 0)
|
|
816
830
|
return [];
|
|
817
|
-
await this.#
|
|
818
|
-
|
|
819
|
-
|
|
831
|
+
return await this.#clientOperation({ workspaceId, actor, contextWorkerId, onLoop: args.onLoop }, async (dispatch) => {
|
|
832
|
+
const results = [];
|
|
833
|
+
for (const statement of statements)
|
|
834
|
+
results.push(await dispatch(statement));
|
|
835
|
+
return results;
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
// {§connection-lifecycle} One client operation: the runtime worker's administrative loop with its own
|
|
839
|
+
// cancellation scope, settled when the operation ends. Nothing else in the runtime worker shares the scope.
|
|
840
|
+
async #clientOperation(operation, body) {
|
|
841
|
+
const { workspaceId, actor, contextWorkerId } = operation;
|
|
842
|
+
const workerId = await RuntimeWorker.ensure(this.#db, workspaceId);
|
|
820
843
|
const releaseCapabilities = await this.#residency.acquire(workspaceId);
|
|
821
844
|
try {
|
|
822
|
-
const
|
|
845
|
+
const loopId = await Envelope.ensureClientLoop(this.#db, workerId, actor);
|
|
846
|
+
const scope = new AbortController();
|
|
847
|
+
const signal = AbortSignal.any([scope.signal, this.#clientOperations.signal]);
|
|
848
|
+
const settled = Promise.withResolvers();
|
|
849
|
+
this.#clientLoops.set(loopId, { workspaceId, scope, settled: settled.promise });
|
|
850
|
+
const unbind = this.#engine.bindLoopSignal(loopId, signal);
|
|
823
851
|
try {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
852
|
+
operation.onLoop?.({ workerId, loopId });
|
|
853
|
+
const result = await body(async (statement) => {
|
|
854
|
+
signal.throwIfAborted();
|
|
855
|
+
return await this.#dispatchClientStatement({
|
|
856
|
+
workspaceId, workerId, loopId, statement,
|
|
857
|
+
contextWorkerId: contextWorkerId ?? null,
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
signal.throwIfAborted();
|
|
861
|
+
await Envelope.closeClientLoop(this.#db, loopId, { status: 200 });
|
|
862
|
+
return result;
|
|
830
863
|
}
|
|
831
864
|
catch (error) {
|
|
832
|
-
|
|
833
|
-
|
|
865
|
+
// A cancelled operation settles cancelled, however its statement in flight ended.
|
|
866
|
+
const failure = signal.aborted ? new OperationFailureError(clientOperationCancelled(signal.reason)) : error;
|
|
867
|
+
await Envelope.closeClientLoop(this.#db, loopId, clientActionFailure(failure));
|
|
868
|
+
throw failure;
|
|
869
|
+
}
|
|
870
|
+
finally {
|
|
871
|
+
unbind();
|
|
872
|
+
this.#clientLoops.delete(loopId);
|
|
873
|
+
settled.resolve();
|
|
834
874
|
}
|
|
835
875
|
}
|
|
836
876
|
finally {
|
|
837
877
|
releaseCapabilities();
|
|
838
878
|
}
|
|
839
879
|
}
|
|
880
|
+
// {§connection-lifecycle} Cancel exactly one client operation: its dispatch while it runs, and every execution its
|
|
881
|
+
// rows started that still runs. The rest of the runtime worker runs on.
|
|
882
|
+
async cancelLoop(args) {
|
|
883
|
+
const workspaceId = ClientInput.assertId("operation.cancel", "workspaceId", args.workspaceId);
|
|
884
|
+
const loopId = ClientInput.assertId("operation.cancel", "loopId", args.loopId);
|
|
885
|
+
const operation = this.#clientLoops.get(loopId);
|
|
886
|
+
const dispatching = operation !== undefined && operation.workspaceId === workspaceId && !operation.scope.signal.aborted;
|
|
887
|
+
if (dispatching)
|
|
888
|
+
operation.scope.abort(args.reason ?? "user_cancelled");
|
|
889
|
+
const executions = await ChannelWrite.findOpenSubscriptionsForClientLoop(this.#db, { workspaceId, loopId });
|
|
890
|
+
const reaped = await Promise.all(executions.map((subscriptionId) => this.#engine.cancelSubscription(subscriptionId)));
|
|
891
|
+
return dispatching || reaped.includes(true);
|
|
892
|
+
}
|
|
840
893
|
async #dispatchClientStatement(args) {
|
|
841
|
-
const { workspaceId, workerId, loopId, statement } = args;
|
|
842
|
-
|
|
843
|
-
try {
|
|
894
|
+
const { workspaceId, workerId, loopId, statement, contextWorkerId } = args;
|
|
895
|
+
return this.#workspaceGate.withTurn(workspaceId, workerId, async () => {
|
|
844
896
|
await this.#residency.reconcile(workspaceId);
|
|
845
897
|
const { id: turnId } = await Turn.open(this.#db, {
|
|
846
898
|
loopId,
|
|
@@ -852,7 +904,8 @@ export default class Daemon {
|
|
|
852
904
|
const entryIds = [];
|
|
853
905
|
const result = await this.#engine.dispatch({
|
|
854
906
|
statement, workspaceId, workerId, loopId, turnId, sequence: 1,
|
|
855
|
-
origin: "client",
|
|
907
|
+
origin: "client", client: { contextWorkerId },
|
|
908
|
+
onDispatch: (logEntryId) => { entryIds.push(logEntryId); }
|
|
856
909
|
});
|
|
857
910
|
await Turn.complete(this.#db, turnId, result.status);
|
|
858
911
|
turnOpen = false;
|
|
@@ -873,51 +926,7 @@ export default class Daemon {
|
|
|
873
926
|
}
|
|
874
927
|
throw cause;
|
|
875
928
|
}
|
|
876
|
-
}
|
|
877
|
-
finally {
|
|
878
|
-
release();
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
// {§op-look} — the pure READ-projection query on the seam: resolve a READ through the
|
|
882
|
-
// full scheme resolver and return its content, writing NO log row — the client's out-of-band
|
|
883
|
-
// inspection primitive (the module rewrites LOOK→READ and parses at its edge, exactly like
|
|
884
|
-
// dispatchClientAction). Its closed observation segment supplies the numeric loop coordinate
|
|
885
|
-
// required by extension context and relative log:/// addresses without impersonating an active
|
|
886
|
-
// client lifecycle. It creates no turn or log row. Engine.look enforces READ-only.
|
|
887
|
-
// {§op-look} — the segment is the acting worker's; the READ resolves as the perspective worker.
|
|
888
|
-
async look(args) {
|
|
889
|
-
const workspaceId = ClientInput.assertId("operation.look", "workspaceId", args.workspaceId);
|
|
890
|
-
const workerId = ClientInput.assertId("operation.look", "workerId", args.workerId);
|
|
891
|
-
const perspective = args.perspectiveWorkerId === undefined
|
|
892
|
-
? workerId
|
|
893
|
-
: ClientInput.assertId("operation.look", "perspectiveWorkerId", args.perspectiveWorkerId);
|
|
894
|
-
const { statement } = args;
|
|
895
|
-
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
896
|
-
if (perspective !== workerId)
|
|
897
|
-
await this.#assertWorkerOwned(workspaceId, perspective);
|
|
898
|
-
const releaseCapabilities = await this.#residency.acquire(workspaceId);
|
|
899
|
-
try {
|
|
900
|
-
const releaseWorkspace = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
|
|
901
|
-
try {
|
|
902
|
-
await this.#residency.reconcile(workspaceId);
|
|
903
|
-
const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
|
|
904
|
-
try {
|
|
905
|
-
const result = await this.#engine.look({ statement, workspaceId, workerId: perspective, loopId: clientLoopId });
|
|
906
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
|
|
907
|
-
return result;
|
|
908
|
-
}
|
|
909
|
-
catch (error) {
|
|
910
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
|
|
911
|
-
throw error;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
finally {
|
|
915
|
-
releaseWorkspace();
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
finally {
|
|
919
|
-
releaseCapabilities();
|
|
920
|
-
}
|
|
929
|
+
}, this.#clientLoops.get(loopId)?.scope.signal);
|
|
921
930
|
}
|
|
922
931
|
// {§methods} — the module's render surface beyond the journal. Thin delegations
|
|
923
932
|
// into core's envelope / membership / provider machinery; the module fans the results into its own views.
|
|
@@ -935,14 +944,23 @@ export default class Daemon {
|
|
|
935
944
|
return {
|
|
936
945
|
aliases: aliases.map((a) => {
|
|
937
946
|
const isActive = active === a.alias;
|
|
947
|
+
// {§agui-provider-routes} — the route a worker seeded from this alias gets: what it would read back.
|
|
948
|
+
let effort;
|
|
949
|
+
try {
|
|
950
|
+
effort = ProviderInstantiate.configuredEffort(a);
|
|
951
|
+
}
|
|
952
|
+
catch (cause) {
|
|
953
|
+
const invalid = new ConfigurationError("PLURNK_PROVIDERS_EFFORT", cause instanceof Error ? cause.message : String(cause), { cause });
|
|
954
|
+
throw new OperationFailureError(Results.configurationFailure(invalid), { cause });
|
|
955
|
+
}
|
|
938
956
|
return {
|
|
939
|
-
|
|
957
|
+
...projectModelRoute(a, effort, "default"), alias: a.alias, active: isActive,
|
|
940
958
|
inputCapacity: isActive && this.#provider !== null ? this.#provider.inputCapacity : null
|
|
941
959
|
};
|
|
942
960
|
})
|
|
943
961
|
};
|
|
944
962
|
}
|
|
945
|
-
// {§model-catalog} The catalog is a bounded,
|
|
963
|
+
// {§model-catalog} The catalog is a bounded, daemon-local projection.
|
|
946
964
|
// It performs no provider I/O and never changes the worker's selection.
|
|
947
965
|
listModels(query) {
|
|
948
966
|
return listModelCatalog(query);
|
|
@@ -970,7 +988,16 @@ export default class Daemon {
|
|
|
970
988
|
}));
|
|
971
989
|
return Validator.assertClientDisplayCapabilities([...schemes, ...mimetypes]);
|
|
972
990
|
}
|
|
973
|
-
listWorkspaces() {
|
|
991
|
+
async listWorkspaces() {
|
|
992
|
+
return (await Envelope.listWorkspaces(this.#db)).map((row) => ({ ...row, project_label: this.#projectLabel(row.project_root) }));
|
|
993
|
+
}
|
|
994
|
+
// {§workspace-label} — the folder as the daemon spells it for display: the spelling a default name takes.
|
|
995
|
+
#projectLabel(projectRoot) {
|
|
996
|
+
return projectRoot === null ? null : this.#hostPaths.abbreviateUserPath(projectRoot);
|
|
997
|
+
}
|
|
998
|
+
#labelled(envelope) {
|
|
999
|
+
return { ...envelope, projectLabel: this.#projectLabel(envelope.projectRoot) };
|
|
1000
|
+
}
|
|
974
1001
|
listWorkers(workspaceId, query = {}) {
|
|
975
1002
|
const origin = query.origin;
|
|
976
1003
|
if (origin !== undefined && !["model", "client", "_plurnk"].includes(origin)) {
|
|
@@ -995,26 +1022,27 @@ export default class Daemon {
|
|
|
995
1022
|
if (hasId === hasName) {
|
|
996
1023
|
throw daemonFailure("daemon:worker", "identity-invalid", 400, "Worker observation requires exactly one id or name.", { retryable: false });
|
|
997
1024
|
}
|
|
1025
|
+
// {§worker-name-minting} — a lookup is no minting door: it resolves any name a listing returns,
|
|
1026
|
+
// the runtime worker's `_plurnk` among them.
|
|
998
1027
|
const row = hasId
|
|
999
1028
|
? await this.#db.envelope_get_worker_by_id.get({
|
|
1000
1029
|
id: ClientInput.assertId("worker.read", "id", args.identity.id)
|
|
1001
1030
|
})
|
|
1002
1031
|
: await this.#db.envelope_get_worker_by_name.get({
|
|
1003
1032
|
workspace_id: workspaceId,
|
|
1004
|
-
name: ClientInput.
|
|
1033
|
+
name: ClientInput.assertOptionalName("worker.read", "name", args.identity.name)
|
|
1005
1034
|
});
|
|
1006
1035
|
if (row === undefined || row.workspace_id !== workspaceId)
|
|
1007
1036
|
return null;
|
|
1008
1037
|
const { workspace_id: _workspaceId, ...projection } = projectWorkerRow(row);
|
|
1009
1038
|
return projection;
|
|
1010
1039
|
}
|
|
1011
|
-
// {§methods-worker-
|
|
1012
|
-
async
|
|
1013
|
-
const workspaceId = ClientInput.assertId("worker.
|
|
1014
|
-
const workerId = ClientInput.assertId("worker.
|
|
1040
|
+
// {§methods-worker-accounting}
|
|
1041
|
+
async workerAccounting(args) {
|
|
1042
|
+
const workspaceId = ClientInput.assertId("worker.accounting", "workspaceId", args.workspaceId);
|
|
1043
|
+
const workerId = ClientInput.assertId("worker.accounting", "workerId", args.workerId);
|
|
1015
1044
|
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
1016
|
-
|
|
1017
|
-
return this.#engine.descendantAccounting(workerId, loopId);
|
|
1045
|
+
return this.#engine.workerAccounting(workerId);
|
|
1018
1046
|
}
|
|
1019
1047
|
async listWorkerLoops(args) {
|
|
1020
1048
|
const workspaceId = ClientInput.assertId("worker.loops", "workspaceId", args.workspaceId);
|
|
@@ -1028,20 +1056,22 @@ export default class Daemon {
|
|
|
1028
1056
|
status: row.status,
|
|
1029
1057
|
prompt: row.prompt,
|
|
1030
1058
|
promptSource: row.promptSource,
|
|
1059
|
+
claimedAt: row.claimedAt,
|
|
1031
1060
|
terminatedAt: row.terminatedAt,
|
|
1032
1061
|
packetCount: row.packetCount,
|
|
1033
1062
|
waitUntil: row.waitUntil,
|
|
1063
|
+
invocation: row.invocation === null || row.invocationWorkerId === null ? null : { workerId: row.invocationWorkerId, address: row.invocation },
|
|
1034
1064
|
// {§validation-topology}: a stored result is chapter 3's (`loops_terminal_result_contract`); it is parsed, not re-asserted.
|
|
1035
1065
|
terminalResult: row.terminalResult === null ? null : JSON.parse(row.terminalResult)
|
|
1036
1066
|
}));
|
|
1037
1067
|
}
|
|
1038
|
-
// {§methods-workspace-prompts}: root-
|
|
1068
|
+
// {§methods-workspace-prompts}: root-worker loop seeds, newest-first.
|
|
1039
1069
|
// {§share} — one workspace's share. The daemon names its own database; the caller names an absolute folder.
|
|
1040
1070
|
async shareWorkspace({ workspaceId, folder }) {
|
|
1041
1071
|
const checkedWorkspaceId = ClientInput.assertId("workspace.share", "workspaceId", workspaceId);
|
|
1042
1072
|
const checkedFolder = ClientInput.assertShareFolder("workspace.share", folder);
|
|
1043
1073
|
if (this.#dbPath === undefined)
|
|
1044
|
-
throw new Error("
|
|
1074
|
+
throw new Error("this daemon was started without its database path");
|
|
1045
1075
|
return await Share.write({ openEvidence: EvidenceReader.open, dbPath: this.#dbPath, folder: checkedFolder, workspaceId: checkedWorkspaceId });
|
|
1046
1076
|
}
|
|
1047
1077
|
listPrompts(workspaceId, limit, workerId) {
|
|
@@ -1050,6 +1080,13 @@ export default class Daemon {
|
|
|
1050
1080
|
const checkedWorkerId = workerId === undefined ? null : ClientInput.assertId("workspace.prompts", "workerId", workerId);
|
|
1051
1081
|
return Envelope.listPromptsForWorkspace(this.#db, checkedWorkspaceId, checkedLimit ?? Knob.integer("PLURNK_SERVICE_PROMPTS_PAGE", 1), checkedWorkerId);
|
|
1052
1082
|
}
|
|
1083
|
+
// {§methods-workspace-activate} — the demand a turn makes, without the turn: a cold workspace activates,
|
|
1084
|
+
// a concurrent demand shares that activation, and the release leaves it warm under the residency policy.
|
|
1085
|
+
async activateWorkspace(args) {
|
|
1086
|
+
const workspaceId = ClientInput.assertId("workspace.activate", "workspaceId", args.workspaceId);
|
|
1087
|
+
const release = await this.#residency.acquire(workspaceId);
|
|
1088
|
+
release();
|
|
1089
|
+
}
|
|
1053
1090
|
workspacePreparationStatus(workspaceId) {
|
|
1054
1091
|
return this.#functionality.preparationStatus(ClientInput.assertId("workspace preparation", "workspaceId", workspaceId));
|
|
1055
1092
|
}
|
|
@@ -1063,81 +1100,105 @@ export default class Daemon {
|
|
|
1063
1100
|
async createWorkspace(args) {
|
|
1064
1101
|
// The seam fails hard on malformed semantic input so every module inherits one wall:
|
|
1065
1102
|
// the settings bag ({§operator-config-workspace-settings}) and absolute projectRoot.
|
|
1066
|
-
const
|
|
1103
|
+
const explicit = ClientInput.assertOptionalWorkspaceName("workspace.create", "name", args.name);
|
|
1067
1104
|
const projectRoot = ClientInput.assertProjectRoot("workspace.create", args.projectRoot);
|
|
1068
1105
|
const settings = ClientInput.parseSettings(args.settings);
|
|
1106
|
+
// {§workspace-name} — with a project root and no name, the workspace is named after the root with
|
|
1107
|
+
// the daemon's home abbreviated to `~`, and that name selects the existing workspace when one exists:
|
|
1108
|
+
// a default is a convention shared by every client, never a second workspace.
|
|
1109
|
+
const derived = explicit === undefined && projectRoot !== null ? this.#hostPaths.abbreviateUserPath(projectRoot) : undefined;
|
|
1110
|
+
const selected = async () => {
|
|
1111
|
+
if (derived === undefined)
|
|
1112
|
+
return undefined;
|
|
1113
|
+
const existing = await this.#db.envelope_get_workspace_by_name.get({ name: derived });
|
|
1114
|
+
return existing === undefined ? undefined : this.#labelled(await Envelope.attachToWorkspace(this.#db, existing.id));
|
|
1115
|
+
};
|
|
1069
1116
|
return observed(// {§observability-boundary}
|
|
1070
1117
|
"workspace.create", {}, async (span) => {
|
|
1071
|
-
const
|
|
1118
|
+
const already = await selected();
|
|
1119
|
+
if (already !== undefined) {
|
|
1120
|
+
span.setAttribute("workspace.id", already.workspaceId);
|
|
1121
|
+
return already;
|
|
1122
|
+
}
|
|
1123
|
+
let envelope;
|
|
1124
|
+
try {
|
|
1125
|
+
envelope = this.#labelled(await Envelope.createClientEnvelope(this.#db, { name: explicit ?? derived, projectRoot, settings }));
|
|
1126
|
+
}
|
|
1127
|
+
catch (error) {
|
|
1128
|
+
// A concurrent default launch created it first: select what it created.
|
|
1129
|
+
const raced = derived !== undefined ? await selected() : undefined;
|
|
1130
|
+
if (raced === undefined)
|
|
1131
|
+
throw error;
|
|
1132
|
+
span.setAttribute("workspace.id", raced.workspaceId);
|
|
1133
|
+
return raced;
|
|
1134
|
+
}
|
|
1072
1135
|
span.setAttribute("workspace.id", envelope.workspaceId);
|
|
1073
|
-
this.#broadcast("all", "workspace/created", { id: envelope.workspaceId, name: envelope.workspaceName, projectRoot: envelope.projectRoot });
|
|
1136
|
+
this.#broadcast("all", "workspace/created", { id: envelope.workspaceId, name: envelope.workspaceName, projectRoot: envelope.projectRoot, projectLabel: envelope.projectLabel });
|
|
1074
1137
|
return envelope;
|
|
1075
1138
|
});
|
|
1076
1139
|
}
|
|
1140
|
+
// {§methods-workspace-attach} Selection is passive and never searches by the stored project folder.
|
|
1077
1141
|
async attachWorkspace(args) {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1142
|
+
const selectors = ["workspaceId", "name", "projectRoot"].filter((key) => Object.hasOwn(args, key));
|
|
1143
|
+
if (selectors.length !== 1 || args[selectors[0]] == null) {
|
|
1144
|
+
throw daemonFailure("daemon:workspace", "identity-invalid", 400, "Workspace selection requires exactly one ID, name or project root.", { retryable: false });
|
|
1145
|
+
}
|
|
1146
|
+
if (Object.hasOwn(args, "workspaceId")) {
|
|
1147
|
+
const workspaceId = ClientInput.assertId("workspace.attach", "workspaceId", args.workspaceId);
|
|
1148
|
+
return this.#labelled(await Envelope.attachToWorkspace(this.#db, workspaceId));
|
|
1149
|
+
}
|
|
1150
|
+
const name = Object.hasOwn(args, "name")
|
|
1151
|
+
? ClientInput.assertOptionalName("workspace.attach", "name", args.name)
|
|
1152
|
+
: this.#hostPaths.abbreviateUserPath(ClientInput.assertProjectRoot("workspace.attach", args.projectRoot));
|
|
1153
|
+
const existing = await this.#db.envelope_get_workspace_by_name.get({ name });
|
|
1154
|
+
if (existing === undefined) {
|
|
1155
|
+
throw daemonFailure("daemon:workspace", "workspace-not-found", 404, `Workspace ${JSON.stringify(name)} does not exist.`, { name });
|
|
1156
|
+
}
|
|
1157
|
+
return this.#labelled(await Envelope.attachToWorkspace(this.#db, existing.id));
|
|
1085
1158
|
}
|
|
1086
|
-
// {§
|
|
1087
|
-
|
|
1159
|
+
// {§worker-external-reference} The adapter interprets the key; core retains its target by identity.
|
|
1160
|
+
async readWorkerReference(args) {
|
|
1161
|
+
return WorkerReferences.read(this.#db, args);
|
|
1162
|
+
}
|
|
1163
|
+
async ensureWorkerReference(args) {
|
|
1164
|
+
const { binding, created } = await WorkerReferences.ensure(this.#db, args);
|
|
1165
|
+
if (created) {
|
|
1166
|
+
const env = await this.attachWorkspace(binding);
|
|
1167
|
+
this.#broadcast("all", "workspace/created", { id: env.workspaceId, name: env.workspaceName, projectRoot: env.projectRoot, projectLabel: env.projectLabel });
|
|
1168
|
+
}
|
|
1169
|
+
return binding;
|
|
1170
|
+
}
|
|
1171
|
+
async listWorkerReferences({ workspaceId, workerId, namespace }) {
|
|
1172
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
1173
|
+
return (await this.#db.worker_reference_list.all({ namespace, worker_id: workerId })).map(({ reference }) => reference);
|
|
1174
|
+
}
|
|
1175
|
+
// {§rollback} {§methods-workspace-rollback} — the compensations are one client operation of the acting person
|
|
1176
|
+
// ({§connection-lifecycle}): attributed to that person and, being that person's decision, never routed for review.
|
|
1088
1177
|
async rollback(args) {
|
|
1089
1178
|
const workspaceId = ClientInput.assertId("workspace.rollback", "workspaceId", args.workspaceId);
|
|
1090
1179
|
const workerId = ClientInput.assertId("workspace.rollback", "workerId", args.workerId);
|
|
1091
|
-
const
|
|
1180
|
+
const actor = ClientInput.assertActor("workspace.rollback", args.actor);
|
|
1092
1181
|
if (typeof args.to !== "string")
|
|
1093
1182
|
throw daemonFailure("daemon:rollback", "coordinate-invalid", 400, "The rollback coordinate must be a string.", {});
|
|
1094
1183
|
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
1095
|
-
await this.#
|
|
1096
|
-
const client = await this.#db.envelope_get_worker_by_id.get({ id: clientWorkerId });
|
|
1097
|
-
if (client === undefined)
|
|
1098
|
-
throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${clientWorkerId} does not exist.`, { workerId: clientWorkerId });
|
|
1099
|
-
const owner = { workspaceId, address: client.owner };
|
|
1100
|
-
const report = await Rollback.run({
|
|
1184
|
+
const report = await this.#clientOperation({ workspaceId, actor, contextWorkerId: workerId }, (dispatch) => Rollback.run({
|
|
1101
1185
|
db: this.#db,
|
|
1102
1186
|
projectRoot: async (id) => (await this.#db.envelope_get_workspace.get({ id }))?.project_root ?? null,
|
|
1103
1187
|
executorTags: (id) => this.#engine.executorTags(id),
|
|
1104
|
-
apply: async (statement) =>
|
|
1105
|
-
|
|
1106
|
-
try {
|
|
1107
|
-
const clientLoopId = await Envelope.ensureClientLoop(this.#db, clientWorkerId);
|
|
1108
|
-
try {
|
|
1109
|
-
const result = await this.#dispatchClientStatement({
|
|
1110
|
-
workspaceId, workerId: clientWorkerId, loopId: clientLoopId, statement,
|
|
1111
|
-
// A proposal pauses the dispatch until it is resolved; this is the owner's own act.
|
|
1112
|
-
onDispatch: (logEntryId) => setImmediate(() => {
|
|
1113
|
-
void this.resolveProposal(logEntryId, { decision: "accept" }, owner).catch(() => undefined);
|
|
1114
|
-
}),
|
|
1115
|
-
});
|
|
1116
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
|
|
1117
|
-
return result;
|
|
1118
|
-
}
|
|
1119
|
-
catch (error) {
|
|
1120
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
|
|
1121
|
-
throw error;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
finally {
|
|
1125
|
-
releaseCapabilities();
|
|
1126
|
-
}
|
|
1127
|
-
},
|
|
1128
|
-
}, { workspaceId, workerId, to: args.to });
|
|
1188
|
+
apply: async (statement) => await dispatch(statement),
|
|
1189
|
+
}, { workspaceId, workerId, to: args.to }));
|
|
1129
1190
|
return Validator.assertRollbackReport(report);
|
|
1130
1191
|
}
|
|
1131
1192
|
async renameWorkspace(workspaceId, name) {
|
|
1132
1193
|
const checkedWorkspaceId = ClientInput.assertId("workspace.rename", "workspaceId", workspaceId);
|
|
1133
|
-
const checkedName = ClientInput.
|
|
1194
|
+
const checkedName = ClientInput.assertOptionalWorkspaceName("workspace.rename", "name", name);
|
|
1134
1195
|
if (checkedName === undefined)
|
|
1135
|
-
throw new Error("ClientInput.
|
|
1196
|
+
throw new Error("ClientInput.assertOptionalWorkspaceName accepted a required name as undefined");
|
|
1136
1197
|
return { id: checkedWorkspaceId, name: await Envelope.updateWorkspaceName(this.#db, checkedWorkspaceId, checkedName) };
|
|
1137
1198
|
}
|
|
1138
|
-
// {§methods-
|
|
1139
|
-
//
|
|
1140
|
-
async
|
|
1199
|
+
// {§methods-worker-create}: a fresh worker is model-origin, with an empty private log.
|
|
1200
|
+
// {§machine-processes} Workers have independent histories within a shared workspace.
|
|
1201
|
+
async createWorker(args) {
|
|
1141
1202
|
const workspaceId = ClientInput.assertId("worker.create", "workspaceId", args.workspaceId);
|
|
1142
1203
|
const name = ClientInput.assertOptionalWorkerName("worker.create", "name", args.name);
|
|
1143
1204
|
const workspace = await this.#db.envelope_get_workspace.get({ id: workspaceId });
|
|
@@ -1164,24 +1225,14 @@ export default class Daemon {
|
|
|
1164
1225
|
const worker = await Envelope.createModelWorker(this.#db, workspaceId, name, parentWorkerId, args.owner);
|
|
1165
1226
|
return { workerId: worker.id, workerName: worker.name };
|
|
1166
1227
|
}
|
|
1167
|
-
// {§worker-scheme-fork} — branch a worker's log while sharing
|
|
1228
|
+
// {§worker-scheme-fork} — branch a worker's log while sharing its workspace.
|
|
1168
1229
|
// Core owns the workspace check and immutable worker-name admission.
|
|
1169
1230
|
async forkWorker(args) {
|
|
1170
1231
|
const workspaceId = ClientInput.assertId("run.fork", "workspaceId", args.workspaceId);
|
|
1171
1232
|
const workerId = ClientInput.assertId("run.fork", "workerId", args.workerId);
|
|
1172
1233
|
const name = ClientInput.assertOptionalWorkerName("run.fork", "name", args.name);
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
|
|
1176
|
-
}
|
|
1177
|
-
if (owner.workspace_id !== workspaceId) {
|
|
1178
|
-
throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, {
|
|
1179
|
-
workerId,
|
|
1180
|
-
workspaceId,
|
|
1181
|
-
actualWorkspaceId: owner.workspace_id,
|
|
1182
|
-
retryable: false
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1234
|
+
// {§machine-processes-worker-origin} A branch is a model's: only a model worker's log forks.
|
|
1235
|
+
await this.#assertModelWorker(workspaceId, workerId);
|
|
1185
1236
|
if (name !== undefined) {
|
|
1186
1237
|
const taken = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name });
|
|
1187
1238
|
if (taken !== undefined) {
|
|
@@ -1251,7 +1302,7 @@ export default class Daemon {
|
|
|
1251
1302
|
const { name, scope, inputSchema, outputSchema, handler } = registration;
|
|
1252
1303
|
if (name.length === 0)
|
|
1253
1304
|
throw new Error("registerModuleAction: action name must not be empty");
|
|
1254
|
-
if (scope !== "
|
|
1305
|
+
if (scope !== "daemon" && scope !== "workspace" && scope !== "worker") {
|
|
1255
1306
|
throw new Error(`module action '${name}' has invalid scope '${String(scope)}'`);
|
|
1256
1307
|
}
|
|
1257
1308
|
if (inputSchema === null || typeof inputSchema !== "object" || Array.isArray(inputSchema)) {
|
|
@@ -1273,8 +1324,8 @@ export default class Daemon {
|
|
|
1273
1324
|
if (registration.residency !== "required" && registration.residency !== "none") {
|
|
1274
1325
|
throw new Error(`module action '${name}' must declare its residency requirement`);
|
|
1275
1326
|
}
|
|
1276
|
-
if (scope === "
|
|
1277
|
-
throw new Error(`
|
|
1327
|
+
if (scope === "daemon" && registration.residency !== "none") {
|
|
1328
|
+
throw new Error(`daemon module action '${name}' cannot require workspace residency`);
|
|
1278
1329
|
}
|
|
1279
1330
|
if (this.#moduleActions.has(name))
|
|
1280
1331
|
throw new Error(`module action '${name}' is already registered`);
|
|
@@ -1306,7 +1357,7 @@ export default class Daemon {
|
|
|
1306
1357
|
throw daemonFailure("daemon:module-action", "worker-not-found", 404, `Worker ${workerId} does not exist in workspace ${workspaceId}.`, { workspaceId, workerId, retryable: false });
|
|
1307
1358
|
}
|
|
1308
1359
|
}
|
|
1309
|
-
if (registration.residency === "required" && context.scope !== "
|
|
1360
|
+
if (registration.residency === "required" && context.scope !== "daemon") {
|
|
1310
1361
|
releaseCapabilities = await this.#residency.acquire(context.workspaceId);
|
|
1311
1362
|
}
|
|
1312
1363
|
else if (context.scope === "workspace") {
|
|
@@ -1372,6 +1423,9 @@ export default class Daemon {
|
|
|
1372
1423
|
await this.#residency.replace(replacement, options);
|
|
1373
1424
|
}
|
|
1374
1425
|
get engine() { return this.#engine; }
|
|
1426
|
+
// {§credential-authority} — the file holding this boot's minted operator secret; null when the
|
|
1427
|
+
// secret is configured, the posture is off, or the daemon has no database path.
|
|
1428
|
+
get secretFile() { return this.#secretMinted ? this.#secretPath : null; }
|
|
1375
1429
|
configurationNotices() { return this.#configuration.notices(); }
|
|
1376
1430
|
get provider() { return this.#provider; }
|
|
1377
1431
|
get schemes() { return this.#schemes; }
|
|
@@ -1391,6 +1445,7 @@ export default class Daemon {
|
|
|
1391
1445
|
await this.#configuration.capture("wait", () => TurnDispositionHandler.configuredWaitSeconds());
|
|
1392
1446
|
await this.#configuration.capture("execution", () => Exec.validateConfiguration());
|
|
1393
1447
|
this.#retention = await this.#configuration.capture("retention", () => new Retention(this.#db, retentionPolicy()));
|
|
1448
|
+
this.#clientOperations = new AbortController();
|
|
1394
1449
|
this.#started = true;
|
|
1395
1450
|
// {§db-space-reclamation} — the file is brought to the policy's auto-vacuum mode before any work.
|
|
1396
1451
|
if (this.#retention !== null) {
|
|
@@ -1475,6 +1530,18 @@ export default class Daemon {
|
|
|
1475
1530
|
this.#moduleLifetimes.push(module);
|
|
1476
1531
|
await module.setup?.(setupSeam);
|
|
1477
1532
|
}
|
|
1533
|
+
// {§credential-authority} — the posture is never an inference: a listening daemon that
|
|
1534
|
+
// authenticates needs a module that issues, and it writes a minted secret where same-user
|
|
1535
|
+
// clients read it before any mount is admitted; a configured or absent secret leaves no file.
|
|
1536
|
+
if (this.#http !== null && this.credentials.operatorSecret !== null && this.credentials.face() === null) {
|
|
1537
|
+
throw new Error("PLURNK_AUTH=1, and no module publishes an authorization server to issue credentials: install the client interface, or set PLURNK_AUTH=0");
|
|
1538
|
+
}
|
|
1539
|
+
if (this.#secretPath !== null) {
|
|
1540
|
+
if (this.#secretMinted && this.credentials.operatorSecret !== null)
|
|
1541
|
+
await Credentials.writeSecret(this.#secretPath, this.credentials.operatorSecret);
|
|
1542
|
+
else
|
|
1543
|
+
await Credentials.removeSecret(this.#secretPath);
|
|
1544
|
+
}
|
|
1478
1545
|
await this.#schemes.ready();
|
|
1479
1546
|
this.#residency.publish();
|
|
1480
1547
|
await this.#recoverLifecycle();
|
|
@@ -1516,15 +1583,7 @@ export default class Daemon {
|
|
|
1516
1583
|
this.#http.admit();
|
|
1517
1584
|
}
|
|
1518
1585
|
async #recoverLifecycle() {
|
|
1519
|
-
await this.#db.
|
|
1520
|
-
await this.#db.recovery_settle_open_provider_requests.run({});
|
|
1521
|
-
await this.#db.recovery_fail_open_model_calls.run({});
|
|
1522
|
-
await this.#db.recovery_fail_open_turns.run({});
|
|
1523
|
-
await this.#db.recovery_fail_ownerless_proposals.run({});
|
|
1524
|
-
await this.#db.recovery_remove_ownerless_client_interactions.run({});
|
|
1525
|
-
await this.#db.recovery_error_orphan_subscription_channels.run({});
|
|
1526
|
-
await this.#db.recovery_fail_orphan_subscriptions.run({});
|
|
1527
|
-
await this.#db.recovery_resume_unblocked_parks.run({});
|
|
1586
|
+
await this.#db.recovery_settle_process_owners({});
|
|
1528
1587
|
const orphanSources = await this.#db.recovery_orphan_message_sources.all({});
|
|
1529
1588
|
const invalidOrphan = orphanSources.find(({ origin }) => origin !== "model");
|
|
1530
1589
|
if (invalidOrphan !== undefined) {
|
|
@@ -1572,6 +1631,7 @@ export default class Daemon {
|
|
|
1572
1631
|
this.#engine.cancelAllProposals("daemon_stopping");
|
|
1573
1632
|
this.#engine.cancelDerivations(derivationAbort);
|
|
1574
1633
|
this.#drains.beginStop("daemon_stopping");
|
|
1634
|
+
this.#clientOperations.abort("daemon_stopping");
|
|
1575
1635
|
this.#residency.beginStop();
|
|
1576
1636
|
const shutdown = deadline ?? new StopDeadline();
|
|
1577
1637
|
// Stop accepting external work immediately, but do not await listener
|
|
@@ -1604,6 +1664,10 @@ export default class Daemon {
|
|
|
1604
1664
|
// process may exit with the WAL in place (SQLite recovers) rather than
|
|
1605
1665
|
// leak as a live-but-wedged tree.
|
|
1606
1666
|
const drainResult = await shutdown.settle("drains idle", () => this.#drains.idle());
|
|
1667
|
+
const clientResult = await shutdown.settle("client operations idle", async () => {
|
|
1668
|
+
await Promise.all([...this.#clientLoops.values()].map(({ settled }) => settled));
|
|
1669
|
+
});
|
|
1670
|
+
const residencyResult = await shutdown.settle("workspace Functionality transitions", () => this.#residency.settle());
|
|
1607
1671
|
// A boundary publication queued behind the last turn settles before
|
|
1608
1672
|
// modules close and before the DB goes away ({§functionality-publication}).
|
|
1609
1673
|
const functionalityResult = await shutdown.settle("functionality publications", () => this.#functionality.settle());
|
|
@@ -1627,8 +1691,10 @@ export default class Daemon {
|
|
|
1627
1691
|
const closeErrors = [
|
|
1628
1692
|
moduleStopResult,
|
|
1629
1693
|
moduleCloseResult,
|
|
1694
|
+
residencyResult,
|
|
1630
1695
|
functionalityResult,
|
|
1631
1696
|
drainResult,
|
|
1697
|
+
clientResult,
|
|
1632
1698
|
streamingResult,
|
|
1633
1699
|
derivationResult,
|
|
1634
1700
|
collectResult,
|