@plurnk/plurnk-service 1.8.0 → 1.9.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 (263) hide show
  1. package/.env.defaults +20 -0
  2. package/INSTALL.md +14 -16
  3. package/README.md +1 -1
  4. package/SPEC.md +635 -296
  5. package/digest-sql/channel-dispositions/dispositions.sql +2 -2
  6. package/digest-sql/entry-dispositions/dispositions.sql +2 -2
  7. package/dist/Paths.d.ts +1 -2
  8. package/dist/Paths.d.ts.map +1 -1
  9. package/dist/Paths.js +1 -9
  10. package/dist/Paths.js.map +1 -1
  11. package/dist/build-info.json +1 -1
  12. package/dist/core/ChannelWrite.d.ts +8 -3
  13. package/dist/core/ChannelWrite.d.ts.map +1 -1
  14. package/dist/core/ChannelWrite.js +40 -8
  15. package/dist/core/ChannelWrite.js.map +1 -1
  16. package/dist/core/ChannelWrite.sql +17 -4
  17. package/dist/core/CoreSchemeServices.d.ts +7 -3
  18. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  19. package/dist/core/CoreSchemeServices.js +8 -1
  20. package/dist/core/CoreSchemeServices.js.map +1 -1
  21. package/dist/core/Dispatcher.d.ts +9 -1
  22. package/dist/core/Dispatcher.d.ts.map +1 -1
  23. package/dist/core/Dispatcher.js +321 -221
  24. package/dist/core/Dispatcher.js.map +1 -1
  25. package/dist/core/Engine.d.ts +13 -4
  26. package/dist/core/Engine.d.ts.map +1 -1
  27. package/dist/core/Engine.js +44 -22
  28. package/dist/core/Engine.js.map +1 -1
  29. package/dist/core/Engine.sql +101 -81
  30. package/dist/core/EntryAddressBinding.d.ts +32 -0
  31. package/dist/core/EntryAddressBinding.d.ts.map +1 -0
  32. package/dist/core/EntryAddressBinding.js +93 -0
  33. package/dist/core/EntryAddressBinding.js.map +1 -0
  34. package/dist/core/ExecutorRegistry.d.ts +4 -4
  35. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  36. package/dist/core/ExecutorRegistry.js +28 -28
  37. package/dist/core/ExecutorRegistry.js.map +1 -1
  38. package/dist/core/LogBody.d.ts.map +1 -1
  39. package/dist/core/LogBody.js +19 -1
  40. package/dist/core/LogBody.js.map +1 -1
  41. package/dist/core/OperatorConfig.d.ts.map +1 -1
  42. package/dist/core/OperatorConfig.js +5 -2
  43. package/dist/core/OperatorConfig.js.map +1 -1
  44. package/dist/core/OverflowTurn.d.ts.map +1 -1
  45. package/dist/core/OverflowTurn.js +5 -1
  46. package/dist/core/OverflowTurn.js.map +1 -1
  47. package/dist/core/Owner.js +1 -1
  48. package/dist/core/Owner.js.map +1 -1
  49. package/dist/core/PacketBuilder.d.ts +5 -1
  50. package/dist/core/PacketBuilder.d.ts.map +1 -1
  51. package/dist/core/PacketBuilder.js +21 -11
  52. package/dist/core/PacketBuilder.js.map +1 -1
  53. package/dist/core/ProposalLifecycle.d.ts +4 -1
  54. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  55. package/dist/core/ProposalLifecycle.js +85 -26
  56. package/dist/core/ProposalLifecycle.js.map +1 -1
  57. package/dist/core/ReasoningEvent.d.ts +13 -0
  58. package/dist/core/ReasoningEvent.d.ts.map +1 -0
  59. package/dist/core/ReasoningEvent.js +6 -0
  60. package/dist/core/ReasoningEvent.js.map +1 -0
  61. package/dist/core/ResourceMutations.d.ts +15 -19
  62. package/dist/core/ResourceMutations.d.ts.map +1 -1
  63. package/dist/core/ResourceMutations.js +113 -38
  64. package/dist/core/ResourceMutations.js.map +1 -1
  65. package/dist/core/SchemeRegistry.d.ts +12 -11
  66. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  67. package/dist/core/SchemeRegistry.js +52 -47
  68. package/dist/core/SchemeRegistry.js.map +1 -1
  69. package/dist/core/ToolResources.d.ts.map +1 -1
  70. package/dist/core/ToolResources.js +20 -19
  71. package/dist/core/ToolResources.js.map +1 -1
  72. package/dist/core/Turn.d.ts +1 -1
  73. package/dist/core/Turn.d.ts.map +1 -1
  74. package/dist/core/TurnOps.d.ts +2 -2
  75. package/dist/core/TurnOps.d.ts.map +1 -1
  76. package/dist/core/TurnOps.js +14 -4
  77. package/dist/core/TurnOps.js.map +1 -1
  78. package/dist/core/TurnRunner.d.ts +46 -3
  79. package/dist/core/TurnRunner.d.ts.map +1 -1
  80. package/dist/core/TurnRunner.js +165 -78
  81. package/dist/core/TurnRunner.js.map +1 -1
  82. package/dist/core/WorkerName.d.ts +1 -0
  83. package/dist/core/WorkerName.d.ts.map +1 -1
  84. package/dist/core/WorkerName.js +2 -1
  85. package/dist/core/WorkerName.js.map +1 -1
  86. package/dist/core/WorkerName.sql +17 -2
  87. package/dist/core/WorkspaceGate.js +1 -1
  88. package/dist/core/WorkspaceGate.js.map +1 -1
  89. package/dist/core/caps/CapsResolve.d.ts +2 -2
  90. package/dist/core/caps/CapsResolve.d.ts.map +1 -1
  91. package/dist/core/caps/CapsResolve.js +5 -7
  92. package/dist/core/caps/CapsResolve.js.map +1 -1
  93. package/dist/core/caps/DbChannelCaps.d.ts +1 -1
  94. package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
  95. package/dist/core/caps/DbChannelCaps.js +13 -11
  96. package/dist/core/caps/DbChannelCaps.js.map +1 -1
  97. package/dist/core/caps/DbEntryCaps.d.ts +5 -5
  98. package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
  99. package/dist/core/caps/DbEntryCaps.js +27 -30
  100. package/dist/core/caps/DbEntryCaps.js.map +1 -1
  101. package/dist/core/caps/DbNotifyCaps.d.ts +1 -1
  102. package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
  103. package/dist/core/caps/DbNotifyCaps.js +5 -3
  104. package/dist/core/caps/DbNotifyCaps.js.map +1 -1
  105. package/dist/core/caps/DbSubscriptionCaps.d.ts +3 -3
  106. package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
  107. package/dist/core/caps/DbSubscriptionCaps.js +16 -26
  108. package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
  109. package/dist/core/caps/SchemeCtxImpl.d.ts +6 -4
  110. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  111. package/dist/core/caps/SchemeCtxImpl.js +37 -5
  112. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  113. package/dist/core/file-creation-policy.d.ts +9 -0
  114. package/dist/core/file-creation-policy.d.ts.map +1 -0
  115. package/dist/core/file-creation-policy.js +25 -0
  116. package/dist/core/file-creation-policy.js.map +1 -0
  117. package/dist/core/fork.d.ts +4 -1
  118. package/dist/core/fork.d.ts.map +1 -1
  119. package/dist/core/fork.js +27 -15
  120. package/dist/core/fork.js.map +1 -1
  121. package/dist/core/fork.sql +35 -22
  122. package/dist/core/git-membership.d.ts +19 -1
  123. package/dist/core/git-membership.d.ts.map +1 -1
  124. package/dist/core/git-membership.js +225 -50
  125. package/dist/core/git-membership.js.map +1 -1
  126. package/dist/core/packet-wire.d.ts.map +1 -1
  127. package/dist/core/packet-wire.js +28 -19
  128. package/dist/core/packet-wire.js.map +1 -1
  129. package/dist/core/plurnk-uri.d.ts +13 -2
  130. package/dist/core/plurnk-uri.d.ts.map +1 -1
  131. package/dist/core/plurnk-uri.js +59 -21
  132. package/dist/core/plurnk-uri.js.map +1 -1
  133. package/dist/core/scheme-types.d.ts +1 -0
  134. package/dist/core/scheme-types.d.ts.map +1 -1
  135. package/dist/core/worker-ops.sql +10 -6
  136. package/dist/core/worker-settings.d.ts +1 -0
  137. package/dist/core/worker-settings.d.ts.map +1 -1
  138. package/dist/core/worker-settings.js +7 -0
  139. package/dist/core/worker-settings.js.map +1 -1
  140. package/dist/core/workspace-settings.d.ts +2 -0
  141. package/dist/core/workspace-settings.d.ts.map +1 -1
  142. package/dist/core/workspace-settings.js +5 -1
  143. package/dist/core/workspace-settings.js.map +1 -1
  144. package/dist/digest/Digest.d.ts.map +1 -1
  145. package/dist/digest/Digest.js +1 -1
  146. package/dist/digest/Digest.js.map +1 -1
  147. package/dist/digest/digest.sql +1 -1
  148. package/dist/schemes/Exec.d.ts +3 -2
  149. package/dist/schemes/Exec.d.ts.map +1 -1
  150. package/dist/schemes/Exec.js +55 -22
  151. package/dist/schemes/Exec.js.map +1 -1
  152. package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
  153. package/dist/schemes/ExecOutputScheme.js +5 -4
  154. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  155. package/dist/schemes/File.d.ts.map +1 -1
  156. package/dist/schemes/File.js +170 -80
  157. package/dist/schemes/File.js.map +1 -1
  158. package/dist/schemes/Prompt.d.ts +2 -3
  159. package/dist/schemes/Prompt.d.ts.map +1 -1
  160. package/dist/schemes/Prompt.js +4 -7
  161. package/dist/schemes/Prompt.js.map +1 -1
  162. package/dist/schemes/Skill.d.ts.map +1 -1
  163. package/dist/schemes/Skill.js +3 -1
  164. package/dist/schemes/Skill.js.map +1 -1
  165. package/dist/schemes/Worker.d.ts.map +1 -1
  166. package/dist/schemes/Worker.js +95 -49
  167. package/dist/schemes/Worker.js.map +1 -1
  168. package/dist/schemes/_entry-crud.d.ts +5 -5
  169. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  170. package/dist/schemes/_entry-crud.js +22 -17
  171. package/dist/schemes/_entry-crud.js.map +1 -1
  172. package/dist/schemes/_entry-crud.sql +58 -23
  173. package/dist/schemes/_entry-find.d.ts +3 -2
  174. package/dist/schemes/_entry-find.d.ts.map +1 -1
  175. package/dist/schemes/_entry-find.js +26 -25
  176. package/dist/schemes/_entry-find.js.map +1 -1
  177. package/dist/schemes/_entry-find.sql +10 -7
  178. package/dist/schemes/_entry-manifest.d.ts +2 -2
  179. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  180. package/dist/schemes/_entry-manifest.js +6 -5
  181. package/dist/schemes/_entry-manifest.js.map +1 -1
  182. package/dist/schemes/_entry-ops.d.ts +6 -5
  183. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  184. package/dist/schemes/_entry-ops.js +39 -46
  185. package/dist/schemes/_entry-ops.js.map +1 -1
  186. package/dist/schemes/_entry-ops.sql +9 -6
  187. package/dist/schemes/_entry-send.d.ts +2 -2
  188. package/dist/schemes/_entry-send.d.ts.map +1 -1
  189. package/dist/schemes/_entry-send.js +16 -14
  190. package/dist/schemes/_entry-send.js.map +1 -1
  191. package/dist/schemes/_search-index.d.ts.map +1 -1
  192. package/dist/schemes/_search-index.js +2 -0
  193. package/dist/schemes/_search-index.js.map +1 -1
  194. package/dist/server/Daemon.d.ts +42 -10
  195. package/dist/server/Daemon.d.ts.map +1 -1
  196. package/dist/server/Daemon.js +326 -248
  197. package/dist/server/Daemon.js.map +1 -1
  198. package/dist/server/DaemonModule.d.ts +84 -19
  199. package/dist/server/DaemonModule.d.ts.map +1 -1
  200. package/dist/server/DrainSupervisor.d.ts +3 -1
  201. package/dist/server/DrainSupervisor.d.ts.map +1 -1
  202. package/dist/server/DrainSupervisor.js +11 -9
  203. package/dist/server/DrainSupervisor.js.map +1 -1
  204. package/dist/server/Functionality.d.ts +44 -0
  205. package/dist/server/Functionality.d.ts.map +1 -0
  206. package/dist/server/Functionality.js +422 -0
  207. package/dist/server/Functionality.js.map +1 -0
  208. package/dist/server/FunctionalityManager.d.ts +22 -0
  209. package/dist/server/FunctionalityManager.d.ts.map +1 -0
  210. package/dist/server/FunctionalityManager.js +121 -0
  211. package/dist/server/FunctionalityManager.js.map +1 -0
  212. package/dist/server/SkillsFunctionality.d.ts +61 -0
  213. package/dist/server/SkillsFunctionality.d.ts.map +1 -0
  214. package/dist/server/SkillsFunctionality.js +449 -0
  215. package/dist/server/SkillsFunctionality.js.map +1 -0
  216. package/dist/server/WorkerCapabilities.d.ts +21 -0
  217. package/dist/server/WorkerCapabilities.d.ts.map +1 -0
  218. package/dist/server/WorkerCapabilities.js +214 -0
  219. package/dist/server/WorkerCapabilities.js.map +1 -0
  220. package/dist/server/WorkerResidency.d.ts +40 -0
  221. package/dist/server/WorkerResidency.d.ts.map +1 -0
  222. package/dist/server/WorkerResidency.js +283 -0
  223. package/dist/server/WorkerResidency.js.map +1 -0
  224. package/dist/server/client-input.d.ts +1 -0
  225. package/dist/server/client-input.d.ts.map +1 -1
  226. package/dist/server/client-input.js +22 -1
  227. package/dist/server/client-input.js.map +1 -1
  228. package/dist/server/dispatch-as-plurnk.d.ts +2 -2
  229. package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
  230. package/dist/server/dispatch-as-plurnk.js +89 -25
  231. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  232. package/dist/server/drain.sql +11 -6
  233. package/dist/server/envelope.d.ts +6 -1
  234. package/dist/server/envelope.d.ts.map +1 -1
  235. package/dist/server/envelope.js +8 -2
  236. package/dist/server/envelope.js.map +1 -1
  237. package/dist/server/envelope.sql +11 -3
  238. package/dist/server/lifecycle-recovery.sql +13 -16
  239. package/dist/server/loopDocs.d.ts +2 -1
  240. package/dist/server/loopDocs.d.ts.map +1 -1
  241. package/dist/server/loopDocs.js +67 -51
  242. package/dist/server/loopDocs.js.map +1 -1
  243. package/dist/server/loopDocs.sql +14 -10
  244. package/dist/server/module-discovery.d.ts +2 -2
  245. package/dist/server/module-discovery.d.ts.map +1 -1
  246. package/dist/server/module-discovery.js +27 -2
  247. package/dist/server/module-discovery.js.map +1 -1
  248. package/dist/server/seam-entry-read.sql +9 -7
  249. package/dist/server/seam-log-read.sql +1 -1
  250. package/dist/server/seam-loop.sql +10 -1
  251. package/dist/server/seam-proposal-list.sql +4 -2
  252. package/dist/server/worker-capabilities.sql +19 -0
  253. package/dist/service.d.ts.map +1 -1
  254. package/dist/service.js +11 -1
  255. package/dist/service.js.map +1 -1
  256. package/migrations/001_schema.sql +486 -136
  257. package/package.json +34 -32
  258. package/dist/server/skillDocs.d.ts +0 -9
  259. package/dist/server/skillDocs.d.ts.map +0 -1
  260. package/dist/server/skillDocs.js +0 -212
  261. package/dist/server/skillDocs.js.map +0 -1
  262. package/dist/server/skillDocs.sql +0 -14
  263. package/dist/server/workspace-capabilities.sql +0 -19
