@plurnk/plurnk-service 0.7.0 → 0.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 (270) hide show
  1. package/SPEC.md +116 -83
  2. package/bin/plurnk-service.ts +132 -0
  3. package/dist/Paths.d.ts +8 -0
  4. package/dist/Paths.d.ts.map +1 -0
  5. package/dist/Paths.js +47 -0
  6. package/dist/Paths.js.map +1 -0
  7. package/dist/content/index.d.ts +9 -0
  8. package/dist/content/index.d.ts.map +1 -0
  9. package/dist/content/index.js +10 -0
  10. package/dist/content/index.js.map +1 -0
  11. package/dist/content/line-marker.d.ts +26 -0
  12. package/dist/content/line-marker.d.ts.map +1 -0
  13. package/dist/content/line-marker.js +323 -0
  14. package/dist/content/line-marker.js.map +1 -0
  15. package/dist/content/matcher.d.ts +15 -0
  16. package/dist/content/matcher.d.ts.map +1 -0
  17. package/dist/content/matcher.js +112 -0
  18. package/dist/content/matcher.js.map +1 -0
  19. package/dist/content/mimetype-binary.d.ts +9 -0
  20. package/dist/content/mimetype-binary.d.ts.map +1 -0
  21. package/dist/content/mimetype-binary.js +86 -0
  22. package/dist/content/mimetype-binary.js.map +1 -0
  23. package/dist/content/path-mimetype.d.ts +6 -0
  24. package/dist/content/path-mimetype.d.ts.map +1 -0
  25. package/dist/content/path-mimetype.js +49 -0
  26. package/dist/content/path-mimetype.js.map +1 -0
  27. package/dist/content/read-resolve.d.ts +20 -0
  28. package/dist/content/read-resolve.d.ts.map +1 -0
  29. package/dist/content/read-resolve.js +60 -0
  30. package/dist/content/read-resolve.js.map +1 -0
  31. package/dist/core/ChannelWrite.d.ts +35 -30
  32. package/dist/core/ChannelWrite.d.ts.map +1 -1
  33. package/dist/core/ChannelWrite.js +49 -41
  34. package/dist/core/ChannelWrite.js.map +1 -1
  35. package/dist/core/Engine.d.ts +16 -10
  36. package/dist/core/Engine.d.ts.map +1 -1
  37. package/dist/core/Engine.js +309 -115
  38. package/dist/core/Engine.js.map +1 -1
  39. package/dist/core/EnvFlags.d.ts +6 -3
  40. package/dist/core/EnvFlags.d.ts.map +1 -1
  41. package/dist/core/EnvFlags.js +62 -60
  42. package/dist/core/EnvFlags.js.map +1 -1
  43. package/dist/core/ExecutorRegistry.d.ts +26 -0
  44. package/dist/core/ExecutorRegistry.d.ts.map +1 -0
  45. package/dist/core/ExecutorRegistry.js +99 -0
  46. package/dist/core/ExecutorRegistry.js.map +1 -0
  47. package/dist/core/PluginLoader.d.ts +6 -3
  48. package/dist/core/PluginLoader.d.ts.map +1 -1
  49. package/dist/core/PluginLoader.js +77 -73
  50. package/dist/core/PluginLoader.js.map +1 -1
  51. package/dist/core/ProviderInstantiate.d.ts +4 -2
  52. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  53. package/dist/core/ProviderInstantiate.js +23 -22
  54. package/dist/core/ProviderInstantiate.js.map +1 -1
  55. package/dist/core/SchemeRegistry.d.ts +1 -1
  56. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  57. package/dist/core/SchemeRegistry.js +3 -3
  58. package/dist/core/SchemeRegistry.js.map +1 -1
  59. package/dist/core/git-membership.d.ts +8 -0
  60. package/dist/core/git-membership.d.ts.map +1 -0
  61. package/dist/core/git-membership.js +125 -0
  62. package/dist/core/git-membership.js.map +1 -0
  63. package/dist/core/packet-wire.d.ts +47 -6
  64. package/dist/core/packet-wire.d.ts.map +1 -1
  65. package/dist/core/packet-wire.js +376 -312
  66. package/dist/core/packet-wire.js.map +1 -1
  67. package/dist/core/resolveForLoop.d.ts +16 -0
  68. package/dist/core/resolveForLoop.d.ts.map +1 -0
  69. package/dist/core/resolveForLoop.js +34 -0
  70. package/dist/core/resolveForLoop.js.map +1 -0
  71. package/dist/core/results.d.ts +40 -0
  72. package/dist/core/results.d.ts.map +1 -0
  73. package/dist/core/results.js +52 -0
  74. package/dist/core/results.js.map +1 -0
  75. package/dist/core/scheme-types.d.ts +7 -4
  76. package/dist/core/scheme-types.d.ts.map +1 -1
  77. package/dist/core/scheme-types.js +4 -4
  78. package/dist/core/scheme-types.js.map +1 -1
  79. package/dist/core/types.d.ts +26 -0
  80. package/dist/core/types.d.ts.map +1 -0
  81. package/dist/core/types.js +17 -0
  82. package/dist/core/types.js.map +1 -0
  83. package/dist/index.d.ts +1 -6
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +4 -43
  86. package/dist/index.js.map +1 -1
  87. package/dist/schemes/EffectPolicy.d.ts +6 -0
  88. package/dist/schemes/EffectPolicy.d.ts.map +1 -0
  89. package/dist/schemes/EffectPolicy.js +18 -0
  90. package/dist/schemes/EffectPolicy.js.map +1 -0
  91. package/dist/schemes/Exec.d.ts +1 -0
  92. package/dist/schemes/Exec.d.ts.map +1 -1
  93. package/dist/schemes/Exec.js +86 -45
  94. package/dist/schemes/Exec.js.map +1 -1
  95. package/dist/schemes/File.d.ts +0 -1
  96. package/dist/schemes/File.d.ts.map +1 -1
  97. package/dist/schemes/File.js +32 -69
  98. package/dist/schemes/File.js.map +1 -1
  99. package/dist/schemes/Known.d.ts.map +1 -1
  100. package/dist/schemes/Known.js +13 -13
  101. package/dist/schemes/Known.js.map +1 -1
  102. package/dist/schemes/Log.d.ts.map +1 -1
  103. package/dist/schemes/Log.js +8 -52
  104. package/dist/schemes/Log.js.map +1 -1
  105. package/dist/schemes/Plurnk.d.ts.map +1 -1
  106. package/dist/schemes/Plurnk.js +13 -13
  107. package/dist/schemes/Plurnk.js.map +1 -1
  108. package/dist/schemes/Skill.d.ts.map +1 -1
  109. package/dist/schemes/Skill.js +13 -13
  110. package/dist/schemes/Skill.js.map +1 -1
  111. package/dist/schemes/Unknown.d.ts.map +1 -1
  112. package/dist/schemes/Unknown.js +13 -13
  113. package/dist/schemes/Unknown.js.map +1 -1
  114. package/dist/schemes/_entry-crud.d.ts +5 -3
  115. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  116. package/dist/schemes/_entry-crud.js +55 -50
  117. package/dist/schemes/_entry-crud.js.map +1 -1
  118. package/dist/schemes/_entry-find.d.ts +10 -3
  119. package/dist/schemes/_entry-find.d.ts.map +1 -1
  120. package/dist/schemes/_entry-find.js +99 -77
  121. package/dist/schemes/_entry-find.js.map +1 -1
  122. package/dist/schemes/_entry-manifest.d.ts +6 -0
  123. package/dist/schemes/_entry-manifest.d.ts.map +1 -0
  124. package/dist/schemes/_entry-manifest.js +45 -0
  125. package/dist/schemes/_entry-manifest.js.map +1 -0
  126. package/dist/schemes/_entry-ops.d.ts +7 -4
  127. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  128. package/dist/schemes/_entry-ops.js +198 -316
  129. package/dist/schemes/_entry-ops.js.map +1 -1
  130. package/dist/schemes/_entry-send.d.ts +4 -1
  131. package/dist/schemes/_entry-send.d.ts.map +1 -1
  132. package/dist/schemes/_entry-send.js +57 -55
  133. package/dist/schemes/_entry-send.js.map +1 -1
  134. package/dist/server/ClientConnection.js +3 -3
  135. package/dist/server/ClientConnection.js.map +1 -1
  136. package/dist/server/Daemon.d.ts +5 -5
  137. package/dist/server/Daemon.d.ts.map +1 -1
  138. package/dist/server/Daemon.js +234 -176
  139. package/dist/server/Daemon.js.map +1 -1
  140. package/dist/server/clientTurn.d.ts +4 -1
  141. package/dist/server/clientTurn.d.ts.map +1 -1
  142. package/dist/server/clientTurn.js +19 -17
  143. package/dist/server/clientTurn.js.map +1 -1
  144. package/dist/server/dsl.d.ts +19 -16
  145. package/dist/server/dsl.d.ts.map +1 -1
  146. package/dist/server/dsl.js +127 -105
  147. package/dist/server/dsl.js.map +1 -1
  148. package/dist/server/envelope.d.ts +22 -19
  149. package/dist/server/envelope.d.ts.map +1 -1
  150. package/dist/server/envelope.js +116 -102
  151. package/dist/server/envelope.js.map +1 -1
  152. package/dist/server/logEntry.d.ts +4 -1
  153. package/dist/server/logEntry.d.ts.map +1 -1
  154. package/dist/server/logEntry.js +41 -39
  155. package/dist/server/logEntry.js.map +1 -1
  156. package/dist/server/methods/_dispatchAsClient.d.ts +3 -1
  157. package/dist/server/methods/_dispatchAsClient.d.ts.map +1 -1
  158. package/dist/server/methods/_dispatchAsClient.js +31 -29
  159. package/dist/server/methods/_dispatchAsClient.js.map +1 -1
  160. package/dist/server/methods/discover.d.ts +3 -1
  161. package/dist/server/methods/discover.d.ts.map +1 -1
  162. package/dist/server/methods/discover.js +8 -6
  163. package/dist/server/methods/discover.js.map +1 -1
  164. package/dist/server/methods/entry_read.d.ts +4 -1
  165. package/dist/server/methods/entry_read.d.ts.map +1 -1
  166. package/dist/server/methods/entry_read.js +54 -52
  167. package/dist/server/methods/entry_read.js.map +1 -1
  168. package/dist/server/methods/log_read.d.ts +4 -1
  169. package/dist/server/methods/log_read.d.ts.map +1 -1
  170. package/dist/server/methods/log_read.js +38 -36
  171. package/dist/server/methods/log_read.js.map +1 -1
  172. package/dist/server/methods/loop_cancel.d.ts +3 -1
  173. package/dist/server/methods/loop_cancel.d.ts.map +1 -1
  174. package/dist/server/methods/loop_cancel.js +19 -17
  175. package/dist/server/methods/loop_cancel.js.map +1 -1
  176. package/dist/server/methods/loop_resolve.d.ts +3 -1
  177. package/dist/server/methods/loop_resolve.d.ts.map +1 -1
  178. package/dist/server/methods/loop_resolve.js +42 -40
  179. package/dist/server/methods/loop_resolve.js.map +1 -1
  180. package/dist/server/methods/loop_run.d.ts +3 -1
  181. package/dist/server/methods/loop_run.d.ts.map +1 -1
  182. package/dist/server/methods/loop_run.js +104 -102
  183. package/dist/server/methods/loop_run.js.map +1 -1
  184. package/dist/server/methods/op_copy.d.ts +3 -1
  185. package/dist/server/methods/op_copy.d.ts.map +1 -1
  186. package/dist/server/methods/op_copy.js +25 -23
  187. package/dist/server/methods/op_copy.js.map +1 -1
  188. package/dist/server/methods/op_dispatch.d.ts +3 -1
  189. package/dist/server/methods/op_dispatch.d.ts.map +1 -1
  190. package/dist/server/methods/op_dispatch.js +18 -16
  191. package/dist/server/methods/op_dispatch.js.map +1 -1
  192. package/dist/server/methods/op_edit.d.ts +3 -1
  193. package/dist/server/methods/op_edit.d.ts.map +1 -1
  194. package/dist/server/methods/op_edit.js +23 -21
  195. package/dist/server/methods/op_edit.js.map +1 -1
  196. package/dist/server/methods/op_exec.d.ts +3 -1
  197. package/dist/server/methods/op_exec.d.ts.map +1 -1
  198. package/dist/server/methods/op_exec.js +20 -18
  199. package/dist/server/methods/op_exec.js.map +1 -1
  200. package/dist/server/methods/op_find.d.ts +3 -1
  201. package/dist/server/methods/op_find.d.ts.map +1 -1
  202. package/dist/server/methods/op_find.js +23 -21
  203. package/dist/server/methods/op_find.js.map +1 -1
  204. package/dist/server/methods/op_hide.d.ts +3 -1
  205. package/dist/server/methods/op_hide.d.ts.map +1 -1
  206. package/dist/server/methods/op_hide.js +23 -21
  207. package/dist/server/methods/op_hide.js.map +1 -1
  208. package/dist/server/methods/op_move.d.ts +3 -1
  209. package/dist/server/methods/op_move.d.ts.map +1 -1
  210. package/dist/server/methods/op_move.js +23 -21
  211. package/dist/server/methods/op_move.js.map +1 -1
  212. package/dist/server/methods/op_parse.d.ts +3 -1
  213. package/dist/server/methods/op_parse.d.ts.map +1 -1
  214. package/dist/server/methods/op_parse.js +24 -22
  215. package/dist/server/methods/op_parse.js.map +1 -1
  216. package/dist/server/methods/op_read.d.ts +3 -1
  217. package/dist/server/methods/op_read.d.ts.map +1 -1
  218. package/dist/server/methods/op_read.js +23 -21
  219. package/dist/server/methods/op_read.js.map +1 -1
  220. package/dist/server/methods/op_send.d.ts +3 -1
  221. package/dist/server/methods/op_send.d.ts.map +1 -1
  222. package/dist/server/methods/op_send.js +22 -20
  223. package/dist/server/methods/op_send.js.map +1 -1
  224. package/dist/server/methods/op_show.d.ts +3 -1
  225. package/dist/server/methods/op_show.d.ts.map +1 -1
  226. package/dist/server/methods/op_show.js +23 -21
  227. package/dist/server/methods/op_show.js.map +1 -1
  228. package/dist/server/methods/ping.d.ts +3 -1
  229. package/dist/server/methods/ping.d.ts.map +1 -1
  230. package/dist/server/methods/ping.js +8 -6
  231. package/dist/server/methods/ping.js.map +1 -1
  232. package/dist/server/methods/providers_list.d.ts +3 -1
  233. package/dist/server/methods/providers_list.d.ts.map +1 -1
  234. package/dist/server/methods/providers_list.js +19 -17
  235. package/dist/server/methods/providers_list.js.map +1 -1
  236. package/dist/server/methods/session_attach.d.ts +3 -1
  237. package/dist/server/methods/session_attach.d.ts.map +1 -1
  238. package/dist/server/methods/session_attach.js +43 -41
  239. package/dist/server/methods/session_attach.js.map +1 -1
  240. package/dist/server/methods/session_create.d.ts +3 -1
  241. package/dist/server/methods/session_create.d.ts.map +1 -1
  242. package/dist/server/methods/session_create.js +51 -49
  243. package/dist/server/methods/session_create.js.map +1 -1
  244. package/dist/server/methods/session_list.d.ts +3 -1
  245. package/dist/server/methods/session_list.d.ts.map +1 -1
  246. package/dist/server/methods/session_list.js +9 -7
  247. package/dist/server/methods/session_list.js.map +1 -1
  248. package/dist/server/methods/session_runs.d.ts +3 -1
  249. package/dist/server/methods/session_runs.d.ts.map +1 -1
  250. package/dist/server/methods/session_runs.js +19 -17
  251. package/dist/server/methods/session_runs.js.map +1 -1
  252. package/dist/server/methods/session_set_persona.d.ts +3 -1
  253. package/dist/server/methods/session_set_persona.d.ts.map +1 -1
  254. package/dist/server/methods/session_set_persona.js +28 -26
  255. package/dist/server/methods/session_set_persona.js.map +1 -1
  256. package/dist/server/methods/session_set_root.d.ts +3 -1
  257. package/dist/server/methods/session_set_root.d.ts.map +1 -1
  258. package/dist/server/methods/session_set_root.js +31 -29
  259. package/dist/server/methods/session_set_root.js.map +1 -1
  260. package/dist/server/noProposals.d.ts +6 -0
  261. package/dist/server/noProposals.d.ts.map +1 -0
  262. package/dist/server/noProposals.js +37 -0
  263. package/dist/server/noProposals.js.map +1 -0
  264. package/dist/server/yolo.d.ts +3 -1
  265. package/dist/server/yolo.d.ts.map +1 -1
  266. package/dist/server/yolo.js +15 -13
  267. package/dist/server/yolo.js.map +1 -1
  268. package/package.json +71 -32
  269. package/bin/plurnk-service.js +0 -112
  270. /package/migrations/{001_schema.sql → 0000-00-00.01_schema.sql} +0 -0
