@plurnk/plurnk-service 1.7.0 → 1.8.0

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