@@ -19,11 +19,11 @@ import LogEntry from "./logEntry.js";
19
19
  import Envelope from "./envelope.js";
20
20
  import ClientInput from "./client-input.js";
21
21
  import Turn from "../core/Turn.js";
22
- import LoopDocs from "./loopDocs.js";
23
- import SkillDocs from "./skillDocs.js";
22
+ import SkillsFunctionality from "./SkillsFunctionality.js";
24
23
  import GitMembership from "../core/git-membership.js";
25
24
  import Fork from "../core/fork.js";
26
25
  import WorkerName from "../core/WorkerName.js";
26
+ import WorkerControlAddress from "../core/WorkerControlAddress.js";
27
27
  import LoopLifecycle from "../core/LoopLifecycle.js";
28
28
  import { promptLoopPrefix } from "../core/plurnk-uri.js";
29
29
  import { contentWeight } from "../core/content-weight.js";
@@ -33,7 +33,9 @@ import { resolveLoopRoute } from "./loop-model.js";
33
33
  import LoopFlagsReader from "../core/LoopFlagsReader.js";
34
34
  import Results, { OperationFailureError } from "../core/results.js";
35
35
  import WorkspaceGate from "../core/WorkspaceGate.js";
36
+ import WorkerResidency from "./WorkerResidency.js";
36
37
  import BranchBatches from "./BranchBatches.js";