@@ -4,41 +4,43 @@
4
4
  import { WebSocketServer } from "ws";
5
5
  import { readFile } from "node:fs/promises";
6
6
  import { resolve } from "node:path";
7
- import { PATHS } from "../index.js";
7
+ import { Paths } from "../index.js";
8
8
  import Engine from "../core/Engine.js";
9
+ import ExecutorRegistry from "../core/ExecutorRegistry.js";
9
10
  import SchemeRegistry from "../core/SchemeRegistry.js";
10
11
  import { Mimetypes } from "@plurnk/plurnk-mimetypes";
11
- import { discoverPlugins, loadPlugin } from "../core/PluginLoader.js";
12
+ import PluginLoader from "../core/PluginLoader.js";
12
13
  import MethodRegistry from "./MethodRegistry.js";
13
14
  import ClientConnection from "./ClientConnection.js";
14
- import { fetchLogEntry } from "./logEntry.js";
15
+ import LogEntry from "./logEntry.js";
16
+ import Yolo from "./yolo.js";
17
+ import NoProposals from "./noProposals.js";
15
18
  import { DEFAULT_LOOP_FLAGS } from "../core/scheme-types.js";
16
- import { register as registerPing } from "./methods/ping.js";
17
- import { register as registerDiscover } from "./methods/discover.js";
18
- import { register as registerSessionCreate } from "./methods/session_create.js";
19
- import { register as registerSessionList } from "./methods/session_list.js";
20
- import { register as registerSessionAttach } from "./methods/session_attach.js";
21
- import { register as registerSessionRuns } from "./methods/session_runs.js";
22
- import { register as registerSessionSetRoot } from "./methods/session_set_root.js";
23
- import { register as registerSessionSetPersona } from "./methods/session_set_persona.js";
24
- import { register as registerOpEdit } from "./methods/op_edit.js";
25
- import { register as registerOpRead } from "./methods/op_read.js";
26
- import { register as registerOpFind } from "./methods/op_find.js";
27
- import { register as registerOpShow } from "./methods/op_show.js";
28
- import { register as registerOpHide } from "./methods/op_hide.js";
29
- import { register as registerOpCopy } from "./methods/op_copy.js";
30
- import { register as registerOpMove } from "./methods/op_move.js";
31
- import { register as registerOpSend } from "./methods/op_send.js";
32
- import { register as registerOpExec } from "./methods/op_exec.js";
33
- import { register as registerOpDispatch } from "./methods/op_dispatch.js";
34
- import { register as registerOpParse } from "./methods/op_parse.js";
35
- import { register as registerLoopRun } from "./methods/loop_run.js";
36
- import { register as registerLoopCancel } from "./methods/loop_cancel.js";
37
- import { register as registerEntryRead } from "./methods/entry_read.js";
38
- import { register as registerLogRead } from "./methods/log_read.js";
39
- import { register as registerProvidersList } from "./methods/providers_list.js";
40
- import { register as registerLoopResolve } from "./methods/loop_resolve.js";
41
- import { attachYolo } from "./yolo.js";
19
+ import PingMethod from "./methods/ping.js";
20
+ import DiscoverMethod from "./methods/discover.js";
21
+ import SessionCreateMethod from "./methods/session_create.js";
22
+ import SessionListMethod from "./methods/session_list.js";
23
+ import SessionAttachMethod from "./methods/session_attach.js";
24
+ import SessionRunsMethod from "./methods/session_runs.js";
25
+ import SessionSetRootMethod from "./methods/session_set_root.js";
26
+ import SessionSetPersonaMethod from "./methods/session_set_persona.js";
27
+ import OpEditMethod from "./methods/op_edit.js";
28
+ import OpReadMethod from "./methods/op_read.js";
29
+ import OpFindMethod from "./methods/op_find.js";
30
+ import OpShowMethod from "./methods/op_show.js";
31
+ import OpHideMethod from "./methods/op_hide.js";
32
+ import OpCopyMethod from "./methods/op_copy.js";
33
+ import OpMoveMethod from "./methods/op_move.js";
34
+ import OpSendMethod from "./methods/op_send.js";
35
+ import OpExecMethod from "./methods/op_exec.js";
36
+ import OpDispatchMethod from "./methods/op_dispatch.js";
37
+ import OpParseMethod from "./methods/op_parse.js";
38
+ import LoopRunMethod from "./methods/loop_run.js";
39
+ import LoopCancelMethod from "./methods/loop_cancel.js";
40
+ import EntryReadMethod from "./methods/entry_read.js";
41
+ import LogReadMethod from "./methods/log_read.js";
42
+ import ProvidersListMethod from "./methods/providers_list.js";
43
+ import LoopResolveMethod from "./methods/loop_resolve.js";
42
44
  export default class Daemon {
43
45
  #db;
44
46
  #engine;
@@ -49,14 +51,21 @@ export default class Daemon {
49
51
  #nodeModulesPath;
50
52
  #wss = null;
51
53
  #connections = new Set();
52
- // Run-level drain registry (rummy AgentLoop parallel). At most one
53
- // drain per run; concurrent loop.run calls inject into the active
54
- // drain instead of starting parallel runLoops.
54
+ // Run-level drain registry. At most one drain per run. The stored object
55
+ // is the drain's identity handle: start/exit compare it by reference so a
56
+ // drain exiting never clobbers a successor that raced in, and a loop
57
+ // enqueued during teardown is never stranded. A drain is a pure queue
58
+ // consumer (claim → run → exit on empty queue); streams live independently
59
+ // (subscriptions + Exec.idle), and a concluding stream routes through
60
+ // inject() like any other loop source.
55
61
  #activeDrains = new Map();
56
- // Wake/event signals for drains parked on "queue empty, active subs in
57
- // flight." A wake-on-completion engine.inject can resolve the pending
58
- // promise so the drain re-checks the queue without polling.
59
- #drainPokes = new Map();
62
+ // Per-run cancellation scope. Loops AND the streams they spawn (execs)
63
+ // share this signal, so loop.cancel / shutdown abort it once and every
64
+ // in-flight subscription tears down even a spawn that registers AFTER the
65
+ // cancel self-aborts against the already-aborted signal (no race). Outlives
66
+ // any single (ephemeral) drain; replaced with a fresh controller once
67
+ // aborted so a later loop.run isn't born cancelled.
68
+ #runAborts = new Map();
60
69
  constructor({ db, schemes, mimetypes, provider, nodeModulesPath, }) {
61
70
  this.#db = db;
62
71
  this.#schemes = schemes ?? new SchemeRegistry();
@@ -74,6 +83,11 @@ export default class Daemon {
74
83
  });
75
84
  this.#engine = new Engine({
76
85
  db, schemes: this.#schemes, mimetypes: this.#mimetypes,
86
+ // Same provider-backed source as the Mimetypes tokenize lambda
87
+ // above; sync here because countTokens is sync (§2.1) and the
88
+ // write helpers store the count inline. Divisor tripwire only
89
+ // until a provider is resolved.
90
+ tokenize: (text) => this.#provider?.countTokens(text) ?? Math.ceil(text.length / 4),
77
91
  streamEventNotify: (sessionId, event) => this.notifyStreamEvent(sessionId, event),
78
92
  wakeRunNotify: (payload) => { void this.#handleWakeRun(payload); },
79
93
  telemetryEventNotify: (sessionId, payload) => this.notifyTelemetryEvent(sessionId, payload),
@@ -102,7 +116,11 @@ export default class Daemon {
102
116
  });
103
117
  // In-tree YOLO listener — auto-accepts proposals when the loop's
104
118
  // persisted flags.yolo === true. Skips client roundtrip entirely.
105
- attachYolo(this.#engine, this.#db);
119
+ Yolo.attachYolo(this.#engine, this.#db);
120
+ // Inverse of YOLO: auto-REJECT proposals in-process when the loop's
121
+ // persisted flags.noProposals === true (client has no review channel).
122
+ // The model sees an ordinary 400, never the orchestration reason.
123
+ NoProposals.attachNoProposals(this.#engine, this.#db);
106
124
  }
107
125
  get registry() { return this.#registry; }
108
126
  get engine() { return this.#engine; }
@@ -116,6 +134,11 @@ export default class Daemon {
116
134
  // Mimetypes owns its own discovery scan over @plurnk/plurnk-mimetypes-*
117
135
  // packages; pre-warm it so first index render doesn't pay the cost.
118
136
  await this.#mimetypes.ready();
137
+ // Discover + probe the installed executor siblings, then hand the
138
+ // registry to the engine for exec dispatch (plurnk-service#181). The
139
+ // shell is the default runtime, so its executor must boot usable.
140
+ const executors = await ExecutorRegistry.build({ defaultRuntime: "sh" });
141
+ this.#engine.setExecutors(executors);
119
142
  return new Promise((resolve, reject) => {
120
143
  const wss = new WebSocketServer({ host, port });
121
144
  wss.on("listening", () => {
@@ -154,8 +177,14 @@ export default class Daemon {
154
177
  // to completion, (3) drain streaming schemes' background work
155
178
  // (exec spawn cleanup, channel writes). Only THEN close the DB
156
179
  // upstream — drain queries hit the DB right up until they exit.
157
- for (const drain of this.#activeDrains.values())
158
- drain.controller.abort("daemon_stopping");
180
+ // Abort every run's cancellation scope — stops in-flight loops AND the
181
+ // streams (background execs) linked to them, so idle() doesn't block on
182
+ // a long-running command. Covers runs whose drain already exited but
183
+ // whose exec is still in flight.
184
+ for (const scope of this.#runAborts.values()) {
185
+ if (!scope.signal.aborted)
186
+ scope.abort("daemon_stopping");
187
+ }
159
188
  const drainPromises = [...this.#activeDrains.values()].map((d) => d.promise);
160
189
  await Promise.allSettled(drainPromises);
161
190
  await this.#drainStreamingSchemes();
@@ -168,31 +197,31 @@ export default class Daemon {
168
197
  await exec.idle();
169
198
  }
170
199
  #registerBuiltins() {
171
- registerPing(this.#registry);
172
- registerDiscover(this.#registry);
173
- registerSessionCreate(this.#registry);
174
- registerSessionList(this.#registry);
175
- registerSessionAttach(this.#registry);
176
- registerSessionRuns(this.#registry);
177
- registerSessionSetRoot(this.#registry);
178
- registerSessionSetPersona(this.#registry);
179
- registerOpEdit(this.#registry);
180
- registerOpRead(this.#registry);
181
- registerOpFind(this.#registry);
182
- registerOpShow(this.#registry);
183
- registerOpHide(this.#registry);
184
- registerOpCopy(this.#registry);
185
- registerOpMove(this.#registry);
186
- registerOpSend(this.#registry);
187
- registerOpExec(this.#registry);
188
- registerOpDispatch(this.#registry);
189
- registerOpParse(this.#registry);
190
- registerLoopRun(this.#registry);
191
- registerLoopCancel(this.#registry);
192
- registerLoopResolve(this.#registry);
193
- registerEntryRead(this.#registry);
194
- registerLogRead(this.#registry);
195
- registerProvidersList(this.#registry);
200
+ PingMethod.register(this.#registry);
201
+ DiscoverMethod.register(this.#registry);
202
+ SessionCreateMethod.register(this.#registry);
203
+ SessionListMethod.register(this.#registry);
204
+ SessionAttachMethod.register(this.#registry);
205
+ SessionRunsMethod.register(this.#registry);
206
+ SessionSetRootMethod.register(this.#registry);
207
+ SessionSetPersonaMethod.register(this.#registry);
208
+ OpEditMethod.register(this.#registry);
209
+ OpReadMethod.register(this.#registry);
210
+ OpFindMethod.register(this.#registry);
211
+ OpShowMethod.register(this.#registry);
212
+ OpHideMethod.register(this.#registry);
213
+ OpCopyMethod.register(this.#registry);
214
+ OpMoveMethod.register(this.#registry);
215
+ OpSendMethod.register(this.#registry);
216
+ OpExecMethod.register(this.#registry);
217
+ OpDispatchMethod.register(this.#registry);
218
+ OpParseMethod.register(this.#registry);
219
+ LoopRunMethod.register(this.#registry);
220
+ LoopCancelMethod.register(this.#registry);
221
+ LoopResolveMethod.register(this.#registry);
222
+ EntryReadMethod.register(this.#registry);
223
+ LogReadMethod.register(this.#registry);
224
+ ProvidersListMethod.register(this.#registry);
196
225
  }
197
226
  #registerNotifications() {
198
227
  this.#registry.registerNotification("log/entry", {
@@ -224,6 +253,7 @@ export default class Daemon {
224
253
  description: "A channel's content grew or its state transitioned. Scoped to the entry's session. Metadata-only; clients fetch new content via entry.read or op.read.",
225
254
  params: {
226
255
  entryId: "number — the entry whose channel changed",
256
+ target: "string — the entry's URI (scheme://pathname); clients route on this without an entryId→URI lookup",
227
257
  channel: "string — the channel name",
228
258
  state: "string — current state (static, active, closed, errored)",
229
259
  contentLength: "number — current length of the channel's content",
@@ -240,6 +270,7 @@ export default class Daemon {
240
270
  description: "A streaming-scheme subscription closed (the underlying connection / subprocess finished, errored, or was cancelled). Scoped to the entry's session. wakeAction describes whether the daemon opened a fresh loop to surface the conclusion to the model.",
241
271
  params: {
242
272
  entryId: "number",
273
+ target: "string — the entry's URI (scheme://pathname)",
243
274
  subscriptionId: "number",
244
275
  scheme: "string — the scheme that owned the subscription (e.g. 'exec')",
245
276
  closeStatus: "number — 200 (clean) / 500 (error) / 499 (aborted)",
@@ -281,21 +312,14 @@ export default class Daemon {
281
312
  */
282
313
  async inject(args) {
283
314
  const { sessionId, runId, prompt } = args;
284
- // Wake any drain parked on "queue empty + active subs" so it
285
- // re-checks the queue immediately regardless of which branch we
286
- // take. The poke fires before we return; if we enqueue a new
287
- // loop, the drain we start below claims it on its first iteration.
288
- this.#pokeDrain(runId);
289
- const drainActive = this.#activeDrains.has(runId);
290
- if (drainActive) {
315
+ // Active loop (status=102)? Fold the wake/prompt into its next turn.
316
+ // engine.inject returns null when no loop is currently executing, so
317
+ // we enqueue a fresh loop below and ensure a drain claims it.
318
+ if (this.#activeDrains.has(runId)) {
291
319
  const result = await this.#engine.inject(runId, prompt);
292
320
  if (result !== null) {
293
321
  return { action: "injected_next_turn", loopId: result.loopId, turnSeq: result.turnSeq };
294
322
  }
295
- // Race: #activeDrains exists but drain hasn't yet claimed a
296
- // loop (status=102). Fall through to the enqueue path — but
297
- // we must NOT start a parallel drain. The existing drain
298
- // will claim this loop on a subsequent iteration.
299
323
  }
300
324
  // Enqueue a fresh loop. Persist flags + persona override on the row.
301
325
  const seqRow = await this.#db.loop_run_next_sequence.get({ run_id: runId });
@@ -313,18 +337,19 @@ export default class Daemon {
313
337
  loop_id: loopId, flags: JSON.stringify(merged),
314
338
  });
315
339
  }
316
- // Existing drain will pick this loop up don't start a parallel one.
317
- if (drainActive) {
318
- // Poke the existing drain in case it's parked.
319
- this.#pokeDrain(runId);
320
- return { action: "enqueued_new_loop", loopId };
321
- }
322
- const { firstLoopPromise, drainPromise } = this.#startDrain({
340
+ // Guarantee a drain claims the loop we just enqueued. Synchronous
341
+ // check-and-start (no await between the membership test and the
342
+ // registry write): a live drain re-claims it; otherwise we start one.
343
+ // The drain's exit coordinates via an identity-checked re-claim so the
344
+ // loop is never stranded (the lost-loop hang). firstLoopPromise is
345
+ // present only when THIS call started the drain — loop.run keys its
346
+ // fast-path response on that.
347
+ const started = this.#ensureDrain({
323
348
  sessionId, runId, provider: args.provider,
324
349
  systemPrompt: args.systemPrompt, personaDefault: args.persona,
325
350
  maxTurns: args.maxTurns ?? Number(process.env.PLURNK_MAX_TURNS ?? "50"),
326
351
  });
327
- return { action: "enqueued_new_loop", loopId, firstLoopPromise, drainPromise };
352
+ return { action: "enqueued_new_loop", loopId, ...(started ?? {}) };
328
353
  }
329
354
  /**
330
355
  * Start a drain for the given run. The drain claims queued loops via
@@ -341,7 +366,12 @@ export default class Daemon {
341
366
  */
342
367
  #startDrain(opts) {
343
368
  const { sessionId, runId, provider, systemPrompt, personaDefault, maxTurns } = opts;
344
- const controller = new AbortController();
369
+ // The drain runs under the run's cancellation scope (shared with the
370
+ // execs its loops spawn), so loop.cancel/shutdown abort it as a unit.
371
+ const controller = this.#runSignal(runId);
372
+ const handle = {
373
+ controller, promise: Promise.resolve(),
374
+ };
345
375
  let resolveFirst = () => { };
346
376
  let rejectFirst = () => { };
347
377
  const firstLoopPromise = new Promise((res, rej) => {
@@ -349,55 +379,65 @@ export default class Daemon {
349
379
  rejectFirst = rej;
350
380
  });
351
381
  let firstSettled = false;
382
+ const claim = () => this.#db.drain_claim_next_loop.get({ run_id: runId });
352
383
  const drainPromise = (async () => {
353
384
  let loopsDrained = 0;
354
385
  let lastResult = null;
355
386
  try {
356
387
  while (true) {
357
388
  controller.signal.throwIfAborted();
358
- const loopRow = await this.#db.drain_claim_next_loop.get({ run_id: runId });
359
- if (loopRow !== undefined) {
360
- const onDispatch = (logEntryId) => {
361
- void (async () => {
362
- const entry = await fetchLogEntry(this.#db, logEntryId);
363
- this.#broadcast({ sessionId }, null, "log/entry", { entry });
364
- })();
365
- };
366
- const result = await this.#engine.runLoop({
367
- provider, sessionId, runId, loopId: loopRow.id, maxTurns,
368
- messages: [
369
- { role: "system", content: systemPrompt },
370
- { role: "user", content: loopRow.prompt },
371
- ],
372
- persona: personaDefault,
373
- origin: "model",
374
- onDispatch,
375
- signal: controller.signal,
376
- });
377
- this.#broadcast({ sessionId }, null, "loop/terminated", {
378
- loopId: loopRow.id,
379
- finalStatus: result.finalStatus,
380
- hitMaxTurns: result.hitMaxTurns,
381
- });
382
- loopsDrained++;
383
- const loopResult = {
384
- loopId: loopRow.id,
385
- turnIds: result.turnIds,
386
- finalStatus: result.finalStatus,
387
- hitMaxTurns: result.hitMaxTurns,
388
- };
389
- lastResult = loopResult;
390
- if (!firstSettled) {
391
- firstSettled = true;
392
- resolveFirst(loopResult);
393
- }
394
- continue;
389
+ let loopRow = await claim();
390
+ if (loopRow === undefined) {
391
+ // Queue empty exit. Coordinate with a concurrent
392
+ // inject + #ensureDrain: relinquish ownership, then
393
+ // re-claim once. A loop that raced in during teardown
394
+ // is caught here (re-acquire + run it); otherwise exit.
395
+ // Identity-checked so we never delete a successor entry.
396
+ if (this.#activeDrains.get(runId) === handle)
397
+ this.#activeDrains.delete(runId);
398
+ loopRow = await claim();
399
+ if (loopRow === undefined)
400
+ break;
401
+ this.#activeDrains.set(runId, handle);
395
402
  }
396
- // Queue empty. Stream-aware: don't exit while subs are open.
397
- const subRow = await this.#db.drain_count_active_subs_for_run.get({ run_id: runId });
398
- if ((subRow?.n ?? 0) === 0)
399
- break;
400
- await this.#awaitDrainPoke(runId, controller.signal);
403
+ const onDispatch = (logEntryId) => {
404
+ void (async () => {
405
+ const entry = await LogEntry.fetchLogEntry(this.#db, logEntryId);
406
+ this.#broadcast({ sessionId }, null, "log/entry", { entry });
407
+ })();
408
+ };
409
+ const result = await this.#engine.runLoop({
410
+ provider, sessionId, runId, loopId: loopRow.id, maxTurns,
411
+ messages: [
412
+ { role: "system", content: systemPrompt },
413
+ { role: "user", content: loopRow.prompt },
414
+ ],
415
+ persona: personaDefault,
416
+ origin: "model",
417
+ onDispatch,
418
+ signal: controller.signal,
419
+ });
420
+ this.#broadcast({ sessionId }, null, "loop/terminated", {
421
+ loopId: loopRow.id,
422
+ finalStatus: result.finalStatus,
423
+ hitMaxTurns: result.hitMaxTurns,
424
+ });
425
+ loopsDrained++;
426
+ const loopResult = {
427
+ loopId: loopRow.id,
428
+ turnIds: result.turnIds,
429
+ finalStatus: result.finalStatus,
430
+ hitMaxTurns: result.hitMaxTurns,
431
+ };
432
+ lastResult = loopResult;
433
+ if (!firstSettled) {
434
+ firstSettled = true;
435
+ resolveFirst(loopResult);
436
+ }
437
+ // A next-turn prompt this loop ended before consuming (a
438
+ // wake conclusion or a loop.run-while-active) is promoted to
439
+ // a fresh queued loop so it's never silently dropped.
440
+ await this.#reconcileOrphanedWake(runId, loopRow.id);
401
441
  }
402
442
  }
403
443
  catch (err) {
@@ -412,59 +452,83 @@ export default class Daemon {
412
452
  firstSettled = true;
413
453
  rejectFirst(new Error("drain exited without producing a result"));
414
454
  }
415
- this.#activeDrains.delete(runId);
416
- this.#drainPokes.delete(runId);
455
+ if (this.#activeDrains.get(runId) === handle)
456
+ this.#activeDrains.delete(runId);
417
457
  }
418
458
  return { loopsDrained, lastResult };
419
459
  })();
420
- this.#activeDrains.set(runId, { controller, promise: drainPromise });
421
- // Attach a swallowing handler so unhandled rejections (e.g. when
422
- // the drain aborts and no caller awaited drainPromise) don't
423
- // crash the process. The error already surfaced via firstLoopPromise
424
- // if relevant, or was already logged inside the drain body.
460
+ handle.promise = drainPromise;
461
+ this.#activeDrains.set(runId, handle);
462
+ // Swallow unhandled rejections (drain aborts with no awaiter); the
463
+ // error already surfaced via firstLoopPromise or was logged inside.
425
464
  drainPromise.catch(() => { });
426
- // firstLoopPromise also needs a handler in case nobody awaits it
427
- // (e.g. wake-on-completion enqueue path doesn't always read it).
428
465
  firstLoopPromise.catch(() => { });
429
466
  return { firstLoopPromise, drainPromise };
430
467
  }
431
- #pokeDrain(runId) {
432
- const poke = this.#drainPokes.get(runId);
433
- if (poke !== undefined) {
434
- this.#drainPokes.delete(runId);
435
- poke();
436
- }
468
+ // Idempotent, synchronous drain guarantee. A live drain will claim the
469
+ // just-enqueued loop in its own iteration (or its exit re-claim) → return
470
+ // null. Otherwise start one. MUST be called synchronously after the
471
+ // enqueue (no await between) so the membership test and #startDrain's
472
+ // registry write are one tick — two concurrent injects can't both start a
473
+ // drain for the same run.
474
+ #ensureDrain(opts) {
475
+ if (this.#activeDrains.has(opts.runId))
476
+ return null;
477
+ return this.#startDrain(opts);
437
478
  }
438
- #awaitDrainPoke(runId, signal) {
439
- return new Promise((resolve, reject) => {
440
- if (signal.aborted) {
441
- reject(new Error("drain aborted"));
442
- return;
443
- }
444
- const onAbort = () => {
445
- this.#drainPokes.delete(runId);
446
- reject(new Error("drain aborted"));
447
- };
448
- signal.addEventListener("abort", onAbort, { once: true });
449
- this.#drainPokes.set(runId, () => {
450
- signal.removeEventListener("abort", onAbort);
451
- resolve();
452
- });
479
+ // After a loop terminates, promote any next-turn prompt it never consumed —
480
+ // an injected wake (stream conclusion) or a loop.run-while-active prompt
481
+ // that landed on a turn the loop didn't reach — into a fresh queued loop.
482
+ // The drain claims it on its next iteration, so a conclusion or client
483
+ // prompt is never silently dropped. Inherits the ended loop's flags + persona.
484
+ async #reconcileOrphanedWake(runId, endedLoopId) {
485
+ const prefix = `prompt/${endedLoopId}/`;
486
+ const orphan = await this.#db.drain_orphaned_prompt_for_loop.get({ loop_id: endedLoopId, pattern: `${prefix}%`, prefix_len: prefix.length });
487
+ if (orphan === undefined)
488
+ return;
489
+ const seqRow = await this.#db.loop_run_next_sequence.get({ run_id: runId });
490
+ if (seqRow === undefined)
491
+ throw new Error("reconcileOrphanedWake: next-sequence query returned no row");
492
+ const fresh = await this.#db.drain_enqueue_loop.get({
493
+ run_id: runId, sequence: seqRow.next, prompt: orphan.body, persona: orphan.persona,
453
494
  });
495
+ if (fresh === undefined)
496
+ throw new Error("reconcileOrphanedWake: enqueue returned no row");
497
+ if (orphan.flags !== null) {
498
+ await this.#db.engine_set_loop_flags.run({ loop_id: fresh.id, flags: orphan.flags });
499
+ }
500
+ }
501
+ // The run's cancellation scope — lazily created, and replaced once aborted
502
+ // so a later loop.run gets a live signal. The drain and the execs its loops
503
+ // spawn all run under it.
504
+ #runSignal(runId) {
505
+ const existing = this.#runAborts.get(runId);
506
+ if (existing !== undefined && !existing.signal.aborted)
507
+ return existing;
508
+ const fresh = new AbortController();
509
+ this.#runAborts.set(runId, fresh);
510
+ return fresh;
454
511
  }
455
512
  /**
456
- * Cancel the active drain for a run. Fires the drain's AbortController;
457
- * the engine's per-loop loopAbort cascades from the runLoop signal arg,
458
- * so in-flight scheme operations (exec spawns, future SSE/WS) tear
459
- * down. Queued loops in the run remain enqueued call again if you
460
- * want them cleared, or rely on subsequent loop.run to resume.
513
+ * Cancel the run's in-flight work (loop.cancel). One abort, one scope: the
514
+ * run signal stops the running loop's turn generation AND tears down every
515
+ * stream linked to it — a background exec that outlived its loop, or even a
516
+ * spawn that registers after this abort (it self-aborts against the aborted
517
+ * signal). Returns cancelled iff there was work. Queued loops stay enqueued.
461
518
  */
462
519
  cancelDrain(runId, reason = "user_cancelled") {
463
- const drain = this.#activeDrains.get(runId);
464
- if (drain === undefined)
465
- return false;
466
- drain.controller.abort(reason);
467
- return true;
520
+ const hadWork = this.#activeDrains.has(runId) || this.#runHasActiveStreams(runId);
521
+ const scope = this.#runAborts.get(runId);
522
+ if (scope !== undefined && !scope.signal.aborted)
523
+ scope.abort(reason);
524
+ return hadWork;
525
+ }
526
+ // Does the run have an in-flight stream (a background exec)? Used only for
527
+ // loop.cancel's cancelled=true/false answer; the teardown itself rides the
528
+ // run signal. Duck-typed like #drainStreamingSchemes.
529
+ #runHasActiveStreams(runId) {
530
+ const exec = this.#schemes.get("exec");
531
+ return exec?.hasActiveSpawns?.(runId) ?? false;
468
532
  }
469
533
  /**
470
534
  * Wake-on-completion handler. Streaming schemes call this when a
@@ -480,12 +544,6 @@ export default class Daemon {
480
544
  * Rummy parallel: plugins/stream/stream.js stream/completed wake:true.
481
545
  */
482
546
  async #handleWakeRun(payload) {
483
- // Every subscription close pokes the run's drain (if any) so a
484
- // parked stream-aware drain re-checks active subs count. This
485
- // happens regardless of whether we go on to inject a new loop,
486
- // so the 499-skipped / no-provider paths still wake the drain
487
- // out of its "queue empty, waiting for streams" state.
488
- this.#pokeDrain(payload.runId);
489
547
  // Aborted streams don't wake — the abort was deliberate.
490
548
  if (payload.closeStatus === 499) {
491
549
  this.#broadcast({ sessionId: payload.sessionId }, null, "stream/concluded", {
@@ -500,8 +558,8 @@ export default class Daemon {
500
558
  return;
501
559
  }
502
560
  try {
503
- const systemPrompt = await readFile(PATHS.instructionsSystem, "utf8");
504
- const personaText = await readFile(PATHS.defaultPersona, "utf8");
561
+ const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
562
+ const personaText = await readFile(Paths.defaultPersona, "utf8");
505
563
  // Unified path: this.inject decides whether to write a prompt
506
564
  // entry for an active drain's next turn (no-op-active-loop) or
507
565
  // enqueue a fresh loop + drain (opened-loop). The summary
@@ -558,11 +616,11 @@ export default class Daemon {
558
616
  // bin script). Mimetypes self-discovers — Mimetypes.ready() in start()
559
617
  // scans @plurnk/plurnk-mimetypes-* packages via the framework's own
560
618
  // discover().
561
- const plugins = await discoverPlugins(this.#nodeModulesPath);
619
+ const plugins = await PluginLoader.discoverPlugins(this.#nodeModulesPath);
562
620
  for (const plugin of plugins) {
563
621
  if (plugin.manifest.kind !== "scheme")
564
622
  continue;
565
- const instance = await loadPlugin(plugin);
623
+ const instance = await PluginLoader.loadPlugin(plugin);
566
624
  this.#schemes.register(plugin.manifest.name, instance);
567
625
  }
568
626
  }