@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.
Files changed (328) hide show
  1. package/.env.defaults +3 -3
  2. package/INSTALL.md +6 -0
  3. package/README.md +2 -2
  4. package/SPEC.md +489 -186
  5. package/dist/build-info.json +1 -1
  6. package/dist/completions/_plurnk-service +1 -1
  7. package/dist/completions/plurnk-service.bash +1 -1
  8. package/dist/completions/plurnk-service.fish +2 -0
  9. package/dist/content/read-projector.js +1 -1
  10. package/dist/content/read-projector.js.map +1 -1
  11. package/dist/core/AdministrativeLoop.d.ts +1 -1
  12. package/dist/core/AdministrativeLoop.d.ts.map +1 -1
  13. package/dist/core/AdministrativeLoop.js +4 -1
  14. package/dist/core/AdministrativeLoop.js.map +1 -1
  15. package/dist/core/AdmittedTurnExecutor.d.ts.map +1 -1
  16. package/dist/core/AdmittedTurnExecutor.js +8 -0
  17. package/dist/core/AdmittedTurnExecutor.js.map +1 -1
  18. package/dist/core/BareBatchRunner.js +2 -1
  19. package/dist/core/BareBatchRunner.js.map +1 -1
  20. package/dist/core/BudgetReadout.d.ts +1 -3
  21. package/dist/core/BudgetReadout.d.ts.map +1 -1
  22. package/dist/core/BudgetReadout.js +43 -46
  23. package/dist/core/BudgetReadout.js.map +1 -1
  24. package/dist/core/ChannelWrite.d.ts +10 -0
  25. package/dist/core/ChannelWrite.d.ts.map +1 -1
  26. package/dist/core/ChannelWrite.js +10 -0
  27. package/dist/core/ChannelWrite.js.map +1 -1
  28. package/dist/core/ChannelWrite.sql +14 -2
  29. package/dist/core/ClientInteractions.d.ts.map +1 -1
  30. package/dist/core/ClientInteractions.js +2 -1
  31. package/dist/core/ClientInteractions.js.map +1 -1
  32. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  33. package/dist/core/CoreSchemeServices.js +6 -0
  34. package/dist/core/CoreSchemeServices.js.map +1 -1
  35. package/dist/core/DataStatementRunner.js +4 -6
  36. package/dist/core/DataStatementRunner.js.map +1 -1
  37. package/dist/core/Dispatcher.d.ts +3 -7
  38. package/dist/core/Dispatcher.d.ts.map +1 -1
  39. package/dist/core/Dispatcher.js +53 -73
  40. package/dist/core/Dispatcher.js.map +1 -1
  41. package/dist/core/Dispatcher.sql +2 -2
  42. package/dist/core/EditMutations.js +1 -0
  43. package/dist/core/EditMutations.js.map +1 -1
  44. package/dist/core/Engine.d.ts +8 -14
  45. package/dist/core/Engine.d.ts.map +1 -1
  46. package/dist/core/Engine.js +22 -24
  47. package/dist/core/Engine.js.map +1 -1
  48. package/dist/core/Engine.sql +6 -6
  49. package/dist/core/ExecutorRegistry.d.ts +0 -5
  50. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  51. package/dist/core/ExecutorRegistry.js +10 -18
  52. package/dist/core/ExecutorRegistry.js.map +1 -1
  53. package/dist/core/FileAddress.js +1 -1
  54. package/dist/core/FileAddress.js.map +1 -1
  55. package/dist/core/FilePublication.js +10 -74
  56. package/dist/core/FilePublication.js.map +1 -1
  57. package/dist/core/FileSnapshot.js +36 -100
  58. package/dist/core/FileSnapshot.js.map +1 -1
  59. package/dist/core/HostPaths.d.ts +1 -0
  60. package/dist/core/HostPaths.d.ts.map +1 -1
  61. package/dist/core/HostPaths.js +11 -1
  62. package/dist/core/HostPaths.js.map +1 -1
  63. package/dist/core/KillHandler.js +3 -1
  64. package/dist/core/KillHandler.js.map +1 -1
  65. package/dist/core/LiveSubscription.js +1 -1
  66. package/dist/core/LiveSubscription.js.map +1 -1
  67. package/dist/core/LogBody.js +1 -1
  68. package/dist/core/LogBody.js.map +1 -1
  69. package/dist/core/LogWriter.js +1 -1
  70. package/dist/core/LogWriter.js.map +1 -1
  71. package/dist/core/LoopDriver.d.ts +3 -3
  72. package/dist/core/LoopDriver.d.ts.map +1 -1
  73. package/dist/core/LoopDriver.js +15 -13
  74. package/dist/core/LoopDriver.js.map +1 -1
  75. package/dist/core/LoopLifecycle.d.ts +5 -0
  76. package/dist/core/LoopLifecycle.d.ts.map +1 -1
  77. package/dist/core/LoopLifecycle.js +14 -1
  78. package/dist/core/LoopLifecycle.js.map +1 -1
  79. package/dist/core/LoopLifecycle.sql +11 -2
  80. package/dist/core/MembershipMaterialization.js +2 -2
  81. package/dist/core/MembershipMaterialization.js.map +1 -1
  82. package/dist/core/MessageResources.d.ts +3 -0
  83. package/dist/core/MessageResources.d.ts.map +1 -1
  84. package/dist/core/MessageResources.js +15 -1
  85. package/dist/core/MessageResources.js.map +1 -1
  86. package/dist/core/OperatorConfig.d.ts +1 -2
  87. package/dist/core/OperatorConfig.d.ts.map +1 -1
  88. package/dist/core/OperatorConfig.js +26 -92
  89. package/dist/core/OperatorConfig.js.map +1 -1
  90. package/dist/core/PacketBuilder.d.ts.map +1 -1
  91. package/dist/core/PacketBuilder.js +21 -18
  92. package/dist/core/PacketBuilder.js.map +1 -1
  93. package/dist/core/PacketBuilder.sql +18 -5
  94. package/dist/core/ProblemLog.d.ts +1 -0
  95. package/dist/core/ProblemLog.d.ts.map +1 -1
  96. package/dist/core/ProblemLog.js +2 -2
  97. package/dist/core/ProblemLog.js.map +1 -1
  98. package/dist/core/ProposalLifecycle.d.ts +1 -0
  99. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  100. package/dist/core/ProposalLifecycle.js +14 -8
  101. package/dist/core/ProposalLifecycle.js.map +1 -1
  102. package/dist/core/ProposalPolicies.d.ts +4 -1
  103. package/dist/core/ProposalPolicies.d.ts.map +1 -1
  104. package/dist/core/ProposalPolicies.js +5 -2
  105. package/dist/core/ProposalPolicies.js.map +1 -1
  106. package/dist/core/ResourceMutations.js +2 -0
  107. package/dist/core/ResourceMutations.js.map +1 -1
  108. package/dist/core/ResourceSelector.js +1 -1
  109. package/dist/core/ResourceSelector.js.map +1 -1
  110. package/dist/core/SchemeRegistry.js +1 -1
  111. package/dist/core/SchemeRegistry.js.map +1 -1
  112. package/dist/core/ServiceHelp.js +1 -1
  113. package/dist/core/ServiceHelp.js.map +1 -1
  114. package/dist/core/ServiceTeardown.js +9 -12
  115. package/dist/core/ServiceTeardown.js.map +1 -1
  116. package/dist/core/StrikeRail.d.ts +2 -1
  117. package/dist/core/StrikeRail.d.ts.map +1 -1
  118. package/dist/core/StrikeRail.js +11 -6
  119. package/dist/core/StrikeRail.js.map +1 -1
  120. package/dist/core/TerminalResult.js +1 -1
  121. package/dist/core/TerminalResult.js.map +1 -1
  122. package/dist/core/TurnDispositionHandler.d.ts +1 -1
  123. package/dist/core/TurnDispositionHandler.d.ts.map +1 -1
  124. package/dist/core/TurnDispositionHandler.js +8 -5
  125. package/dist/core/TurnDispositionHandler.js.map +1 -1
  126. package/dist/core/TurnMaterialization.js +6 -4
  127. package/dist/core/TurnMaterialization.js.map +1 -1
  128. package/dist/core/TurnMaterialization.sql +1 -1
  129. package/dist/core/TurnOps.js +1 -0
  130. package/dist/core/TurnOps.js.map +1 -1
  131. package/dist/core/TurnRunner.d.ts +4 -2
  132. package/dist/core/TurnRunner.d.ts.map +1 -1
  133. package/dist/core/TurnRunner.js +63 -51
  134. package/dist/core/TurnRunner.js.map +1 -1
  135. package/dist/core/WorkerControlHandler.d.ts +2 -0
  136. package/dist/core/WorkerControlHandler.d.ts.map +1 -1
  137. package/dist/core/WorkerControlHandler.js +84 -16
  138. package/dist/core/WorkerControlHandler.js.map +1 -1
  139. package/dist/core/WorkerName.d.ts +0 -4
  140. package/dist/core/WorkerName.d.ts.map +1 -1
  141. package/dist/core/WorkerName.js +5 -30
  142. package/dist/core/WorkerName.js.map +1 -1
  143. package/dist/core/WorkerName.sql +4 -20
  144. package/dist/core/WorkerOwners.d.ts +1 -0
  145. package/dist/core/WorkerOwners.d.ts.map +1 -1
  146. package/dist/core/WorkerOwners.js +6 -0
  147. package/dist/core/WorkerOwners.js.map +1 -1
  148. package/dist/core/WorkerOwners.sql +7 -0
  149. package/dist/core/WorkspaceGate.d.ts +3 -1
  150. package/dist/core/WorkspaceGate.d.ts.map +1 -1
  151. package/dist/core/WorkspaceGate.js +59 -10
  152. package/dist/core/WorkspaceGate.js.map +1 -1
  153. package/dist/core/caps/SchemeCtxImpl.d.ts +3 -0
  154. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  155. package/dist/core/caps/SchemeCtxImpl.js +14 -0
  156. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  157. package/dist/core/env-catalog.js +1 -1
  158. package/dist/core/env-catalog.js.map +1 -1
  159. package/dist/core/fork.js +1 -1
  160. package/dist/core/fork.js.map +1 -1
  161. package/dist/core/fork.sql +7 -6
  162. package/dist/core/mutation-types.d.ts +1 -0
  163. package/dist/core/mutation-types.d.ts.map +1 -1
  164. package/dist/core/mutation-types.js +4 -1
  165. package/dist/core/mutation-types.js.map +1 -1
  166. package/dist/core/namespace.js +1 -1
  167. package/dist/core/namespace.js.map +1 -1
  168. package/dist/core/packet-wire.d.ts +1 -1
  169. package/dist/core/packet-wire.d.ts.map +1 -1
  170. package/dist/core/packet-wire.js +24 -15
  171. package/dist/core/packet-wire.js.map +1 -1
  172. package/dist/core/plurnk-uri.d.ts +1 -1
  173. package/dist/core/plurnk-uri.d.ts.map +1 -1
  174. package/dist/core/plurnk-uri.js +1 -1
  175. package/dist/core/plurnk-uri.js.map +1 -1
  176. package/dist/core/scheme-types.d.ts +5 -0
  177. package/dist/core/scheme-types.d.ts.map +1 -1
  178. package/dist/core/turn-signals.d.ts +4 -0
  179. package/dist/core/turn-signals.d.ts.map +1 -1
  180. package/dist/core/turn-signals.js +1 -0
  181. package/dist/core/turn-signals.js.map +1 -1
  182. package/dist/core/workspace-settings.js +1 -1
  183. package/dist/core/workspace-settings.js.map +1 -1
  184. package/dist/evidence/EvidenceReader.js +5 -5
  185. package/dist/evidence/EvidenceReader.js.map +1 -1
  186. package/dist/man/plurnk-service.1 +27 -6
  187. package/dist/schemes/Env.d.ts +7 -3
  188. package/dist/schemes/Env.d.ts.map +1 -1
  189. package/dist/schemes/Env.js +12 -6
  190. package/dist/schemes/Env.js.map +1 -1
  191. package/dist/schemes/Exec.d.ts.map +1 -1
  192. package/dist/schemes/Exec.js +37 -25
  193. package/dist/schemes/Exec.js.map +1 -1
  194. package/dist/schemes/ExecOutputScheme.js +3 -3
  195. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  196. package/dist/schemes/ExecutionInput.d.ts +2 -1
  197. package/dist/schemes/ExecutionInput.d.ts.map +1 -1
  198. package/dist/schemes/ExecutionInput.js +45 -7
  199. package/dist/schemes/ExecutionInput.js.map +1 -1
  200. package/dist/schemes/File.d.ts.map +1 -1
  201. package/dist/schemes/File.js +32 -23
  202. package/dist/schemes/File.js.map +1 -1
  203. package/dist/schemes/Log.d.ts.map +1 -1
  204. package/dist/schemes/Log.js +23 -5
  205. package/dist/schemes/Log.js.map +1 -1
  206. package/dist/schemes/Log.sql +24 -1
  207. package/dist/schemes/ResourceTreeScheme.js +2 -5
  208. package/dist/schemes/ResourceTreeScheme.js.map +1 -1
  209. package/dist/schemes/TurnSource.js +1 -1
  210. package/dist/schemes/TurnSource.js.map +1 -1
  211. package/dist/schemes/Worker.d.ts.map +1 -1
  212. package/dist/schemes/Worker.js +10 -3
  213. package/dist/schemes/Worker.js.map +1 -1
  214. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  215. package/dist/schemes/_entry-crud.js +2 -4
  216. package/dist/schemes/_entry-crud.js.map +1 -1
  217. package/dist/schemes/_entry-crud.sql +5 -3
  218. package/dist/schemes/_entry-find.js +3 -3
  219. package/dist/schemes/_entry-find.js.map +1 -1
  220. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  221. package/dist/schemes/_entry-manifest.js +3 -3
  222. package/dist/schemes/_entry-manifest.js.map +1 -1
  223. package/dist/schemes/_entry-manifest.sql +2 -0
  224. package/dist/schemes/_entry-ops.js +1 -1
  225. package/dist/schemes/_entry-ops.js.map +1 -1
  226. package/dist/schemes/_entry-send.js +1 -1
  227. package/dist/schemes/_entry-send.js.map +1 -1
  228. package/dist/server/ClientReads.d.ts +1 -0
  229. package/dist/server/ClientReads.d.ts.map +1 -1
  230. package/dist/server/ClientReads.js +7 -2
  231. package/dist/server/ClientReads.js.map +1 -1
  232. package/dist/server/Credentials.d.ts +48 -0
  233. package/dist/server/Credentials.d.ts.map +1 -0
  234. package/dist/server/Credentials.js +203 -0
  235. package/dist/server/Credentials.js.map +1 -0
  236. package/dist/server/Daemon.d.ts +44 -39
  237. package/dist/server/Daemon.d.ts.map +1 -1
  238. package/dist/server/Daemon.js +287 -172
  239. package/dist/server/Daemon.js.map +1 -1
  240. package/dist/server/DrainSupervisor.d.ts +8 -3
  241. package/dist/server/DrainSupervisor.d.ts.map +1 -1
  242. package/dist/server/DrainSupervisor.js +44 -32
  243. package/dist/server/DrainSupervisor.js.map +1 -1
  244. package/dist/server/EnvFunctionality.d.ts.map +1 -1
  245. package/dist/server/EnvFunctionality.js +2 -1
  246. package/dist/server/EnvFunctionality.js.map +1 -1
  247. package/dist/server/Functionality.d.ts +4 -2
  248. package/dist/server/Functionality.d.ts.map +1 -1
  249. package/dist/server/Functionality.js +192 -96
  250. package/dist/server/Functionality.js.map +1 -1
  251. package/dist/server/FunctionalityManager.d.ts +1 -4
  252. package/dist/server/FunctionalityManager.d.ts.map +1 -1
  253. package/dist/server/FunctionalityManager.js +6 -6
  254. package/dist/server/FunctionalityManager.js.map +1 -1
  255. package/dist/server/HttpListener.d.ts +1 -1
  256. package/dist/server/HttpListener.d.ts.map +1 -1
  257. package/dist/server/HttpListener.js.map +1 -1
  258. package/dist/server/MembersFunctionality.d.ts +7 -8
  259. package/dist/server/MembersFunctionality.d.ts.map +1 -1
  260. package/dist/server/MembersFunctionality.js +90 -56
  261. package/dist/server/MembersFunctionality.js.map +1 -1
  262. package/dist/server/MessageAbandonment.d.ts +7 -0
  263. package/dist/server/MessageAbandonment.d.ts.map +1 -0
  264. package/dist/server/MessageAbandonment.js +57 -0
  265. package/dist/server/MessageAbandonment.js.map +1 -0
  266. package/dist/server/ModuleHost.d.ts +2 -1
  267. package/dist/server/ModuleHost.d.ts.map +1 -1
  268. package/dist/server/PlurnkSkill.js +4 -7
  269. package/dist/server/PlurnkSkill.js.map +1 -1
  270. package/dist/server/Rollback.d.ts +29 -0
  271. package/dist/server/Rollback.d.ts.map +1 -0
  272. package/dist/server/Rollback.js +111 -0
  273. package/dist/server/Rollback.js.map +1 -0
  274. package/dist/server/WorkerModelResolver.js +1 -0
  275. package/dist/server/WorkerModelResolver.js.map +1 -1
  276. package/dist/server/WorkerReferences.d.ts +14 -0
  277. package/dist/server/WorkerReferences.d.ts.map +1 -0
  278. package/dist/server/WorkerReferences.js +24 -0
  279. package/dist/server/WorkerReferences.js.map +1 -0
  280. package/dist/server/WorkerReferences.sql +31 -0
  281. package/dist/server/WorkspaceCapabilities.d.ts +2 -0
  282. package/dist/server/WorkspaceCapabilities.d.ts.map +1 -1
  283. package/dist/server/WorkspaceCapabilities.js +35 -18
  284. package/dist/server/WorkspaceCapabilities.js.map +1 -1
  285. package/dist/server/WorkspacePlugins.js +1 -0
  286. package/dist/server/WorkspacePlugins.js.map +1 -1
  287. package/dist/server/WorkspaceResidency.d.ts +3 -1
  288. package/dist/server/WorkspaceResidency.d.ts.map +1 -1
  289. package/dist/server/WorkspaceResidency.js +12 -5
  290. package/dist/server/WorkspaceResidency.js.map +1 -1
  291. package/dist/server/client-input.d.ts +2 -0
  292. package/dist/server/client-input.d.ts.map +1 -1
  293. package/dist/server/client-input.js +28 -0
  294. package/dist/server/client-input.js.map +1 -1
  295. package/dist/server/drain.sql +46 -40
  296. package/dist/server/envelope.d.ts +5 -9
  297. package/dist/server/envelope.d.ts.map +1 -1
  298. package/dist/server/envelope.js +21 -84
  299. package/dist/server/envelope.js.map +1 -1
  300. package/dist/server/envelope.sql +17 -17
  301. package/dist/server/lifecycle-recovery.sql +5 -15
  302. package/dist/server/logEntry.d.ts.map +1 -1
  303. package/dist/server/logEntry.js +6 -0
  304. package/dist/server/logEntry.js.map +1 -1
  305. package/dist/server/logEntry.sql +94 -6
  306. package/dist/server/module-discovery.d.ts +1 -1
  307. package/dist/server/module-discovery.d.ts.map +1 -1
  308. package/dist/server/module-discovery.js +1 -0
  309. package/dist/server/module-discovery.js.map +1 -1
  310. package/dist/server/seam-loop.sql +15 -7
  311. package/dist/server/seam-proposal-list.sql +10 -4
  312. package/dist/service.d.ts.map +1 -1
  313. package/dist/service.js +7 -1
  314. package/dist/service.js.map +1 -1
  315. package/docs/members.md +20 -13
  316. package/migrations/002_workers.sql +2 -2
  317. package/migrations/006_log.sql +1 -1
  318. package/migrations/018_worker_owners.sql +1 -1
  319. package/migrations/026_message_obligations.sql +32 -0
  320. package/migrations/027_members_identity.sql +20 -0
  321. package/migrations/028_mutation_patch.sql +20 -0
  322. package/migrations/029_default_worker.sql +9 -0
  323. package/migrations/030_client_workers.sql +105 -0
  324. package/migrations/031_message_openers.sql +83 -0
  325. package/migrations/032_worker_references.sql +29 -0
  326. package/package.json +24 -24
  327. package/plurnk.nginx.conf +71 -0
  328. package/plurnk.service +2 -0