38
+ import Functionality from "./Functionality.js";
37
39
  import { observed, observedSync } from "../observe/spans.js";
38
40
  import { listModelCatalog } from "./model-catalog.js";
39
41
  const clientActionFailure = (error) => {
@@ -64,16 +66,17 @@ export default class Daemon {
64
66
  #nodeModulesPath;
65
67
  #discoveryCwd;
66
68
  #started = false; // {§module-lifecycle}: one discovery/module boot; no listener
67
- #capabilitiesPublished = false;
68
69
  #modules = [];
69
70
  #moduleClosers = [];
70
71
  #moduleActions = new Map();
71
- #workspaceCapabilityProviders = new Map();
72
+ #residency;
73
+ #functionality;
74
+ #skills;
72
75
  // {§methods-event-subscribe} — the broadcast's in-process event source. A transport
73
76
  // module (plurnk-agui) subscribes and fans out to its OWN clients; core emits, never owns
74
77
  // client transport or connection state.
75
78
  #eventSubscribers = new Set();
76
- constructor({ db, schemes, mimetypes, provider, nodeModulesPath, }) {
79
+ constructor({ db, schemes, mimetypes, provider, nodeModulesPath, skills, }) {
77
80
  this.#db = db;
78
81
  this.#lifecycle = new LoopLifecycle(db);
79
82
  this.#schemes = schemes ?? new SchemeRegistry();
@@ -104,6 +107,26 @@ export default class Daemon {
104
107
  });
105
108
  return row !== undefined;
106
109
  });
110
+ // {§module-worker-residency} — residency, activation/cooling, and
111
+ // capability replacement live in their one owner; the Daemon delegates.
112
+ this.#residency = new WorkerResidency({
113
+ db,
114
+ engine: () => this.#engine,
115
+ workspaceGate: this.#workspaceGate,
116
+ normalizeRuntime: (registration) => this.#normalizeRuntime(registration),
117
+ });
118
+ // {§functionality-coordinator} — the one owner above every family
119
+ // adapter; Core hosts it, modules register adapters through the seam.
120
+ this.#functionality = new Functionality({
121
+ registerModuleAction: (registration) => this.registerModuleAction(registration),
122
+ registerWorkerCapabilityProvider: (owner, provider) => this.registerWorkerCapabilityProvider(owner, provider),
123
+ readWorkerModuleState: (workerId, owner) => this.readWorkerModuleState(workerId, owner),
124
+ replaceWorkerCapabilities: (replacement, options) => this.replaceWorkerCapabilities(replacement, options),
125
+ retainWorker: (workerId) => this.#residency.retain(workerId),
126
+ });
127
+ // {§skills-functionality} — Core's own family: standard Agent Skills.
128
+ this.#skills = new SkillsFunctionality({ db, ...skills });
129
+ this.#skills.attach(this.#functionality.register(this.#skills));
107
130
  this.#branchBatches = new BranchBatches(db, this.#workspaceGate, {
108
131
  settleWorkspace: async (workspaceId) => this.#engine.drainWorkspaceDerivations(workspaceId),
