@plurnk/plurnk-service 3.2.0 → 4.0.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 +3 -3
- package/INSTALL.md +6 -0
- package/README.md +2 -2
- package/SPEC.md +489 -186
- 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.map +1 -1
- package/dist/core/AdmittedTurnExecutor.js +8 -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/BudgetReadout.d.ts +1 -3
- package/dist/core/BudgetReadout.d.ts.map +1 -1
- package/dist/core/BudgetReadout.js +43 -46
- package/dist/core/BudgetReadout.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 +6 -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 +3 -7
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +53 -73
- 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.js +1 -1
- 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 +11 -2
- 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/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 +21 -18
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/PacketBuilder.sql +18 -5
- 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 +1 -0
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +14 -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.d.ts +1 -1
- package/dist/core/TurnDispositionHandler.d.ts.map +1 -1
- package/dist/core/TurnDispositionHandler.js +8 -5
- 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.js +1 -0
- 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 +63 -51
- 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 +84 -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 +3 -0
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +14 -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 +7 -6
- package/dist/core/mutation-types.d.ts +1 -0
- package/dist/core/mutation-types.d.ts.map +1 -1
- 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 +1 -1
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +24 -15
- 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 +1 -1
- package/dist/core/plurnk-uri.js.map +1 -1
- package/dist/core/scheme-types.d.ts +5 -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 +37 -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.d.ts +2 -1
- package/dist/schemes/ExecutionInput.d.ts.map +1 -1
- package/dist/schemes/ExecutionInput.js +45 -7
- package/dist/schemes/ExecutionInput.js.map +1 -1
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +32 -23
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +23 -5
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +24 -1
- 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 +44 -39
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +287 -172
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DrainSupervisor.d.ts +8 -3
- package/dist/server/DrainSupervisor.d.ts.map +1 -1
- package/dist/server/DrainSupervisor.js +44 -32
- 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 +192 -96
- package/dist/server/Functionality.js.map +1 -1
- package/dist/server/FunctionalityManager.d.ts +1 -4
- package/dist/server/FunctionalityManager.d.ts.map +1 -1
- package/dist/server/FunctionalityManager.js +6 -6
- 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 +7 -8
- package/dist/server/MembersFunctionality.d.ts.map +1 -1
- package/dist/server/MembersFunctionality.js +90 -56
- package/dist/server/MembersFunctionality.js.map +1 -1
- package/dist/server/MessageAbandonment.d.ts +7 -0
- package/dist/server/MessageAbandonment.d.ts.map +1 -0
- package/dist/server/MessageAbandonment.js +57 -0
- package/dist/server/MessageAbandonment.js.map +1 -0
- 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.d.ts +29 -0
- package/dist/server/Rollback.d.ts.map +1 -0
- package/dist/server/Rollback.js +111 -0
- package/dist/server/Rollback.js.map +1 -0
- 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 +46 -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 +20 -13
- package/migrations/002_workers.sql +2 -2
- package/migrations/006_log.sql +1 -1
- package/migrations/018_worker_owners.sql +1 -1
- package/migrations/026_message_obligations.sql +32 -0
- package/migrations/027_members_identity.sql +20 -0
- package/migrations/028_mutation_patch.sql +20 -0
- 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 +24 -24
- 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,13 +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";
|
|
20
|
+
import MessageAbandonment from "./MessageAbandonment.js";
|
|
19
21
|
import Retention, { retentionPolicy } from "./Retention.js";
|
|
20
22
|
import TurnDispositionHandler from "../core/TurnDispositionHandler.js";
|
|
21
23
|
import { Validator } from "@plurnk/plurnk-contracts";
|
|
22
24
|
import LogEntry from "./logEntry.js";
|
|
23
25
|
import HttpListener from "./HttpListener.js";
|
|
26
|
+
import Credentials from "./Credentials.js";
|
|
24
27
|
import Envelope, { projectWorkerRow } from "./envelope.js";
|
|
28
|
+
import WorkerReferences from "./WorkerReferences.js";
|
|
25
29
|
import ClientInput from "./client-input.js";
|
|
26
30
|
import Turn from "../core/Turn.js";
|
|
27
31
|
import WorkspacePlugins from "./WorkspacePlugins.js";
|
|
@@ -30,6 +34,7 @@ import ExecEnv from "../schemes/exec-env.js";
|
|
|
30
34
|
import PlurnkSkill from "./PlurnkSkill.js";
|
|
31
35
|
import ResourceTreeScheme from "../schemes/ResourceTreeScheme.js";
|
|
32
36
|
import MembersFunctionality, { modelScope, serviceMembers } from "./MembersFunctionality.js";
|
|
37
|
+
import Rollback from "./Rollback.js";
|
|
33
38
|
import FileCreationPolicy from "../core/file-creation-policy.js";
|
|
34
39
|
import EnvFunctionality, { ENV_OWNER } from "./EnvFunctionality.js";
|
|
35
40
|
import EnvScheme from "../schemes/Env.js";
|
|
@@ -58,6 +63,13 @@ import { daemonFailure, modelRouteLabel } from "./daemon-results.js";
|
|
|
58
63
|
import WorkerModelResolver from "./WorkerModelResolver.js";
|
|
59
64
|
import ClientReads from "./ClientReads.js";
|
|
60
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
|
+
};
|
|
61
73
|
const clientActionFailure = (error) => {
|
|
62
74
|
if (error instanceof OperationFailureError)
|
|
63
75
|
return error.result;
|
|
@@ -69,6 +81,7 @@ const clientActionFailure = (error) => {
|
|
|
69
81
|
};
|
|
70
82
|
export default class Daemon {
|
|
71
83
|
static validateConfiguration() {
|
|
84
|
+
Credentials.posture();
|
|
72
85
|
FileCreationPolicy.serviceScope();
|
|
73
86
|
EffectPolicy.validateConfiguration();
|
|
74
87
|
ProposalPolicies.read();
|
|
@@ -97,6 +110,13 @@ export default class Daemon {
|
|
|
97
110
|
// {§http-host} — the one listener, bound by the service before durable admission; null in
|
|
98
111
|
// tests that never open a transport.
|
|
99
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;
|
|
100
120
|
#discoveryCwd;
|
|
101
121
|
#started = false; // {§module-lifecycle}: one discovery/module boot; no listener
|
|
102
122
|
#modules = [];
|
|
@@ -105,6 +125,11 @@ export default class Daemon {
|
|
|
105
125
|
#mounted = new Set();
|
|
106
126
|
#moduleLifetimes = [];
|
|
107
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();
|
|
108
133
|
#residency;
|
|
109
134
|
#functionality;
|
|
110
135
|
// {§retention-policy} — the operator's retention, run on a cadence and at shutdown.
|
|
@@ -125,6 +150,10 @@ export default class Daemon {
|
|
|
125
150
|
this.#dbPath = dbPath;
|
|
126
151
|
this.#http = http;
|
|
127
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;
|
|
128
157
|
this.#storage = new WorkspaceStorage(db, hostPaths);
|
|
129
158
|
this.#lifecycle = new LoopLifecycle(db);
|
|
130
159
|
this.#schemes = schemes ?? new SchemeRegistry();
|
|
@@ -163,13 +192,15 @@ export default class Daemon {
|
|
|
163
192
|
// {§functionality-coordinator} — the one owner above every family
|
|
164
193
|
// adapter; Core hosts it, modules register adapters through the seam.
|
|
165
194
|
this.#functionality = new Functionality({
|
|
195
|
+
signal: this.#residency.signal,
|
|
166
196
|
registerModuleAction: (registration) => this.registerModuleAction(registration),
|
|
167
197
|
registerWorkspaceCapabilityProvider: (owner, provider) => this.registerWorkspaceCapabilityProvider(owner, provider),
|
|
168
198
|
readWorkspaceModuleState: (workspaceId, owner) => this.readWorkspaceModuleState(workspaceId, owner),
|
|
169
199
|
replaceWorkspaceCapabilities: (replacement, options) => this.replaceWorkspaceCapabilities(replacement, options),
|
|
170
200
|
readWorkerModuleState: (workerId, owner) => this.#residency.readWorkerModuleState(workerId, owner),
|
|
171
201
|
replaceWorkerModuleState: (workerId, owner, state) => this.#residency.replaceWorkerModuleState(workerId, owner, state),
|
|
172
|
-
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),
|
|
173
204
|
retainWorkspace: (workspaceId) => this.#residency.retain(workspaceId),
|
|
174
205
|
preparationChanged: (workspaceId, preparation) => this.#broadcast({ workspaceId }, "workspace/preparation", { workspaceId, preparation })
|
|
175
206
|
});
|
|
@@ -179,8 +210,10 @@ export default class Daemon {
|
|
|
179
210
|
this.#functionality.register(this.#members);
|
|
180
211
|
// {§env-functionality} — Core's own family: the environment a Worker's commands run in.
|
|
181
212
|
// {§env-scheme} — the family's face: reads project the dispatching Worker's effective
|
|
182
|
-
// environment, writes go through the family's own admission at the Worker's layer.
|
|
183
|
-
|
|
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")));
|
|
184
217
|
this.#engine = new Engine({
|
|
185
218
|
db, lifecycle: this.#lifecycle, schemes: this.#schemes, mimetypes: this.#mimetypes,
|
|
186
219
|
// {§tokenomics-agnostic-ruler} — stored and catalog curation weights
|
|
@@ -198,13 +231,13 @@ export default class Daemon {
|
|
|
198
231
|
// daemon owns provider + the law-file system prompt; the worker scheme
|
|
199
232
|
// handler carries neither. Fire-and-forget: the returned drain runs
|
|
200
233
|
// independently (the sister is its own worker). {§machine-processes}
|
|
201
|
-
injectWorker: async ({ workspaceId, workerId, sourceLoopId, prompt, spawn, environment, attachments }) => {
|
|
234
|
+
injectWorker: async ({ workspaceId, workerId, sourceLoopId, prompt, spawn, environment, attachments, opener }) => {
|
|
202
235
|
await this.#assertModelWorker(workspaceId, workerId);
|
|
203
236
|
const sender = await this.#db.drain_message_source.get({ loop_id: sourceLoopId });
|
|
204
237
|
if (sender === undefined || sender.workspace_id !== workspaceId) {
|
|
205
238
|
throw new Error(`injectWorker: source loop ${sourceLoopId} does not belong to workspace ${workspaceId}`);
|
|
206
239
|
}
|
|
207
|
-
const source = await this.#
|
|
240
|
+
const source = await this.#messageSource(workspaceId, sourceLoopId, sender.worker_id, workerId);
|
|
208
241
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
209
242
|
let providerSpec;
|
|
210
243
|
let childProviderSpec;
|
|
@@ -237,14 +270,14 @@ export default class Daemon {
|
|
|
237
270
|
effort: effort,
|
|
238
271
|
effort_source: "default"
|
|
239
272
|
});
|
|
240
|
-
// {§env-option} — explicit child overrides replace copied values and enable them.
|
|
241
|
-
// Names were admitted at the operation; a refusal here is a defect and throws.
|
|
242
|
-
for (const [alias, value] of Object.entries(environment ?? {})) {
|
|
243
|
-
await this.#functionality.invoke("env", "replace", { alias, definition: { value } }, { workspaceId, workerId }, "operation");
|
|
244
|
-
await this.#functionality.invoke("env", "enable", { alias }, { workspaceId, workerId }, "operation");
|
|
245
|
-
}
|
|
246
273
|
}
|
|
247
|
-
|
|
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 () => {
|
|
248
281
|
const delivered = await this.#prepareMessage(workspaceId, workerId, prompt, attachments);
|
|
249
282
|
return this.inject({
|
|
250
283
|
workspaceId,
|
|
@@ -253,15 +286,20 @@ export default class Daemon {
|
|
|
253
286
|
evidence: delivered.evidence,
|
|
254
287
|
sourceLoopId,
|
|
255
288
|
...(source === undefined ? {} : { source }),
|
|
289
|
+
...(opener === undefined ? {} : { opener }),
|
|
256
290
|
providerSpec,
|
|
257
291
|
effort,
|
|
258
292
|
childProviderSpec,
|
|
259
293
|
systemPrompt
|
|
260
294
|
});
|
|
261
295
|
});
|
|
262
|
-
|
|
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 };
|
|
263
301
|
},
|
|
264
|
-
|
|
302
|
+
withWorkspaceTurn: (workspaceId, workerId, run, signal) => this.#workspaceGate.withTurn(workspaceId, workerId, run, signal),
|
|
265
303
|
// {§functionality-hotload} — skills and plugins change out of band.
|
|
266
304
|
// Republish under the workspace turn gate before packet assembly so
|
|
267
305
|
// the first subsequent model turn sees their exact result.
|
|
@@ -279,8 +317,10 @@ export default class Daemon {
|
|
|
279
317
|
});
|
|
280
318
|
this.#drains = new DrainSupervisor({
|
|
281
319
|
db,
|
|
320
|
+
// {§message-abandonment} — the daemon answers, 499, what an ended loop left unanswered.
|
|
321
|
+
completeAbandonedMessages: (workspaceId, workerId, loopId, status) => MessageAbandonment.complete(this.#engine, this.#db, workspaceId, workerId, loopId, status),
|
|
282
322
|
lifecycle: this.#lifecycle,
|
|
283
|
-
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),
|
|
284
324
|
assertInjectionCompatibility: async ({ workerId, loopId, providerSpec, providerSpecExplicit, effort, childProviderSpec, turnCeiling }) => {
|
|
285
325
|
// An omitted selector keeps the loop's durable provider; only an
|
|
286
326
|
// explicit selection is checked against it (a deliberate switch).
|
|
@@ -416,6 +456,11 @@ export default class Daemon {
|
|
|
416
456
|
await this.#assertModelWorker(args.workspaceId, args.workerId);
|
|
417
457
|
return MessageResources.read(this.#db, args);
|
|
418
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
|
+
}
|
|
419
464
|
// {§methods-loop-run} — drive/steer a loop. The module supplies only workspace/worker/prompt;
|
|
420
465
|
// the provider and the law-file system prompt are core's and stay inside. Returns immediately — the
|
|
421
466
|
// loop runs async and its outcome arrives on the event source (loop/terminated). `cancelDrain` (public)
|
|
@@ -571,12 +616,6 @@ export default class Daemon {
|
|
|
571
616
|
});
|
|
572
617
|
}
|
|
573
618
|
}
|
|
574
|
-
// {§methods-model-worker} — the workspace's model worker (created on first use), distinct from the client
|
|
575
|
-
// worker so the model's packets never carry client-action rows. The module binds its threads to this.
|
|
576
|
-
async ensureModelWorker(workspaceId) {
|
|
577
|
-
const checked = ClientInput.assertId("worker.ensure-model", "workspaceId", workspaceId);
|
|
578
|
-
return await Envelope.ensureModelWorker(this.#db, checked);
|
|
579
|
-
}
|
|
580
619
|
async ensureRuntimeWorker(workspaceId) {
|
|
581
620
|
const checked = ClientInput.assertId("worker.ensure-runtime", "workspaceId", workspaceId);
|
|
582
621
|
if (await this.#db.envelope_get_workspace.get({ id: checked }) === undefined) {
|
|
@@ -753,7 +792,12 @@ export default class Daemon {
|
|
|
753
792
|
});
|
|
754
793
|
}
|
|
755
794
|
}
|
|
756
|
-
|
|
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;
|
|
757
801
|
if (sourceWorkerId === targetWorkerId)
|
|
758
802
|
return undefined;
|
|
759
803
|
const source = await this.#db.envelope_get_worker_by_id.get({ id: sourceWorkerId });
|
|
@@ -762,81 +806,93 @@ export default class Daemon {
|
|
|
762
806
|
}
|
|
763
807
|
return WorkerControlAddress.render(source.name);
|
|
764
808
|
}
|
|
765
|
-
// {§methods-op-mirror} — execute parsed ops on behalf of a client as a
|
|
766
|
-
// client-origin turn (the log is core's, a client op is a first-class citizen), dispatched through
|
|
767
|
-
// the engine, then emitted as log/entry on the event source. One seam op backs the whole op_*
|
|
768
|
-
// family (read/edit/copy/find/kill/look/move/send/exec); the module parses at its edge with the
|
|
769
|
-
// grammar package and hands over the statement, then fans the emitted entry out to its own clients.
|
|
770
809
|
// {§functionality-model-mutation} — await every queued Functionality
|
|
771
810
|
// publication (a Worker's accepted mutation publishes at its turn boundary).
|
|
772
811
|
async settleFunctionality(workspaceId) {
|
|
773
812
|
await this.#functionality.settle(workspaceId);
|
|
774
813
|
}
|
|
775
|
-
async dispatchAsClient(args) {
|
|
776
|
-
const workspaceId = ClientInput.assertId("operation.dispatch", "workspaceId", args.workspaceId);
|
|
777
|
-
const workerId = ClientInput.assertId("operation.dispatch", "workerId", args.workerId);
|
|
778
|
-
const { statement } = args;
|
|
779
|
-
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
780
|
-
const releaseCapabilities = await this.#residency.acquire(workspaceId);
|
|
781
|
-
try {
|
|
782
|
-
const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
|
|
783
|
-
try {
|
|
784
|
-
const result = await this.#dispatchClientStatement({ workspaceId, workerId, loopId: clientLoopId, statement });
|
|
785
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
|
|
786
|
-
return result;
|
|
787
|
-
}
|
|
788
|
-
catch (error) {
|
|
789
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
|
|
790
|
-
throw error;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
finally {
|
|
794
|
-
releaseCapabilities();
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
// The client-interface action contract: one AG-UI action owns one administrative
|
|
798
|
-
// loop, regardless of how many statements op.parse produced. Each statement is one
|
|
799
|
-
// ordinary operation turn; a proposal may keep that turn and loop open across
|
|
800
|
-
// interrupt/resume until settlement.
|
|
801
814
|
executorTags(workspaceId) {
|
|
802
815
|
return this.#engine.executorTags(workspaceId);
|
|
803
816
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
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}).
|
|
807
820
|
async dispatchClientAction(args) {
|
|
808
821
|
const workspaceId = ClientInput.assertId("operation.dispatch-batch", "workspaceId", args.workspaceId);
|
|
809
|
-
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);
|
|
810
828
|
const { statements } = args;
|
|
811
829
|
if (statements.length === 0)
|
|
812
830
|
return [];
|
|
813
|
-
await this.#
|
|
814
|
-
|
|
815
|
-
|
|
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);
|
|
816
843
|
const releaseCapabilities = await this.#residency.acquire(workspaceId);
|
|
817
844
|
try {
|
|
818
|
-
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);
|
|
819
851
|
try {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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;
|
|
826
863
|
}
|
|
827
864
|
catch (error) {
|
|
828
|
-
|
|
829
|
-
|
|
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();
|
|
830
874
|
}
|
|
831
875
|
}
|
|
832
876
|
finally {
|
|
833
877
|
releaseCapabilities();
|
|
834
878
|
}
|
|
835
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
|
+
}
|
|
836
893
|
async #dispatchClientStatement(args) {
|
|
837
|
-
const { workspaceId, workerId, loopId, statement } = args;
|
|
838
|
-
|
|
839
|
-
try {
|
|
894
|
+
const { workspaceId, workerId, loopId, statement, contextWorkerId } = args;
|
|
895
|
+
return this.#workspaceGate.withTurn(workspaceId, workerId, async () => {
|
|
840
896
|
await this.#residency.reconcile(workspaceId);
|
|
841
897
|
const { id: turnId } = await Turn.open(this.#db, {
|
|
842
898
|
loopId,
|
|
@@ -848,7 +904,8 @@ export default class Daemon {
|
|
|
848
904
|
const entryIds = [];
|
|
849
905
|
const result = await this.#engine.dispatch({
|
|
850
906
|
statement, workspaceId, workerId, loopId, turnId, sequence: 1,
|
|
851
|
-
origin: "client",
|
|
907
|
+
origin: "client", client: { contextWorkerId },
|
|
908
|
+
onDispatch: (logEntryId) => { entryIds.push(logEntryId); }
|
|
852
909
|
});
|
|
853
910
|
await Turn.complete(this.#db, turnId, result.status);
|
|
854
911
|
turnOpen = false;
|
|
@@ -869,51 +926,7 @@ export default class Daemon {
|
|
|
869
926
|
}
|
|
870
927
|
throw cause;
|
|
871
928
|
}
|
|
872
|
-
}
|
|
873
|
-
finally {
|
|
874
|
-
release();
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
// {§op-look} — the pure READ-projection query on the seam: resolve a READ through the
|
|
878
|
-
// full scheme resolver and return its content, writing NO log row — the client's out-of-band
|
|
879
|
-
// inspection primitive (the module rewrites LOOK→READ and parses at its edge, exactly like
|
|
880
|
-
// dispatchClientAction). Its closed observation segment supplies the numeric loop coordinate
|
|
881
|
-
// required by extension context and relative log:/// addresses without impersonating an active
|
|
882
|
-
// client lifecycle. It creates no turn or log row. Engine.look enforces READ-only.
|
|
883
|
-
// {§op-look} — the segment is the acting worker's; the READ resolves as the perspective worker.
|
|
884
|
-
async look(args) {
|
|
885
|
-
const workspaceId = ClientInput.assertId("operation.look", "workspaceId", args.workspaceId);
|
|
886
|
-
const workerId = ClientInput.assertId("operation.look", "workerId", args.workerId);
|
|
887
|
-
const perspective = args.perspectiveWorkerId === undefined
|
|
888
|
-
? workerId
|
|
889
|
-
: ClientInput.assertId("operation.look", "perspectiveWorkerId", args.perspectiveWorkerId);
|
|
890
|
-
const { statement } = args;
|
|
891
|
-
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
892
|
-
if (perspective !== workerId)
|
|
893
|
-
await this.#assertWorkerOwned(workspaceId, perspective);
|
|
894
|
-
const releaseCapabilities = await this.#residency.acquire(workspaceId);
|
|
895
|
-
try {
|
|
896
|
-
const releaseWorkspace = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
|
|
897
|
-
try {
|
|
898
|
-
await this.#residency.reconcile(workspaceId);
|
|
899
|
-
const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
|
|
900
|
-
try {
|
|
901
|
-
const result = await this.#engine.look({ statement, workspaceId, workerId: perspective, loopId: clientLoopId });
|
|
902
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
|
|
903
|
-
return result;
|
|
904
|
-
}
|
|
905
|
-
catch (error) {
|
|
906
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
|
|
907
|
-
throw error;
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
finally {
|
|
911
|
-
releaseWorkspace();
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
finally {
|
|
915
|
-
releaseCapabilities();
|
|
916
|
-
}
|
|
929
|
+
}, this.#clientLoops.get(loopId)?.scope.signal);
|
|
917
930
|
}
|
|
918
931
|
// {§methods} — the module's render surface beyond the journal. Thin delegations
|
|
919
932
|
// into core's envelope / membership / provider machinery; the module fans the results into its own views.
|
|
@@ -931,14 +944,23 @@ export default class Daemon {
|
|
|
931
944
|
return {
|
|
932
945
|
aliases: aliases.map((a) => {
|
|
933
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
|
+
}
|
|
934
956
|
return {
|
|
935
|
-
|
|
957
|
+
...projectModelRoute(a, effort, "default"), alias: a.alias, active: isActive,
|
|
936
958
|
inputCapacity: isActive && this.#provider !== null ? this.#provider.inputCapacity : null
|
|
937
959
|
};
|
|
938
960
|
})
|
|
939
961
|
};
|
|
940
962
|
}
|
|
941
|
-
// {§model-catalog} The catalog is a bounded,
|
|
963
|
+
// {§model-catalog} The catalog is a bounded, daemon-local projection.
|
|
942
964
|
// It performs no provider I/O and never changes the worker's selection.
|
|
943
965
|
listModels(query) {
|
|
944
966
|
return listModelCatalog(query);
|
|
@@ -966,7 +988,16 @@ export default class Daemon {
|
|
|
966
988
|
}));
|
|
967
989
|
return Validator.assertClientDisplayCapabilities([...schemes, ...mimetypes]);
|
|
968
990
|
}
|
|
969
|
-
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
|
+
}
|
|
970
1001
|
listWorkers(workspaceId, query = {}) {
|
|
971
1002
|
const origin = query.origin;
|
|
972
1003
|
if (origin !== undefined && !["model", "client", "_plurnk"].includes(origin)) {
|
|
@@ -991,26 +1022,27 @@ export default class Daemon {
|
|
|
991
1022
|
if (hasId === hasName) {
|
|
992
1023
|
throw daemonFailure("daemon:worker", "identity-invalid", 400, "Worker observation requires exactly one id or name.", { retryable: false });
|
|
993
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.
|
|
994
1027
|
const row = hasId
|
|
995
1028
|
? await this.#db.envelope_get_worker_by_id.get({
|
|
996
1029
|
id: ClientInput.assertId("worker.read", "id", args.identity.id)
|
|
997
1030
|
})
|
|
998
1031
|
: await this.#db.envelope_get_worker_by_name.get({
|
|
999
1032
|
workspace_id: workspaceId,
|
|
1000
|
-
name: ClientInput.
|
|
1033
|
+
name: ClientInput.assertOptionalName("worker.read", "name", args.identity.name)
|
|
1001
1034
|
});
|
|
1002
1035
|
if (row === undefined || row.workspace_id !== workspaceId)
|
|
1003
1036
|
return null;
|
|
1004
1037
|
const { workspace_id: _workspaceId, ...projection } = projectWorkerRow(row);
|
|
1005
1038
|
return projection;
|
|
1006
1039
|
}
|
|
1007
|
-
// {§methods-worker-
|
|
1008
|
-
async
|
|
1009
|
-
const workspaceId = ClientInput.assertId("worker.
|
|
1010
|
-
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);
|
|
1011
1044
|
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
1012
|
-
|
|
1013
|
-
return this.#engine.descendantAccounting(workerId, loopId);
|
|
1045
|
+
return this.#engine.workerAccounting(workerId);
|
|
1014
1046
|
}
|
|
1015
1047
|
async listWorkerLoops(args) {
|
|
1016
1048
|
const workspaceId = ClientInput.assertId("worker.loops", "workspaceId", args.workspaceId);
|
|
@@ -1024,20 +1056,22 @@ export default class Daemon {
|
|
|
1024
1056
|
status: row.status,
|
|
1025
1057
|
prompt: row.prompt,
|
|
1026
1058
|
promptSource: row.promptSource,
|
|
1059
|
+
claimedAt: row.claimedAt,
|
|
1027
1060
|
terminatedAt: row.terminatedAt,
|
|
1028
1061
|
packetCount: row.packetCount,
|
|
1029
1062
|
waitUntil: row.waitUntil,
|
|
1063
|
+
invocation: row.invocation === null || row.invocationWorkerId === null ? null : { workerId: row.invocationWorkerId, address: row.invocation },
|
|
1030
1064
|
// {§validation-topology}: a stored result is chapter 3's (`loops_terminal_result_contract`); it is parsed, not re-asserted.
|
|
1031
1065
|
terminalResult: row.terminalResult === null ? null : JSON.parse(row.terminalResult)
|
|
1032
1066
|
}));
|
|
1033
1067
|
}
|
|
1034
|
-
// {§methods-workspace-prompts}: root-
|
|
1068
|
+
// {§methods-workspace-prompts}: root-worker loop seeds, newest-first.
|
|
1035
1069
|
// {§share} — one workspace's share. The daemon names its own database; the caller names an absolute folder.
|
|
1036
1070
|
async shareWorkspace({ workspaceId, folder }) {
|
|
1037
1071
|
const checkedWorkspaceId = ClientInput.assertId("workspace.share", "workspaceId", workspaceId);
|
|
1038
1072
|
const checkedFolder = ClientInput.assertShareFolder("workspace.share", folder);
|
|
1039
1073
|
if (this.#dbPath === undefined)
|
|
1040
|
-
throw new Error("
|
|
1074
|
+
throw new Error("this daemon was started without its database path");
|
|
1041
1075
|
return await Share.write({ openEvidence: EvidenceReader.open, dbPath: this.#dbPath, folder: checkedFolder, workspaceId: checkedWorkspaceId });
|
|
1042
1076
|
}
|
|
1043
1077
|
listPrompts(workspaceId, limit, workerId) {
|
|
@@ -1046,6 +1080,13 @@ export default class Daemon {
|
|
|
1046
1080
|
const checkedWorkerId = workerId === undefined ? null : ClientInput.assertId("workspace.prompts", "workerId", workerId);
|
|
1047
1081
|
return Envelope.listPromptsForWorkspace(this.#db, checkedWorkspaceId, checkedLimit ?? Knob.integer("PLURNK_SERVICE_PROMPTS_PAGE", 1), checkedWorkerId);
|
|
1048
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
|
+
}
|
|
1049
1090
|
workspacePreparationStatus(workspaceId) {
|
|
1050
1091
|
return this.#functionality.preparationStatus(ClientInput.assertId("workspace preparation", "workspaceId", workspaceId));
|
|
1051
1092
|
}
|
|
@@ -1059,36 +1100,105 @@ export default class Daemon {
|
|
|
1059
1100
|
async createWorkspace(args) {
|
|
1060
1101
|
// The seam fails hard on malformed semantic input so every module inherits one wall:
|
|
1061
1102
|
// the settings bag ({§operator-config-workspace-settings}) and absolute projectRoot.
|
|
1062
|
-
const
|
|
1103
|
+
const explicit = ClientInput.assertOptionalWorkspaceName("workspace.create", "name", args.name);
|
|
1063
1104
|
const projectRoot = ClientInput.assertProjectRoot("workspace.create", args.projectRoot);
|
|
1064
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
|
+
};
|
|
1065
1116
|
return observed(// {§observability-boundary}
|
|
1066
1117
|
"workspace.create", {}, async (span) => {
|
|
1067
|
-
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
|
+
}
|
|
1068
1135
|
span.setAttribute("workspace.id", envelope.workspaceId);
|
|
1069
|
-
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 });
|
|
1070
1137
|
return envelope;
|
|
1071
1138
|
});
|
|
1072
1139
|
}
|
|
1140
|
+
// {§methods-workspace-attach} Selection is passive and never searches by the stored project folder.
|
|
1073
1141
|
async attachWorkspace(args) {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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));
|
|
1158
|
+
}
|
|
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.
|
|
1177
|
+
async rollback(args) {
|
|
1178
|
+
const workspaceId = ClientInput.assertId("workspace.rollback", "workspaceId", args.workspaceId);
|
|
1179
|
+
const workerId = ClientInput.assertId("workspace.rollback", "workerId", args.workerId);
|
|
1180
|
+
const actor = ClientInput.assertActor("workspace.rollback", args.actor);
|
|
1181
|
+
if (typeof args.to !== "string")
|
|
1182
|
+
throw daemonFailure("daemon:rollback", "coordinate-invalid", 400, "The rollback coordinate must be a string.", {});
|
|
1183
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
1184
|
+
const report = await this.#clientOperation({ workspaceId, actor, contextWorkerId: workerId }, (dispatch) => Rollback.run({
|
|
1185
|
+
db: this.#db,
|
|
1186
|
+
projectRoot: async (id) => (await this.#db.envelope_get_workspace.get({ id }))?.project_root ?? null,
|
|
1187
|
+
executorTags: (id) => this.#engine.executorTags(id),
|
|
1188
|
+
apply: async (statement) => await dispatch(statement),
|
|
1189
|
+
}, { workspaceId, workerId, to: args.to }));
|
|
1190
|
+
return Validator.assertRollbackReport(report);
|
|
1081
1191
|
}
|
|
1082
1192
|
async renameWorkspace(workspaceId, name) {
|
|
1083
1193
|
const checkedWorkspaceId = ClientInput.assertId("workspace.rename", "workspaceId", workspaceId);
|
|
1084
|
-
const checkedName = ClientInput.
|
|
1194
|
+
const checkedName = ClientInput.assertOptionalWorkspaceName("workspace.rename", "name", name);
|
|
1085
1195
|
if (checkedName === undefined)
|
|
1086
|
-
throw new Error("ClientInput.
|
|
1196
|
+
throw new Error("ClientInput.assertOptionalWorkspaceName accepted a required name as undefined");
|
|
1087
1197
|
return { id: checkedWorkspaceId, name: await Envelope.updateWorkspaceName(this.#db, checkedWorkspaceId, checkedName) };
|
|
1088
1198
|
}
|
|
1089
|
-
// {§methods-
|
|
1090
|
-
//
|
|
1091
|
-
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) {
|
|
1092
1202
|
const workspaceId = ClientInput.assertId("worker.create", "workspaceId", args.workspaceId);
|
|
1093
1203
|
const name = ClientInput.assertOptionalWorkerName("worker.create", "name", args.name);
|
|
1094
1204
|
const workspace = await this.#db.envelope_get_workspace.get({ id: workspaceId });
|
|
@@ -1115,24 +1225,14 @@ export default class Daemon {
|
|
|
1115
1225
|
const worker = await Envelope.createModelWorker(this.#db, workspaceId, name, parentWorkerId, args.owner);
|
|
1116
1226
|
return { workerId: worker.id, workerName: worker.name };
|
|
1117
1227
|
}
|
|
1118
|
-
// {§worker-scheme-fork} — branch a worker's log while sharing
|
|
1228
|
+
// {§worker-scheme-fork} — branch a worker's log while sharing its workspace.
|
|
1119
1229
|
// Core owns the workspace check and immutable worker-name admission.
|
|
1120
1230
|
async forkWorker(args) {
|
|
1121
1231
|
const workspaceId = ClientInput.assertId("run.fork", "workspaceId", args.workspaceId);
|
|
1122
1232
|
const workerId = ClientInput.assertId("run.fork", "workerId", args.workerId);
|
|
1123
1233
|
const name = ClientInput.assertOptionalWorkerName("run.fork", "name", args.name);
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
|
|
1127
|
-
}
|
|
1128
|
-
if (owner.workspace_id !== workspaceId) {
|
|
1129
|
-
throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, {
|
|
1130
|
-
workerId,
|
|
1131
|
-
workspaceId,
|
|
1132
|
-
actualWorkspaceId: owner.workspace_id,
|
|
1133
|
-
retryable: false
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1234
|
+
// {§machine-processes-worker-origin} A branch is a model's: only a model worker's log forks.
|
|
1235
|
+
await this.#assertModelWorker(workspaceId, workerId);
|
|
1136
1236
|
if (name !== undefined) {
|
|
1137
1237
|
const taken = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name });
|
|
1138
1238
|
if (taken !== undefined) {
|
|
@@ -1202,7 +1302,7 @@ export default class Daemon {
|
|
|
1202
1302
|
const { name, scope, inputSchema, outputSchema, handler } = registration;
|
|
1203
1303
|
if (name.length === 0)
|
|
1204
1304
|
throw new Error("registerModuleAction: action name must not be empty");
|
|
1205
|
-
if (scope !== "
|
|
1305
|
+
if (scope !== "daemon" && scope !== "workspace" && scope !== "worker") {
|
|
1206
1306
|
throw new Error(`module action '${name}' has invalid scope '${String(scope)}'`);
|
|
1207
1307
|
}
|
|
1208
1308
|
if (inputSchema === null || typeof inputSchema !== "object" || Array.isArray(inputSchema)) {
|
|
@@ -1224,8 +1324,8 @@ export default class Daemon {
|
|
|
1224
1324
|
if (registration.residency !== "required" && registration.residency !== "none") {
|
|
1225
1325
|
throw new Error(`module action '${name}' must declare its residency requirement`);
|
|
1226
1326
|
}
|
|
1227
|
-
if (scope === "
|
|
1228
|
-
throw new Error(`
|
|
1327
|
+
if (scope === "daemon" && registration.residency !== "none") {
|
|
1328
|
+
throw new Error(`daemon module action '${name}' cannot require workspace residency`);
|
|
1229
1329
|
}
|
|
1230
1330
|
if (this.#moduleActions.has(name))
|
|
1231
1331
|
throw new Error(`module action '${name}' is already registered`);
|
|
@@ -1257,7 +1357,7 @@ export default class Daemon {
|
|
|
1257
1357
|
throw daemonFailure("daemon:module-action", "worker-not-found", 404, `Worker ${workerId} does not exist in workspace ${workspaceId}.`, { workspaceId, workerId, retryable: false });
|
|
1258
1358
|
}
|
|
1259
1359
|
}
|
|
1260
|
-
if (registration.residency === "required" && context.scope !== "
|
|
1360
|
+
if (registration.residency === "required" && context.scope !== "daemon") {
|
|
1261
1361
|
releaseCapabilities = await this.#residency.acquire(context.workspaceId);
|
|
1262
1362
|
}
|
|
1263
1363
|
else if (context.scope === "workspace") {
|
|
@@ -1323,6 +1423,9 @@ export default class Daemon {
|
|
|
1323
1423
|
await this.#residency.replace(replacement, options);
|
|
1324
1424
|
}
|
|
1325
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; }
|
|
1326
1429
|
configurationNotices() { return this.#configuration.notices(); }
|
|
1327
1430
|
get provider() { return this.#provider; }
|
|
1328
1431
|
get schemes() { return this.#schemes; }
|
|
@@ -1342,6 +1445,7 @@ export default class Daemon {
|
|
|
1342
1445
|
await this.#configuration.capture("wait", () => TurnDispositionHandler.configuredWaitSeconds());
|
|
1343
1446
|
await this.#configuration.capture("execution", () => Exec.validateConfiguration());
|
|
1344
1447
|
this.#retention = await this.#configuration.capture("retention", () => new Retention(this.#db, retentionPolicy()));
|
|
1448
|
+
this.#clientOperations = new AbortController();
|
|
1345
1449
|
this.#started = true;
|
|
1346
1450
|
// {§db-space-reclamation} — the file is brought to the policy's auto-vacuum mode before any work.
|
|
1347
1451
|
if (this.#retention !== null) {
|
|
@@ -1426,6 +1530,18 @@ export default class Daemon {
|
|
|
1426
1530
|
this.#moduleLifetimes.push(module);
|
|
1427
1531
|
await module.setup?.(setupSeam);
|
|
1428
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
|
+
}
|
|
1429
1545
|
await this.#schemes.ready();
|
|
1430
1546
|
this.#residency.publish();
|
|
1431
1547
|
await this.#recoverLifecycle();
|
|
@@ -1467,15 +1583,7 @@ export default class Daemon {
|
|
|
1467
1583
|
this.#http.admit();
|
|
1468
1584
|
}
|
|
1469
1585
|
async #recoverLifecycle() {
|
|
1470
|
-
await this.#db.
|
|
1471
|
-
await this.#db.recovery_settle_open_provider_requests.run({});
|
|
1472
|
-
await this.#db.recovery_fail_open_model_calls.run({});
|
|
1473
|
-
await this.#db.recovery_fail_open_turns.run({});
|
|
1474
|
-
await this.#db.recovery_fail_ownerless_proposals.run({});
|
|
1475
|
-
await this.#db.recovery_remove_ownerless_client_interactions.run({});
|
|
1476
|
-
await this.#db.recovery_error_orphan_subscription_channels.run({});
|
|
1477
|
-
await this.#db.recovery_fail_orphan_subscriptions.run({});
|
|
1478
|
-
await this.#db.recovery_resume_unblocked_parks.run({});
|
|
1586
|
+
await this.#db.recovery_settle_process_owners({});
|
|
1479
1587
|
const orphanSources = await this.#db.recovery_orphan_message_sources.all({});
|
|
1480
1588
|
const invalidOrphan = orphanSources.find(({ origin }) => origin !== "model");
|
|
1481
1589
|
if (invalidOrphan !== undefined) {
|
|
@@ -1523,6 +1631,7 @@ export default class Daemon {
|
|
|
1523
1631
|
this.#engine.cancelAllProposals("daemon_stopping");
|
|
1524
1632
|
this.#engine.cancelDerivations(derivationAbort);
|
|
1525
1633
|
this.#drains.beginStop("daemon_stopping");
|
|
1634
|
+
this.#clientOperations.abort("daemon_stopping");
|
|
1526
1635
|
this.#residency.beginStop();
|
|
1527
1636
|
const shutdown = deadline ?? new StopDeadline();
|
|
1528
1637
|
// Stop accepting external work immediately, but do not await listener
|
|
@@ -1555,6 +1664,10 @@ export default class Daemon {
|
|
|
1555
1664
|
// process may exit with the WAL in place (SQLite recovers) rather than
|
|
1556
1665
|
// leak as a live-but-wedged tree.
|
|
1557
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());
|
|
1558
1671
|
// A boundary publication queued behind the last turn settles before
|
|
1559
1672
|
// modules close and before the DB goes away ({§functionality-publication}).
|
|
1560
1673
|
const functionalityResult = await shutdown.settle("functionality publications", () => this.#functionality.settle());
|
|
@@ -1578,8 +1691,10 @@ export default class Daemon {
|
|
|
1578
1691
|
const closeErrors = [
|
|
1579
1692
|
moduleStopResult,
|
|
1580
1693
|
moduleCloseResult,
|
|
1694
|
+
residencyResult,
|
|
1581
1695
|
functionalityResult,
|
|
1582
1696
|
drainResult,
|
|
1697
|
+
clientResult,
|
|
1583
1698
|
streamingResult,
|
|
1584
1699
|
derivationResult,
|
|
1585
1700
|
collectResult,
|