@@ -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
- 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"))));
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.#workerPromptSource(workspaceId, sender.worker_id, workerId);
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
- const { action, loopId } = await ContentUse.read(this.#db, async () => {
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
- return { action, loopId };
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
- acquireWorkspaceTurn: async (workspaceId, workerId, signal) => this.#workspaceGate.acquireTurn(workspaceId, workerId, signal),
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
- async #workerPromptSource(workspaceId, sourceWorkerId, targetWorkerId) {
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
- executorJsonBodyTags(workspaceId) {
805
- return this.#engine.executorJsonBodyTags(workspaceId);
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 workerId = ClientInput.assertId("operation.dispatch-batch", "workerId", args.workerId);
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.#assertWorkerOwned(workspaceId, workerId);
814
- // {§actor-boundary-attached-functionality} The client retains its journal;
815
- // the workspace supplies policy and the shared runtime environment.
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 clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
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
- const results = [];
821
- for (const statement of statements) {
822
- results.push(await this.#dispatchClientStatement({ workspaceId, workerId, loopId: clientLoopId, statement }));
823
- }
824
- await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
825
- return results;
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
- await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
829
- throw error;
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
- const release = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
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", onDispatch: (logEntryId) => { entryIds.push(logEntryId); }
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
- alias: a.alias, provider: a.provider, model: a.model, active: isActive,
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, worldless local projection.
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() { return Envelope.listWorkspaces(this.#db); }
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.assertOptionalWorkerName("worker.read", "name", args.identity.name)
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-descendants}
1008
- async descendantAccounting(args) {
1009
- const workspaceId = ClientInput.assertId("worker.descendants", "workspaceId", args.workspaceId);
1010
- const workerId = ClientInput.assertId("worker.descendants", "workerId", args.workerId);
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
- const loopId = args.loopId === null ? null : ClientInput.assertId("worker.descendants", "loopId", args.loopId);
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-conversation loop seeds, newest-first.
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("share: this daemon was started without its database path");
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 name = ClientInput.assertOptionalName("workspace.create", "name", args.name);
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 envelope = await Envelope.createClientEnvelope(this.#db, { name, projectRoot, settings });
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
- // attachToWorkspace owns the reserved-name + worker-ownership invariants; the seam just delegates.
1075
- const workspaceId = ClientInput.assertId("workspace.attach", "workspaceId", args.workspaceId);
1076
- const workerId = args.workerId === undefined
1077
- ? undefined
1078
- : ClientInput.assertId("workspace.attach", "workerId", args.workerId);
1079
- const workerName = ClientInput.assertOptionalWorkerName("workspace.attach", "workerName", args.workerName);
1080
- return Envelope.attachToWorkspace(this.#db, workspaceId, { workerId, workerName });
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.assertOptionalName("workspace.rename", "name", name);
1194
+ const checkedName = ClientInput.assertOptionalWorkspaceName("workspace.rename", "name", name);
1085
1195
  if (checkedName === undefined)
1086
- throw new Error("ClientInput.assertOptionalName accepted a required name as undefined");
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-conversation-worker}: a fresh conversation is a model-origin worker with an empty private log.
1090
- // AG-UI threads map to these workers while the workspace world remains shared ({§machine-processes}).
1091
- async createConversationWorker(args) {
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 the workspace world.
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
- const owner = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
1125
- if (owner === undefined) {
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 !== "worldless" && scope !== "workspace" && scope !== "worker") {
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 === "worldless" && registration.residency !== "none") {
1228
- throw new Error(`worldless module action '${name}' cannot require workspace residency`);
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 !== "worldless") {
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.recovery_fail_active_loops.run({});
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,