109
132
  createChild: async ({ workspaceId, parentWorkerId, parentLoopId, op, name, prompt, flags, origin }) => {
@@ -111,12 +134,13 @@ export default class Daemon {
111
134
  const providerSpec = parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
112
135
  const workerName = WorkerName.assert(name);
113
136
  const workerId = op === "FORK"
114
- ? await Fork.fork(this.#db, parentWorkerId, workerName)
137
+ ? await Fork.fork(this.#db, parentWorkerId, workerName, (scheme) => this.#schemes.entryInheritanceForStoredScheme(scheme, parentWorkerId))
115
138
  : (await this.#db.fork_insert_worker.get({
116
139
  workspace_id: workspaceId,
117
140
  name: workerName,
118
141
  parent_worker_id: parentWorkerId,
119
142
  origin,
143
+ fork_snapshot: 0,
120
144
  }))?.id;
121
145
  if (workerId === undefined)
122
146
  throw new Error("Branch worker insert returned no row");
@@ -128,9 +152,11 @@ export default class Daemon {
128
152
  spawn_model_route_id: null,
129
153
  reasoning_policy: parentPolicy.reasoningPolicy,
130
154
  });
155
+ const source = await this.#workerPromptSource(workspaceId, parentWorkerId, workerId);
131
156
  const loopId = await this.#drains.enqueueFreshLoop({
132
157
  workerId,
133
158
  prompt,
159
+ ...(source === undefined ? {} : { source }),
134
160
  providerSpec,
135
161
  reasoningPolicy: parentPolicy.reasoningPolicy,
136
162
  childProviderSpec: parentPolicy.childProviderSpec,
@@ -164,13 +190,16 @@ export default class Daemon {
164
190
  // model-independent. Request-shaped token facts stay provider-owned.
165
191
  weigh: contentWeight,
166
192
  streamEventNotify: (workspaceId, event) => this.notifyStreamEvent(workspaceId, event),
193
+ reasoningEventNotify: (workspaceId, event) => this.notifyReasoningEvent(workspaceId, event),
167
194
  wakeWorkerNotify: (payload) => this.#drains.notifyWakeWorker(payload),
168
195
  // worker:// loop-start primitive — spawn/fork/irc deliver through
169
196
  // Daemon.inject (active sister → fold; idle → enqueue + drain). The
170
197
  // daemon owns provider + the law-file system prompt; the worker scheme
171
198
  // handler carries neither. Fire-and-forget: the returned drain runs
172
199
  // independently (the sister is its own worker). {§machine-processes}
173
- injectWorker: async ({ workspaceId, workerId, prompt, flags, parentLoopId }) => {
200
+ injectWorker: async ({ workspaceId, workerId, sourceWorkerId, prompt, flags, parentLoopId }) => {
201
+ await this.#assertModelWorker(workspaceId, workerId);
202
+ const source = await this.#workerPromptSource(workspaceId, sourceWorkerId, workerId);
174
203
  const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
175
204
  let providerSpec;
176
205
  let childProviderSpec;
@@ -207,6 +236,7 @@ export default class Daemon {
207
236
  workspaceId,
208
237
  workerId,
209
238
  prompt,
239
+ ...(source === undefined ? {} : { source }),
210
240
  providerSpec,
211
241
  reasoningPolicy,
212
242
  childProviderSpec,
@@ -218,11 +248,11 @@ export default class Daemon {
218
248
  branchWorker: async (args) => this.#branchBatches.enqueue(args),
219
249
  branchCompletionGate: async (workerId) => this.#branchBatches.completionGate(workerId),
220
250
  acquireWorkspaceTurn: async (workspaceId, workerId) => this.#workspaceGate.acquireTurn(workspaceId, workerId),
221
- // {§skills-materialization} — filesystem installers operate out of
222
- // band. Refresh under the workspace turn gate before packet assembly
223
- // so the first subsequent model turn sees their exact result.
224
- workspaceTurnStarting: async ({ workspaceId }) => {
225
- await SkillDocs.refreshIfChanged(this.#engine, this.#db, workspaceId);
251
+ // {§skills-hotload} — filesystem installers operate out of band.
252
+ // Republish under the workspace turn gate before packet assembly so
253
+ // the first subsequent model turn sees their exact result.
254
+ workspaceTurnStarting: async ({ workspaceId, workerId }) => {
255
+ await this.#skills.refreshIfChanged({ workspaceId, workerId });
226
256
  },
227
257
  workspaceTurnCompleted: async ({ turnId }) => {
228
258
  await this.#branchBatches.sealTurn(turnId);
@@ -236,7 +266,7 @@ export default class Daemon {
236
266
  this.#drains = new DrainSupervisor({
237
267
  db,
238
268
  lifecycle: this.#lifecycle,
239
- injectPrompt: (workerId, prompt, openPaths) => this.#engine.inject(workerId, prompt, openPaths),
269
+ injectPrompt: (workerId, prompt, openPaths, source) => this.#engine.inject(workerId, prompt, openPaths, source),
240
270
  assertInjectionCompatibility: async ({ workerId, loopId, providerSpec, providerSpecExplicit, reasoningPolicy, childProviderSpec, turnCeiling, flags, }) => {
241
271
  await this.#assertFoldPosture(workerId, flags, loopId);
242
272
  // An omitted selector keeps the loop's durable provider; only an
@@ -252,21 +282,34 @@ export default class Daemon {
252
282
  },
253
283
  reconcilePrompts: (workerId, endedLoopId) => this.#reconcileOrphanedPrompts(workerId, endedLoopId),
254
284
  runLoop: async ({ workspaceId, workerId, loopId, maxTurns, prompt, systemPrompt, signal, onDispatch, }) => {
255
- const { provider, childProvider } = await this.#providersForLoop(loopId);
256
- return this.#engine.runLoop({
257
- provider,
258
- childProvider,
259
- workspaceId,
260
- workerId,
261
- loopId,
262
- maxTurns,
263
- messages: [
264
- { role: "system", content: systemPrompt },
265
- { role: "user", content: prompt },
266
- ],
267
- signal,
268
- onDispatch,
269
- });
285
+ // The drain callback is the final provider/model boundary. Every
286
+ // admission path, including boot recovery, terminates here.
287
+ await this.#assertModelWorker(workspaceId, workerId);
288
+ const releaseCapabilities = await this.#residency.acquire(workspaceId, workerId);
289
+ try {
290
+ // Worker settings can change while Functionality remains
291
+ // resident. Reconcile the worker-private discovery surface
292
+ // at the same pre-inference boundary used for activation.
293
+ await this.#residency.reconcile(workspaceId, workerId);
294
+ const { provider, childProvider } = await this.#providersForLoop(loopId);
295
+ return await this.#engine.runLoop({
296
+ provider,
297
+ childProvider,
298
+ workspaceId,
299
+ workerId,
300
+ loopId,
301
+ maxTurns,
302
+ messages: [
303
+ { role: "system", content: systemPrompt },
304
+ { role: "user", content: prompt },
305
+ ],
306
+ signal,
307
+ onDispatch,
308
+ });
309
+ }
310
+ finally {
311
+ releaseCapabilities();
312
+ }
270
313
  },
271
314
  loopUsage: (loopId) => this.#engine.loopUsage(loopId),
272
315
  loopAttributions: (loopId) => this.#engine.loopAttributions(loopId),
@@ -333,7 +376,9 @@ export default class Daemon {
333
376
  async runLoop(args) {
334
377
  const workspaceId = ClientInput.assertId("runLoop", "workspaceId", args.workspaceId);
335
378
  const workerId = ClientInput.assertId("runLoop", "workerId", args.workerId);
379
+ await this.#assertModelWorker(workspaceId, workerId);
336
380
  const prompt = ClientInput.assertPrompt("runLoop", args.prompt);
381
+ const source = ClientInput.assertOptionalSource("runLoop", args.source);
337
382
  const requestedMaxTurns = ClientInput.assertMaxTurns("runLoop", args.maxTurns);
338
383
  const openPaths = ClientInput.assertOpenPaths("runLoop", args.openPaths);
339
384
  const selector = ClientInput.assertOptionalSelector("runLoop", "selector", args.selector);
@@ -356,28 +401,6 @@ export default class Daemon {
356
401
  // {§methods-loop-run-child-provider} — the worker's persistent spawn override.
357
402
  const childSelection = await this.#resolveWorkerSpawnModel(workerId, childSelector);
358
403
  const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
359
- // {§machine-processes} — the model NEVER runs in a client-origin worker (its packets would carry
360
- // client-action rows). The module resolves the model worker via ensureModelWorker and passes it (or a
361
- // fork); a client worker here is a caller error, refused loudly rather than silently rehomed.
362
- const target = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
363
- if (target === undefined) {
364
- throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
365
- }
366
- if (target.workspace_id !== workspaceId) {
367
- throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, {
368
- workerId,
369
- workspaceId,
370
- actualWorkspaceId: target.workspace_id,
371
- retryable: false,
372
- });
373
- }
374
- if (target.origin === "client") {
375
- throw daemonFailure("daemon:worker", "model-worker-required", 409, `Worker ${workerId} is not a model worker.`, {
376
- workerId,
377
- recovery: "Select or create a model worker for this loop.",
378
- retryable: false,
379
- });
380
- }
381
404
  // {§operator-config-max-turns-ceiling} — the operator ceiling clamps a per-call maxTurns; a
382
405
  // seam caller must not bypass operator policy (inject only DEFAULTS from env, never clamps).
383
406
  const ceiling = Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "-1");
@@ -391,6 +414,7 @@ export default class Daemon {
391
414
  workspaceId,
392
415
  workerId,
393
416
  prompt,
417
+ ...(source === undefined ? {} : { source }),
394
418
  ...(flags !== undefined ? { flags } : {}),
395
419
  ...(openPaths !== undefined ? { openPaths } : {}),
396
420
  turnCeiling,
@@ -819,24 +843,71 @@ export default class Daemon {
819
843
  throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, { workerId, workspaceId, actualWorkspaceId: owner.workspace_id });
820
844
  }
821
845
  }
846
+ // {§machine-processes} — provider inference is an exclusive capability of a
847
+ // model worker. The same assertion is used by early client admissions and
848
+ // the final drain callback, so recovery and internal callers cannot bypass it.
849
+ async #assertModelWorker(workspaceId, workerId) {
850
+ const worker = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
851
+ if (worker === undefined) {
852
+ throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
853
+ }
854
+ if (worker.workspace_id !== workspaceId) {
855
+ throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, {
856
+ workerId,
857
+ workspaceId,
858
+ actualWorkspaceId: worker.workspace_id,
859
+ retryable: false,
860
+ });
861
+ }
862
+ if (worker.origin !== "model") {
863
+ throw daemonFailure("daemon:worker", "model-worker-required", 409, `Worker ${workerId} is not a model worker.`, {
864
+ workerId,
865
+ actualOrigin: worker.origin,
866
+ recovery: "Select or create a model worker for this loop.",
867
+ retryable: false,
868
+ });
869
+ }
870
+ }
871
+ async #workerPromptSource(workspaceId, sourceWorkerId, targetWorkerId) {
872
+ if (sourceWorkerId === targetWorkerId)
873
+ return undefined;
874
+ const source = await this.#db.envelope_get_worker_by_id.get({ id: sourceWorkerId });
875
+ if (source === undefined || source.workspace_id !== workspaceId) {
876
+ throw new Error(`Worker prompt source ${sourceWorkerId} does not belong to workspace ${workspaceId}.`);
877
+ }
878
+ return WorkerControlAddress.render(source.name);
879
+ }
822
880
  // {§methods-op-mirror} — execute parsed ops on behalf of a client as a
823
881
  // client-origin turn (the log is core's, a client op is a first-class citizen), dispatched through
824
882
  // the engine, then emitted as log/entry on the event source. One seam op backs the whole op_*
825
883
  // family (read/edit/copy/find/fold/look/move/open/send/exec); the module parses at its edge with the
826
884
  // grammar package and hands over the statement, then fans the emitted entry out to its own clients.
885
+ // {§functionality-model-mutation} — await every queued Functionality
886
+ // publication (a Worker's accepted mutation publishes at its turn boundary).
887
+ async settleFunctionality(workerId) {
888
+ await this.#functionality.settle(workerId);
889
+ }
827
890
  async dispatchAsClient(args) {
828
891
  const workspaceId = ClientInput.assertId("operation.dispatch", "workspaceId", args.workspaceId);
829
892
  const workerId = ClientInput.assertId("operation.dispatch", "workerId", args.workerId);
893
+ const functionalityWorkerId = ClientInput.assertId("operation.dispatch", "functionalityWorkerId", args.functionalityWorkerId);
830
894
  const { statement } = args;
831
- const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
895
+ await this.#assertWorkerOwned(workspaceId, functionalityWorkerId);
896
+ const releaseCapabilities = await this.#residency.acquire(workspaceId, functionalityWorkerId);
832
897
  try {
833
- const result = await this.#dispatchClientStatement({ workspaceId, workerId, loopId: clientLoopId, statement });
834
- await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
835
- return result;
898
+ const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
899
+ try {
900
+ const result = await this.#dispatchClientStatement({ workspaceId, workerId, functionalityWorkerId, loopId: clientLoopId, statement });
901
+ await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
902
+ return result;
903
+ }
904
+ catch (error) {
905
+ await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
906
+ throw error;
907
+ }
836
908
  }
837
- catch (error) {
838
- await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
839
- throw error;
909
+ finally {
910
+ releaseCapabilities();
840
911
  }
841
912
  }
842
913
  // The client-interface action contract: one AG-UI action owns one administrative
@@ -846,25 +917,35 @@ export default class Daemon {
846
917
  async dispatchClientAction(args) {
847
918
  const workspaceId = ClientInput.assertId("operation.dispatch-batch", "workspaceId", args.workspaceId);
848
919
  const workerId = ClientInput.assertId("operation.dispatch-batch", "workerId", args.workerId);
920
+ const functionalityWorkerId = ClientInput.assertId("operation.dispatch-batch", "functionalityWorkerId", args.functionalityWorkerId);
849
921
  const { statements } = args;
850
922
  if (statements.length === 0)
851
923
  return [];
852
- const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
924
+ await this.#assertWorkerOwned(workspaceId, functionalityWorkerId);
925
+ // {§actor-boundary-attached-functionality} — the attached Worker's
926
+ // Functionality is what the client operation executes in.
927
+ const releaseCapabilities = await this.#residency.acquire(workspaceId, functionalityWorkerId);
853
928
  try {
854
- const results = [];
855
- for (const statement of statements) {
856
- results.push(await this.#dispatchClientStatement({ workspaceId, workerId, loopId: clientLoopId, statement }));
929
+ const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
930
+ try {
931
+ const results = [];
932
+ for (const statement of statements) {
933
+ results.push(await this.#dispatchClientStatement({ workspaceId, workerId, functionalityWorkerId, loopId: clientLoopId, statement }));
934
+ }
935
+ await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
936
+ return results;
937
+ }
938
+ catch (error) {
939
+ await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
940
+ throw error;
857
941
  }
858
- await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
859
- return results;
860
942
  }
861
- catch (error) {
862
- await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
863
- throw error;
943
+ finally {
944
+ releaseCapabilities();
864
945
  }
865
946
  }
866
947
  async #dispatchClientStatement(args) {
867
- const { workspaceId, workerId, loopId, statement } = args;
948
+ const { workspaceId, workerId, functionalityWorkerId, loopId, statement } = args;
868
949
  const release = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
869
950
  try {
870
951
  const { id: turnId } = await Turn.open(this.#db, {
@@ -876,7 +957,7 @@ export default class Daemon {
876
957
  try {
877
958
  const entryIds = [];
878
959
  const result = await this.#engine.dispatch({
879
- statement, workspaceId, workerId, loopId, turnId, sequence: 1,
960
+ statement, workspaceId, workerId, functionalityWorkerId, loopId, turnId, sequence: 1,
880
961
  origin: "client", onDispatch: (logEntryId) => { entryIds.push(logEntryId); },
881
962
  });
882
963
  await Turn.complete(this.#db, turnId, result.status);
@@ -913,20 +994,30 @@ export default class Daemon {
913
994
  async look(args) {
914
995
  const workspaceId = ClientInput.assertId("operation.look", "workspaceId", args.workspaceId);
915
996
  const workerId = ClientInput.assertId("operation.look", "workerId", args.workerId);
997
+ const functionalityWorkerId = ClientInput.assertId("operation.look", "functionalityWorkerId", args.functionalityWorkerId);
916
998
  const { statement } = args;
917
- const release = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
918
- const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
999
+ await this.#assertWorkerOwned(workspaceId, functionalityWorkerId);
1000
+ const releaseCapabilities = await this.#residency.acquire(workspaceId, functionalityWorkerId);
919
1001
  try {
920
- const result = await this.#engine.look({ statement, workspaceId, workerId, loopId: clientLoopId });
921
- await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
922
- return result;
923
- }
924
- catch (error) {
925
- await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
926
- throw error;
1002
+ const releaseWorkspace = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
1003
+ try {
1004
+ const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
1005
+ try {
1006
+ const result = await this.#engine.look({ statement, workspaceId, workerId, functionalityWorkerId, loopId: clientLoopId });
1007
+ await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
1008
+ return result;
1009
+ }
1010
+ catch (error) {
1011
+ await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
1012
+ throw error;
1013
+ }
1014
+ }
1015
+ finally {
1016
+ releaseWorkspace();
1017
+ }
927
1018
  }
928
1019
  finally {
929
- release();
1020
+ releaseCapabilities();
930
1021
  }
931
1022
  }
932
1023
  // {§methods-log-read} — a workspace's journal, the module's primary render input. The worker is
@@ -1030,8 +1121,60 @@ export default class Daemon {
1030
1121
  return Validator.assertClientDisplayCapabilities([...schemes, ...mimetypes]);
1031
1122
  }
1032
1123
  listWorkspaces() { return Envelope.listWorkspaces(this.#db); }
1033
- listWorkers(workspaceId) {
1034
- return Envelope.listWorkersForWorkspace(this.#db, ClientInput.assertId("workspace.workers", "workspaceId", workspaceId));
1124
+ listWorkers(workspaceId, query = {}) {
1125
+ const origin = query.origin;
1126
+ if (origin !== undefined && !["model", "client", "_plurnk"].includes(origin)) {
1127
+ throw daemonFailure("daemon:worker", "origin-invalid", 400, `Worker origin '${String(origin)}' is invalid.`, { origin, retryable: false });
1128
+ }
1129
+ const parentWorkerId = query.parentWorkerId === undefined
1130
+ ? undefined
1131
+ : query.parentWorkerId === null
1132
+ ? null
1133
+ : ClientInput.assertId("workspace.workers", "parentWorkerId", query.parentWorkerId);
1134
+ // {§methods-worker-list} — an omitted parent filter must stay absent:
1135
+ // the envelope keys the filter on the property's presence, not its value.
1136
+ return Envelope.listWorkersForWorkspace(this.#db, ClientInput.assertId("workspace.workers", "workspaceId", workspaceId), {
1137
+ ...(origin === undefined ? {} : { origin }),
1138
+ ...(parentWorkerId === undefined ? {} : { parentWorkerId }),
1139
+ });
1140
+ }
1141
+ async readWorker(args) {
1142
+ const workspaceId = ClientInput.assertId("worker.read", "workspaceId", args.workspaceId);
1143
+ const hasId = Object.hasOwn(args.identity, "id");
1144
+ const hasName = Object.hasOwn(args.identity, "name");
1145
+ if (hasId === hasName) {
1146
+ throw daemonFailure("daemon:worker", "identity-invalid", 400, "Worker observation requires exactly one id or name.", { retryable: false });
1147
+ }
1148
+ const row = hasId
1149
+ ? await this.#db.envelope_get_worker_by_id.get({
1150
+ id: ClientInput.assertId("worker.read", "id", args.identity.id),
1151
+ })
1152
+ : await this.#db.envelope_get_worker_by_name.get({
1153
+ workspace_id: workspaceId,
1154
+ name: ClientInput.assertOptionalWorkerName("worker.read", "name", args.identity.name),
1155
+ });
1156
+ if (row === undefined || row.workspace_id !== workspaceId)
1157
+ return null;
1158
+ const { workspace_id: _workspaceId, ...projection } = row;
1159
+ return projection;
1160
+ }
1161
+ async listWorkerLoops(args) {
1162
+ const workspaceId = ClientInput.assertId("worker.loops", "workspaceId", args.workspaceId);
1163
+ const workerId = ClientInput.assertId("worker.loops", "workerId", args.workerId);
1164
+ await this.#assertWorkerOwned(workspaceId, workerId);
1165
+ const rows = await this.#db.application_list_worker_loops.all({ worker_id: workerId });
1166
+ return rows.map((row) => ({
1167
+ id: row.id,
1168
+ workerId: row.workerId,
1169
+ sequence: row.sequence,
1170
+ status: row.status,
1171
+ prompt: row.prompt,
1172
+ promptSource: row.promptSource,
1173
+ terminatedAt: row.terminatedAt,
1174
+ terminalResult: row.terminalResult === null
1175
+ ? null
1176
+ : Validator.assertOperationResult(JSON.parse(row.terminalResult)),
1177
+ }));
1035
1178
  }
1036
1179
  // {§methods-workspace-prompts}: root-conversation loop seeds, newest-first.
1037
1180
  listPrompts(workspaceId, limit) {
@@ -1058,7 +1201,7 @@ export default class Daemon {
1058
1201
  }
1059
1202
  // {§methods-workspace-create}: the module owns protocol decoding; core validates the typed seam
1060
1203
  // inputs and owns the envelope, its reserved-name + name-uniqueness invariants,
1061
- // membership resolution, warmWorkspaceDerivations, and the workspace/created emit. No connection state
1204
+ // membership resolution and the workspace/created emit. No connection state
1062
1205
  // (which client is on which workspace) lives here — that's the module's.
1063
1206
  async createWorkspace(args) {
1064
1207
  // The seam fails hard on malformed semantic input so every module inherits one wall:
@@ -1078,26 +1221,18 @@ export default class Daemon {
1078
1221
  }
1079
1222
  if (constraints.length > 0)
1080
1223
  await GitMembership.resolveGitMembership(this.#db, envelope.workspaceId, undefined);
1081
- for (const provider of this.#workspaceCapabilityProviders.values()) {
1082
- await provider.hydrate(envelope.workspaceId);
1083
- }
1084
- await LoopDocs.materialize(this.#engine, this.#db, envelope.workspaceId);
1085
- await SkillDocs.materialize(this.#engine, this.#db, envelope.workspaceId);
1086
- void this.#engine.warmWorkspaceDerivations(envelope.workspaceId).catch(() => { });
1087
1224
  this.#broadcast("all", "workspace/created", { id: envelope.workspaceId, name: envelope.workspaceName, projectRoot: envelope.projectRoot });
1088
1225
  return envelope;
1089
1226
  });
1090
1227
  }
1091
1228
  async attachWorkspace(args) {
1092
- // attachToWorkspace owns the reserved-name + worker-ownership invariants; the seam just delegates + warms.
1229
+ // attachToWorkspace owns the reserved-name + worker-ownership invariants; the seam just delegates.
1093
1230
  const workspaceId = ClientInput.assertId("workspace.attach", "workspaceId", args.workspaceId);
1094
1231
  const workerId = args.workerId === undefined
1095
1232
  ? undefined
1096
1233
  : ClientInput.assertId("workspace.attach", "workerId", args.workerId);
1097
1234
  const workerName = ClientInput.assertOptionalWorkerName("workspace.attach", "workerName", args.workerName);
1098
- const envelope = await Envelope.attachToWorkspace(this.#db, workspaceId, { workerId, workerName });
1099
- void this.#engine.warmWorkspaceDerivations(envelope.workspaceId).catch(() => { });
1100
- return envelope;
1235
+ return Envelope.attachToWorkspace(this.#db, workspaceId, { workerId, workerName });
1101
1236
  }
1102
1237
  async renameWorkspace(workspaceId, name) {
1103
1238
  const checkedWorkspaceId = ClientInput.assertId("workspace.rename", "workspaceId", workspaceId);
@@ -1116,7 +1251,7 @@ export default class Daemon {
1116
1251
  // Members may have just landed — begin warming now, but return the constraint response
1117
1252
  // immediately so prompts do not wait for the complete derivation corpus.
1118
1253
  void this.#engine.warmWorkspaceDerivations(checkedWorkspaceId).catch(() => { });
1119
- return { effect, glob };
1254
+ return { effect, glob, source: "explicit" };
1120
1255
  }
1121
1256
  finally {
1122
1257
  release();
@@ -1163,8 +1298,10 @@ export default class Daemon {
1163
1298
  retryable: false,
1164
1299
  });
1165
1300
  }
1166
- const release = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
1301
+ const releaseCapabilities = await this.#residency.acquire(workspaceId, workerId);
1302
+ let releaseWorkspace;
1167
1303
  try {
1304
+ releaseWorkspace = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
1168
1305
  let parsed;
1169
1306
  try {
1170
1307
  parsed = parsePath(args.target);
@@ -1215,6 +1352,7 @@ export default class Daemon {
1215
1352
  workspace_id: workspaceId,
1216
1353
  owner_id: location.ownerId,
1217
1354
  scheme: location.scheme,
1355
+ authority: location.authority,
1218
1356
  pathname: location.pathname,
1219
1357
  });
1220
1358
  if (row === undefined) {
@@ -1262,7 +1400,8 @@ export default class Daemon {
1262
1400
  });
1263
1401
  }
1264
1402
  finally {
1265
- release();
1403
+ releaseWorkspace?.();
1404
+ releaseCapabilities();
1266
1405
  }
1267
1406
  }
1268
1407
  // {§methods-conversation-worker}: a fresh conversation is a model-origin root worker with an empty private log.
@@ -1289,9 +1428,9 @@ export default class Daemon {
1289
1428
  // {§worker-scheme-fork} — branch a worker's log while sharing the workspace world.
1290
1429
  // Core owns the workspace check and immutable worker-name admission.
1291
1430
  async forkWorker(args) {
1292
- const workspaceId = ClientInput.assertId("worker.fork", "workspaceId", args.workspaceId);
1293
- const workerId = ClientInput.assertId("worker.fork", "workerId", args.workerId);
1294
- const name = ClientInput.assertOptionalWorkerName("worker.fork", "name", args.name);
1431
+ const workspaceId = ClientInput.assertId("run.fork", "workspaceId", args.workspaceId);
1432
+ const workerId = ClientInput.assertId("run.fork", "workerId", args.workerId);
1433
+ const name = ClientInput.assertOptionalWorkerName("run.fork", "name", args.name);
1295
1434
  const owner = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
1296
1435
  if (owner === undefined) {
1297
1436
  throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
@@ -1310,32 +1449,23 @@ export default class Daemon {
1310
1449
  throw daemonFailure("daemon:worker", "name-conflict", 409, `Worker name '${name}' is already in use in workspace ${workspaceId}.`, { workspaceId, name, recovery: "Choose another worker name.", retryable: false });
1311
1450
  }
1312
1451
  }
1313
- const branchWorkerId = await Fork.fork(this.#db, workerId, name);
1452
+ const branchWorkerId = await Fork.fork(this.#db, workerId, name, (scheme) => this.#schemes.entryInheritanceForStoredScheme(scheme, workerId));
1314
1453
  const branch = await this.#db.envelope_get_worker_by_id.get({ id: branchWorkerId });
1315
1454
  return { workerId: branchWorkerId, workerName: branch?.name ?? null, parentWorkerId: workerId };
1316
1455
  }
1317
1456
  async registerRuntimes(registrations) {
1318
1457
  const normalized = registrations.map((registration) => this.#normalizeRuntime(registration));
1319
1458
  this.#engine.registerRuntimes(normalized);
1320
- if (this.#capabilitiesPublished) {
1321
- for (const workspace of await Envelope.listWorkspaces(this.#db)) {
1322
- await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
1323
- await SkillDocs.materialize(this.#engine, this.#db, workspace.id);
1324
- }
1325
- }
1459
+ await this.#residency.rematerializeActive();
1326
1460
  }
1327
1461
  async registerRuntime(registration) {
1328
1462
  await this.registerRuntimes([registration]);
1329
1463
  }
1330
1464
  async registerScheme(name, handler) {
1331
1465
  this.#schemes.register(name, handler);
1332
- if (this.#capabilitiesPublished) {
1466
+ if (this.#residency.published)
1333
1467
  await this.#schemes.ready();
1334
- for (const workspace of await Envelope.listWorkspaces(this.#db)) {
1335
- await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
1336
- await SkillDocs.materialize(this.#engine, this.#db, workspace.id);
1337
- }
1338
- }
1468
+ await this.#residency.rematerializeActive();
1339
1469
  }
1340
1470
  #normalizeRuntime({ namespaceOwner, decl, executor, availability, scheme }) {
1341
1471
  if (typeof namespaceOwner !== "string" || namespaceOwner.trim().length === 0) {
@@ -1360,12 +1490,26 @@ export default class Daemon {
1360
1490
  };
1361
1491
  }
1362
1492
  registerModuleAction(registration) {
1363
- const { name, scope, handler } = registration;
1493
+ const { name, scope, inputSchema, outputSchema, handler } = registration;
1364
1494
  if (name.length === 0)
1365
1495
  throw new Error("registerModuleAction: action name must not be empty");
1366
- if (scope !== "worldless" && scope !== "workspace") {
1496
+ if (scope !== "worldless" && scope !== "workspace" && scope !== "worker") {
1367
1497
  throw new Error(`module action '${name}' has invalid scope '${String(scope)}'`);
1368
1498
  }
1499
+ if (inputSchema === null || typeof inputSchema !== "object" || Array.isArray(inputSchema)) {
1500
+ throw new Error(`module action '${name}' has no input schema`);
1501
+ }
1502
+ if (outputSchema === null || typeof outputSchema !== "object" || Array.isArray(outputSchema)) {
1503
+ throw new Error(`module action '${name}' has no output schema`);
1504
+ }
1505
+ for (const [role, schema] of [["input", inputSchema], ["output", outputSchema]]) {
1506
+ try {
1507
+ Validator.validateJsonSchemaInstance(schema, {});
1508
+ }
1509
+ catch (cause) {
1510
+ throw new Error(`module action '${name}' has an invalid ${role} schema`, { cause });
1511
+ }
1512
+ }
1369
1513
  if (typeof handler !== "function")
1370
1514
  throw new Error(`module action '${name}' has no handler`);
1371
1515
  if (this.#moduleActions.has(name))
@@ -1374,7 +1518,12 @@ export default class Daemon {
1374
1518
  }
1375
1519
  listModuleActions() {
1376
1520
  return [...this.#moduleActions.values()]
1377
- .map(({ name, scope }) => ({ name, scope }))
1521
+ .map(({ name, scope, inputSchema, outputSchema }) => ({
1522
+ name,
1523
+ scope,
1524
+ inputSchema,
1525
+ outputSchema,
1526
+ }))
1378
1527
  .toSorted((left, right) => left.name.localeCompare(right.name));
1379
1528
  }
1380
1529
  async invokeModuleAction(name, params, context) {
@@ -1384,131 +1533,38 @@ export default class Daemon {
1384
1533
  if (registration.scope !== context.scope) {
1385
1534
  throw new Error(`module action '${name}' requires ${registration.scope} context, not ${context.scope}`);
1386
1535
  }
1536
+ let releaseCapabilities;
1387
1537
  if (context.scope === "workspace") {
1388
1538
  ClientInput.assertId(`module action '${name}'`, "workspaceId", context.workspaceId);
1389
1539
  }
1390
- return registration.handler(params, context);
1391
- }
1392
- registerWorkspaceCapabilityProvider(namespaceOwner, provider) {
1393
- if (namespaceOwner.trim().length === 0) {
1394
- throw new Error("workspace capability provider requires a non-empty namespace owner");
1395
- }
1396
- if (this.#workspaceCapabilityProviders.has(namespaceOwner)) {
1397
- throw new Error(`workspace capability provider '${namespaceOwner}' is already registered`);
1398
- }
1399
- this.#workspaceCapabilityProviders.set(namespaceOwner, provider);
1400
- }
1401
- async readWorkspaceModuleState(workspaceId, namespaceOwner) {
1402
- const checkedWorkspaceId = ClientInput.assertId("workspace module state", "workspaceId", workspaceId);
1403
- if (namespaceOwner.trim().length === 0) {
1404
- throw new Error("workspace module state requires a non-empty namespace owner");
1405
- }
1406
- const row = await this.#db.workspace_module_state_get.get({
1407
- workspace_id: checkedWorkspaceId,
1408
- namespace_owner: namespaceOwner,
1409
- });
1410
- return row === undefined ? null : JSON.parse(row.state);
1411
- }
1412
- async replaceWorkspaceCapabilities({ workspaceId, namespaceOwner, state, runtimes, }) {
1413
- const checkedWorkspaceId = ClientInput.assertId("workspace capability replacement", "workspaceId", workspaceId);
1414
- if (namespaceOwner.trim().length === 0) {
1415
- throw new Error("workspace capability replacement requires a non-empty namespace owner");
1416
- }
1417
- const workspace = await this.#db.envelope_get_workspace.get({ id: checkedWorkspaceId });
1418
- if (workspace === undefined) {
1419
- throw daemonFailure("daemon:workspace-capability", "workspace-not-found", 404, `Workspace ${checkedWorkspaceId} does not exist.`, { workspaceId: checkedWorkspaceId, retryable: false });
1420
- }
1421
- const encoded = state === null ? null : JSON.stringify(state);
1422
- if (state !== null && encoded === undefined) {
1423
- throw daemonFailure("daemon:workspace-capability", "state-not-json", 400, "Workspace module state is not JSON-serializable.", { namespaceOwner, retryable: false });
1424
- }
1425
- if (encoded !== null)
1426
- JSON.parse(encoded);
1427
- const normalized = runtimes.map((registration) => {
1428
- if (registration.namespaceOwner !== namespaceOwner) {
1429
- throw new Error(`workspace runtime owner '${registration.namespaceOwner}' does not match '${namespaceOwner}'`);
1540
+ else if (context.scope === "worker") {
1541
+ const workspaceId = ClientInput.assertId(`module action '${name}'`, "workspaceId", context.workspaceId);
1542
+ const workerId = ClientInput.assertId(`module action '${name}'`, "workerId", context.workerId);
1543
+ const worker = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
1544
+ if (worker === undefined || worker.workspace_id !== workspaceId) {
1545
+ throw daemonFailure("daemon:module-action", "worker-not-found", 404, `Worker ${workerId} does not exist in workspace ${workspaceId}.`, { workspaceId, workerId, retryable: false });
1430
1546
  }
1431
- return this.#normalizeRuntime(registration);
1432
- });
1433
- const gate = this.#workspaceGate.tryExclusive(checkedWorkspaceId);
1434
- if (gate === null) {
1435
- throw daemonFailure("daemon:workspace-capability", "workspace-busy", 409, `Workspace ${checkedWorkspaceId} is running an operation or another capability change.`, {
1436
- workspaceId: checkedWorkspaceId,
1437
- namespaceOwner,
1438
- recovery: "Settle the current operation and retry the capability change.",
1439
- retryable: true,
1440
- });
1547
+ releaseCapabilities = await this.#residency.acquire(workspaceId, workerId);
1441
1548
  }
1442
- await gate.acquired;
1443
- const prior = await this.#db.workspace_module_state_get.get({
1444
- workspace_id: checkedWorkspaceId,
1445
- namespace_owner: namespaceOwner,
1446
- });
1447
- let rollbackRuntimes;
1448
- let stateChanged = false;
1449
1549
  try {
1450
- const commitRuntimes = await this.#engine.prepareWorkspaceRuntimes(checkedWorkspaceId, namespaceOwner, normalized);
1451
- if (encoded === null) {
1452
- await this.#db.workspace_module_state_delete.run({
1453
- workspace_id: checkedWorkspaceId,
1454
- namespace_owner: namespaceOwner,
1455
- });
1456
- }
1457
- else {
1458
- await this.#db.workspace_module_state_put.run({
1459
- workspace_id: checkedWorkspaceId,
1460
- namespace_owner: namespaceOwner,
1461
- state: encoded,
1462
- });
1463
- }
1464
- stateChanged = true;
1465
- rollbackRuntimes = commitRuntimes();
1466
- if (this.#capabilitiesPublished) {
1467
- await LoopDocs.materialize(this.#engine, this.#db, checkedWorkspaceId);
1468
- await SkillDocs.materialize(this.#engine, this.#db, checkedWorkspaceId);
1469
- }
1470
- }
1471
- catch (cause) {
1472
- rollbackRuntimes?.();
1473
- const rollbackErrors = [];
1474
- if (stateChanged) {
1475
- try {
1476
- if (prior === undefined) {
1477
- await this.#db.workspace_module_state_delete.run({
1478
- workspace_id: checkedWorkspaceId,
1479
- namespace_owner: namespaceOwner,
1480
- });
1481
- }
1482
- else {
1483
- await this.#db.workspace_module_state_put.run({
1484
- workspace_id: checkedWorkspaceId,
1485
- namespace_owner: namespaceOwner,
1486
- state: prior.state,
1487
- });
1488
- }
1489
- }
1490
- catch (rollbackCause) {
1491
- rollbackErrors.push(rollbackCause);
1492
- }
1493
- if (this.#capabilitiesPublished) {
1494
- try {
1495
- await LoopDocs.materialize(this.#engine, this.#db, checkedWorkspaceId);
1496
- await SkillDocs.materialize(this.#engine, this.#db, checkedWorkspaceId);
1497
- }
1498
- catch (rollbackCause) {
1499
- rollbackErrors.push(rollbackCause);
1500
- }
1501
- }
1502
- }
1503
- if (rollbackErrors.length > 0) {
1504
- throw new AggregateError([cause, ...rollbackErrors], "Workspace capability replacement and rollback failed");
1505
- }
1506
- throw cause;
1550
+ return await registration.handler(params, context);
1507
1551
  }
1508
1552
  finally {
1509
- gate.release();
1553
+ releaseCapabilities?.();
1510
1554
  }
1511
1555
  }
1556
+ registerWorkerCapabilityProvider(namespaceOwner, provider) {
1557
+ this.#residency.registerProvider(namespaceOwner, provider);
1558
+ }
1559
+ registerFunctionalityAdapter(adapter) {
1560
+ return this.#functionality.register(adapter);
1561
+ }
1562
+ async readWorkerModuleState(workerId, namespaceOwner) {
1563
+ return this.#residency.readModuleState(workerId, namespaceOwner);
1564
+ }
1565
+ async replaceWorkerCapabilities(replacement, options = {}) {
1566
+ await this.#residency.replace(replacement, options);
1567
+ }
1512
1568
  get engine() { return this.#engine; }
1513
1569
  get provider() { return this.#provider; }
1514
1570
  get schemes() { return this.#schemes; }
@@ -1534,6 +1590,7 @@ export default class Daemon {
1534
1590
  // shell is the default runtime, so its executor must boot usable.
1535
1591
  const executors = await ExecutorRegistry.build({ defaultRuntime: "sh", cwd: this.#discoveryCwd });
1536
1592
  this.#engine.setExecutors(executors);
1593
+ this.#engine.setFunctionalityDocuments((workerId) => this.#functionality.documents(workerId));
1537
1594
  // {§question-tool} — the native request-user-input runtime, process-wide;
1538
1595
  // per-worker admission gates its doc visibility and dispatch ({§worker-settings}).
1539
1596
  await this.#engine.registerRuntimes([{
@@ -1574,20 +1631,8 @@ export default class Daemon {
1574
1631
  this.#moduleClosers.push(module);
1575
1632
  await module.setup?.(setupSeam);
1576
1633
  }
1577
- for (const workspace of await Envelope.listWorkspaces(this.#db)) {
1578
- for (const provider of this.#workspaceCapabilityProviders.values()) {
1579
- await provider.hydrate(workspace.id);
1580
- }
1581
- }
1582
1634
  await this.#schemes.ready();
1583
- // Reconcile the kernel-published documentation surface once per existing workspace.
1584
- // Installed capabilities and operator configuration are now fully known; model loops
1585
- // consume this workspace state but never republish it.
1586
- for (const workspace of await Envelope.listWorkspaces(this.#db)) {
1587
- await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
1588
- await SkillDocs.materialize(this.#engine, this.#db, workspace.id);
1589
- }
1590
- this.#capabilitiesPublished = true;
1635
+ this.#residency.publish();
1591
1636
  await this.#recoverLifecycle();
1592
1637
  // {§module-lifecycle} — the daemon opens no transport. Modules start their listeners only
1593
1638
  // after capability publication and durable lifecycle recovery are complete.
@@ -1610,11 +1655,24 @@ export default class Daemon {
1610
1655
  await this.#db.recovery_resume_unblocked_parks.run({});
1611
1656
  await this.#branchBatches.recover();
1612
1657
  const orphanSources = await this.#db.recovery_orphan_prompt_sources.all({});
1658
+ const invalidOrphan = orphanSources.find(({ origin }) => origin !== "model");
1659
+ if (invalidOrphan !== undefined) {
1660
+ throw new Error(`Orphaned provider work belongs to ${invalidOrphan.origin} worker ${invalidOrphan.worker_id}; only model workers may enter inference.`);
1661
+ }
1613
1662
  for (const source of orphanSources) {
1614
1663
  await this.#drains.reconcileOrphanedPrompts(source.worker_id, source.loop_id);
1615
1664
  }
1616
1665
  const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
1617
1666
  const queued = await this.#db.recovery_queued_workers.all({});
1667
+ const invalidQueued = queued.find(({ origin }) => origin !== "model");
1668
+ if (invalidQueued !== undefined) {
1669
+ throw new Error(`Queued provider work belongs to ${invalidQueued.origin} worker ${invalidQueued.worker_id}; only model workers may enter inference.`);
1670
+ }
1671
+ const parked = await this.#db.recovery_parked_workers.all({});
1672
+ const invalidParked = parked.find(({ origin }) => origin !== "model");
1673
+ if (invalidParked !== undefined) {
1674
+ throw new Error(`Parked provider work belongs to ${invalidParked.origin} worker ${invalidParked.worker_id}; only model workers may enter inference.`);
1675
+ }
1618
1676
  for (const row of queued) {
1619
1677
  const started = await this.#drains.ensureDrain({
1620
1678
  workspaceId: row.workspace_id,
@@ -1622,10 +1680,11 @@ export default class Daemon {
1622
1680
  systemPrompt,
1623
1681
  });
1624
1682
  started?.drainPromise.catch((err) => {
1625
- console.error(`recovered drain failed for worker ${row.worker_id}:`, err);
1683
+ if (this.#started) {
1684
+ console.error(`recovered drain failed for worker ${row.worker_id}:`, err);
1685
+ }
1626
1686
  });
1627
1687
  }
1628
- const parked = await this.#db.recovery_parked_workers.all({});
1629
1688
  for (const row of parked) {
1630
1689
  await this.#drains.schedulePollWake(row.workspace_id, row.worker_id, systemPrompt);
1631
1690
  }
@@ -1644,6 +1703,15 @@ export default class Daemon {
1644
1703
  if (!this.#started)
1645
1704
  return;
1646
1705
  this.#started = false;
1706
+ // Stop every producer before refusing capability acquisition. A drain
1707
+ // already between queue claim and activation must observe its own abort,
1708
+ // not misclassify orderly shutdown as a capability failure.
1709
+ const derivationAbort = new DOMException("daemon stopping", "AbortError");
1710
+ this.#engine.cancelAllProposals("daemon_stopping");
1711
+ this.#engine.cancelDerivations(derivationAbort);
1712
+ this.#branchBatches.beginStop();
1713
+ this.#drains.beginStop("daemon_stopping");
1714
+ this.#residency.beginStop();
1647
1715
  const stopDeadlineMs = Daemon.#stopDeadlineMs();
1648
1716
  const deadline = Date.now() + stopDeadlineMs;
1649
1717
  const settle = (label, wait) => new Promise((resolve) => {
@@ -1677,11 +1745,6 @@ export default class Daemon {
1677
1745
  // Settle the stopped world FIRST: a drain paused at a pending proposal awaits a resolution
1678
1746
  // that will never arrive once clients are gone — allSettled(drains) below would deadlock
1679
1747
  // the stop forever (a daemon with a pending HITL proposal could not shut down).
1680
- const derivationAbort = new DOMException("daemon stopping", "AbortError");
1681
- this.#engine.cancelAllProposals("daemon_stopping");
1682
- this.#engine.cancelDerivations(derivationAbort);
1683
- this.#branchBatches.beginStop();
1684
- this.#drains.beginStop("daemon_stopping");
1685
1748
  // {§crash-only-stop} — the settle sequence is DEADLINE-BOUNDED: a child
1686
1749
  // that never closes (a wedged MCP server, a stuck stream) must not hang
1687
1750
  // the daemon forever. Past the deadline the waits are abandoned; the
@@ -1689,6 +1752,9 @@ export default class Daemon {
1689
1752
  // leak as a live-but-wedged tree.
1690
1753
  const branchResult = await settle("branch batches idle", () => this.#branchBatches.idle());
1691
1754
  const drainResult = await settle("drains idle", () => this.#drains.idle());
1755
+ // A boundary publication queued behind the last turn settles before
1756
+ // modules close and before the DB goes away ({§functionality-publication}).
1757
+ const functionalityResult = await settle("functionality publications", () => this.#functionality.settle());
1692
1758
  const moduleResult = await settle("modules close", async () => {
1693
1759
  const results = await moduleClose;
1694
1760
  if (results.some((r) => r.status === "rejected"))
@@ -1706,6 +1772,7 @@ export default class Daemon {
1706
1772
  const wakeResult = await settle("drains idle (wake)", () => this.#drains.idle());
1707
1773
  const closeErrors = [
1708
1774
  moduleResult,
1775
+ functionalityResult,
1709
1776
  branchResult,
1710
1777
  drainResult,
1711
1778
  streamingResult,
@@ -1736,6 +1803,10 @@ export default class Daemon {
1736
1803
  notifyStreamEvent(workspaceId, event) {
1737
1804
  this.#broadcast({ workspaceId }, "stream/event", event);
1738
1805
  }
1806
+ /** Emit transient readable reasoning for an in-flight model call. */
1807
+ notifyReasoningEvent(workspaceId, event) {
1808
+ this.#broadcast({ workspaceId }, "reasoning/event", event);
1809
+ }
1739
1810
  /**
1740
1811
  * Emit a transient notice scoped to the workspace containing the loop.
1741
1812
  */
@@ -1781,6 +1852,7 @@ export default class Daemon {
1781
1852
  worker_id: workerId,
1782
1853
  sequence: seqRow.next,
1783
1854
  prompt: first.body,
1855
+ prompt_source: first.prompt_source,
1784
1856
  flags: first.flags,
1785
1857
  model_route_id: first.model_route_id,
1786
1858
  spawn_model_route_id: first.spawn_model_route_id,
@@ -1807,6 +1879,12 @@ export default class Daemon {
1807
1879
  cancelDrain(workerId, reason = "user_cancelled") {
1808
1880
  return this.#drains.cancel(workerId, reason);
1809
1881
  }
1882
+ async cancelWorker(args) {
1883
+ const workspaceId = ClientInput.assertId("worker.cancel", "workspaceId", args.workspaceId);
1884
+ const workerId = ClientInput.assertId("worker.cancel", "workerId", args.workerId);
1885
+ await this.#assertWorkerOwned(workspaceId, workerId);
1886
+ await this.#drains.cancelWorkerTree(workerId, args.reason ?? "user_cancelled");
1887
+ }
1810
1888
  // Process-local stream activity is supplied to the drain owner through the
1811
1889
  // scheme registry rather than exposing that registry across the boundary.
1812
1890
  #workerHasActiveStreams(workerId) {