@plurnk/plurnk-service 1.0.5 → 1.0.6

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 (201) hide show
  1. package/.env.defaults +55 -31
  2. package/INSTALL.md +3 -3
  3. package/README.md +4 -4
  4. package/SPEC.md +204 -192
  5. package/dist/Paths.js +1 -1
  6. package/dist/Paths.js.map +1 -1
  7. package/dist/core/ChannelWrite.d.ts +18 -18
  8. package/dist/core/ChannelWrite.d.ts.map +1 -1
  9. package/dist/core/ChannelWrite.js +17 -17
  10. package/dist/core/ChannelWrite.js.map +1 -1
  11. package/dist/core/ChannelWrite.sql +11 -11
  12. package/dist/core/Dispatcher.d.ts +11 -11
  13. package/dist/core/Dispatcher.d.ts.map +1 -1
  14. package/dist/core/Dispatcher.js +122 -124
  15. package/dist/core/Dispatcher.js.map +1 -1
  16. package/dist/core/Engine.d.ts +20 -23
  17. package/dist/core/Engine.d.ts.map +1 -1
  18. package/dist/core/Engine.js +207 -183
  19. package/dist/core/Engine.js.map +1 -1
  20. package/dist/core/Engine.sql +106 -109
  21. package/dist/core/PacketBuilder.d.ts +9 -9
  22. package/dist/core/PacketBuilder.d.ts.map +1 -1
  23. package/dist/core/PacketBuilder.js +183 -115
  24. package/dist/core/PacketBuilder.js.map +1 -1
  25. package/dist/core/ProposalLifecycle.d.ts +8 -8
  26. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  27. package/dist/core/ProposalLifecycle.js +10 -10
  28. package/dist/core/ProposalLifecycle.js.map +1 -1
  29. package/dist/core/ProviderInstantiate.d.ts +1 -0
  30. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  31. package/dist/core/ProviderInstantiate.js +9 -3
  32. package/dist/core/ProviderInstantiate.js.map +1 -1
  33. package/dist/core/SchemeRegistry.d.ts +4 -1
  34. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  35. package/dist/core/SchemeRegistry.js +18 -9
  36. package/dist/core/SchemeRegistry.js.map +1 -1
  37. package/dist/core/TelemetryChannel.d.ts +5 -5
  38. package/dist/core/TelemetryChannel.d.ts.map +1 -1
  39. package/dist/core/TelemetryChannel.js +9 -9
  40. package/dist/core/TelemetryChannel.js.map +1 -1
  41. package/dist/core/caps/CapsResolve.js +2 -2
  42. package/dist/core/caps/CapsResolve.js.map +1 -1
  43. package/dist/core/caps/DbNotifyCaps.js +2 -2
  44. package/dist/core/caps/DbNotifyCaps.js.map +1 -1
  45. package/dist/core/caps/DbSubscriptionCaps.js +7 -7
  46. package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
  47. package/dist/core/caps/SchemeCtxImpl.d.ts +2 -2
  48. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  49. package/dist/core/caps/SchemeCtxImpl.js +4 -4
  50. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  51. package/dist/core/fork.d.ts +1 -1
  52. package/dist/core/fork.d.ts.map +1 -1
  53. package/dist/core/fork.js +31 -26
  54. package/dist/core/fork.js.map +1 -1
  55. package/dist/core/fork.sql +41 -32
  56. package/dist/core/git-env.d.ts.map +1 -1
  57. package/dist/core/git-env.js +23 -7
  58. package/dist/core/git-env.js.map +1 -1
  59. package/dist/core/git-iso.d.ts +9 -0
  60. package/dist/core/git-iso.d.ts.map +1 -0
  61. package/dist/core/git-iso.js +227 -0
  62. package/dist/core/git-iso.js.map +1 -0
  63. package/dist/core/git-membership.d.ts +2 -2
  64. package/dist/core/git-membership.d.ts.map +1 -1
  65. package/dist/core/git-membership.js +70 -50
  66. package/dist/core/git-membership.js.map +1 -1
  67. package/dist/core/git-state.d.ts +1 -1
  68. package/dist/core/git-state.d.ts.map +1 -1
  69. package/dist/core/git-state.js +21 -11
  70. package/dist/core/git-state.js.map +1 -1
  71. package/dist/core/packet-wire.d.ts.map +1 -1
  72. package/dist/core/packet-wire.js +81 -22
  73. package/dist/core/packet-wire.js.map +1 -1
  74. package/dist/core/plurnk-uri.d.ts +2 -2
  75. package/dist/core/plurnk-uri.d.ts.map +1 -1
  76. package/dist/core/plurnk-uri.js +19 -12
  77. package/dist/core/plurnk-uri.js.map +1 -1
  78. package/dist/core/run-ops.sql +26 -26
  79. package/dist/core/scheme-types.d.ts +6 -6
  80. package/dist/core/scheme-types.d.ts.map +1 -1
  81. package/dist/core/search-gate.js +0 -0
  82. package/dist/core/search-gate.js.map +1 -1
  83. package/dist/core/token-ruler.js +2 -2
  84. package/dist/core/token-ruler.js.map +1 -1
  85. package/dist/core/worker-cap.d.ts +8 -0
  86. package/dist/core/worker-cap.d.ts.map +1 -0
  87. package/dist/core/worker-cap.js +20 -0
  88. package/dist/core/worker-cap.js.map +1 -0
  89. package/dist/core/{session-settings.d.ts → workspace-settings.d.ts} +5 -5
  90. package/dist/core/workspace-settings.d.ts.map +1 -0
  91. package/dist/core/{session-settings.js → workspace-settings.js} +13 -13
  92. package/dist/core/workspace-settings.js.map +1 -0
  93. package/dist/digest/Digest.d.ts +3 -3
  94. package/dist/digest/Digest.d.ts.map +1 -1
  95. package/dist/digest/Digest.js +79 -73
  96. package/dist/digest/Digest.js.map +1 -1
  97. package/dist/digest/digest.sql +18 -18
  98. package/dist/schemes/Exec.d.ts +9 -2
  99. package/dist/schemes/Exec.d.ts.map +1 -1
  100. package/dist/schemes/Exec.js +121 -68
  101. package/dist/schemes/Exec.js.map +1 -1
  102. package/dist/schemes/ExecOutputScheme.js +3 -3
  103. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  104. package/dist/schemes/File.js +23 -23
  105. package/dist/schemes/File.js.map +1 -1
  106. package/dist/schemes/Known.js +5 -5
  107. package/dist/schemes/Known.js.map +1 -1
  108. package/dist/schemes/Log.d.ts.map +1 -1
  109. package/dist/schemes/Log.js +63 -14
  110. package/dist/schemes/Log.js.map +1 -1
  111. package/dist/schemes/Log.sql +49 -8
  112. package/dist/schemes/Plurnk.js +5 -5
  113. package/dist/schemes/Plurnk.js.map +1 -1
  114. package/dist/schemes/Skill.js +5 -5
  115. package/dist/schemes/Skill.js.map +1 -1
  116. package/dist/schemes/Unknown.js +5 -5
  117. package/dist/schemes/Unknown.js.map +1 -1
  118. package/dist/schemes/{Run.d.ts → Worker.d.ts} +2 -2
  119. package/dist/schemes/Worker.d.ts.map +1 -0
  120. package/dist/schemes/{Run.js → Worker.js} +67 -67
  121. package/dist/schemes/Worker.js.map +1 -0
  122. package/dist/schemes/_entry-crud.js +8 -8
  123. package/dist/schemes/_entry-crud.js.map +1 -1
  124. package/dist/schemes/_entry-crud.sql +29 -29
  125. package/dist/schemes/_entry-find.d.ts +2 -1
  126. package/dist/schemes/_entry-find.d.ts.map +1 -1
  127. package/dist/schemes/_entry-find.js +31 -19
  128. package/dist/schemes/_entry-find.js.map +1 -1
  129. package/dist/schemes/_entry-find.sql +9 -9
  130. package/dist/schemes/_entry-graph.d.ts +2 -2
  131. package/dist/schemes/_entry-graph.d.ts.map +1 -1
  132. package/dist/schemes/_entry-graph.js +18 -18
  133. package/dist/schemes/_entry-graph.js.map +1 -1
  134. package/dist/schemes/_entry-graph.sql +8 -8
  135. package/dist/schemes/_entry-manifest.d.ts +1 -1
  136. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  137. package/dist/schemes/_entry-manifest.js +72 -28
  138. package/dist/schemes/_entry-manifest.js.map +1 -1
  139. package/dist/schemes/_entry-ops.d.ts +4 -4
  140. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  141. package/dist/schemes/_entry-ops.js +25 -25
  142. package/dist/schemes/_entry-ops.js.map +1 -1
  143. package/dist/schemes/_entry-ops.sql +9 -9
  144. package/dist/schemes/_entry-semantic.d.ts +2 -1
  145. package/dist/schemes/_entry-semantic.d.ts.map +1 -1
  146. package/dist/schemes/_entry-semantic.js +23 -4
  147. package/dist/schemes/_entry-semantic.js.map +1 -1
  148. package/dist/schemes/_entry-semantic.sql +18 -4
  149. package/dist/schemes/_entry-send.d.ts +1 -1
  150. package/dist/schemes/_entry-send.d.ts.map +1 -1
  151. package/dist/schemes/_entry-send.js +6 -6
  152. package/dist/schemes/_entry-send.js.map +1 -1
  153. package/dist/schemes/exec-abort.js +1 -1
  154. package/dist/schemes/exec-abort.js.map +1 -1
  155. package/dist/server/Daemon.d.ts +48 -48
  156. package/dist/server/Daemon.d.ts.map +1 -1
  157. package/dist/server/Daemon.js +285 -291
  158. package/dist/server/Daemon.js.map +1 -1
  159. package/dist/server/client-input.js +20 -20
  160. package/dist/server/client-input.js.map +1 -1
  161. package/dist/server/dispatch-as-plurnk.d.ts +1 -1
  162. package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
  163. package/dist/server/dispatch-as-plurnk.js +7 -7
  164. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  165. package/dist/server/drain.sql +27 -27
  166. package/dist/server/envelope.d.ts +20 -20
  167. package/dist/server/envelope.d.ts.map +1 -1
  168. package/dist/server/envelope.js +86 -86
  169. package/dist/server/envelope.js.map +1 -1
  170. package/dist/server/envelope.sql +39 -39
  171. package/dist/server/logEntry.d.ts +1 -1
  172. package/dist/server/logEntry.d.ts.map +1 -1
  173. package/dist/server/logEntry.js +1 -1
  174. package/dist/server/logEntry.js.map +1 -1
  175. package/dist/server/loopDocs.d.ts +1 -1
  176. package/dist/server/loopDocs.d.ts.map +1 -1
  177. package/dist/server/loopDocs.js +6 -6
  178. package/dist/server/loopDocs.js.map +1 -1
  179. package/dist/server/seam-entry-read.sql +2 -2
  180. package/dist/server/seam-log-read.sql +3 -3
  181. package/dist/server/seam-loop.sql +3 -3
  182. package/dist/server/seam-proposal-list.sql +4 -4
  183. package/dist/server/yolo.js +2 -2
  184. package/dist/server/yolo.js.map +1 -1
  185. package/dist/service.d.ts.map +1 -1
  186. package/dist/service.js +21 -6
  187. package/dist/service.js.map +1 -1
  188. package/migrations/0000-00-00.01_schema.sql +82 -70
  189. package/package.json +17 -14
  190. package/dist/core/run-cap.d.ts +0 -8
  191. package/dist/core/run-cap.d.ts.map +0 -1
  192. package/dist/core/run-cap.js +0 -20
  193. package/dist/core/run-cap.js.map +0 -1
  194. package/dist/core/search-prefetch.d.ts +0 -17
  195. package/dist/core/search-prefetch.d.ts.map +0 -1
  196. package/dist/core/search-prefetch.js +0 -164
  197. package/dist/core/search-prefetch.js.map +0 -1
  198. package/dist/core/session-settings.d.ts.map +0 -1
  199. package/dist/core/session-settings.js.map +0 -1
  200. package/dist/schemes/Run.d.ts.map +0 -1
  201. package/dist/schemes/Run.js.map +0 -1
@@ -37,7 +37,7 @@ export default class Daemon {
37
37
  // module (plurnk-agui) subscribes and fans out to its OWN clients; core emits, never fans out
38
38
  // for it. The WS fan-out below is legacy scaffolding that retires at the AG-UI+ cutover.
39
39
  #eventSubscribers = new Set();
40
- // Run-level drain registry. At most one drain per run. The stored object
40
+ // Run-level drain registry. At most one drain per worker. The stored object
41
41
  // is the drain's identity handle: start/exit compare it by reference so a
42
42
  // drain exiting never clobbers a successor that raced in, and a loop
43
43
  // enqueued during teardown is never stranded. A drain is a pure queue
@@ -51,18 +51,18 @@ export default class Daemon {
51
51
  // cancel self-aborts against the already-aborted signal (no race). Outlives
52
52
  // any single (ephemeral) drain; replaced with a fresh controller once
53
53
  // aborted so a later loop.run isn't born cancelled.
54
- #runAborts = new Map();
55
- // grammar 0.74.20 EXEC `<T,P>` — per-run hibernation poll-wake timer. When a loop parks at
54
+ #workerAborts = new Map();
55
+ // grammar 0.74.20 EXEC `<T,P>` — per-worker hibernation poll-wake timer. When a loop parks at
56
56
  // a park with a polled stream, a timer fires every P seconds to resume it (§exec-poll). One
57
- // per run (the tightest cadence); cleared/replaced on each park and on cancel.
57
+ // per worker (the tightest cadence); cleared/replaced on each park and on cancel.
58
58
  #parkTimers = new Map();
59
59
  #pollTimers = new Map();
60
- // Per-run drain-transition lock — see #withDrainLock (R4 / §run-lifecycle-single-drain).
60
+ // Per-run drain-transition lock — see #withDrainLock (R4 / §worker-lifecycle-single-drain).
61
61
  #drainLocks = new Map();
62
- // §run-lifecycle-child-wake — runs OWED a wake: a child/stream conclusion fired while the run was
63
- // mid-turn (not yet slept), so #wakeParkedRun could not resume it. A worker-run conclusion is a
62
+ // §worker-lifecycle-child-wake — runs OWED a wake: a child/stream conclusion fired while the worker was
63
+ // mid-turn (not yet slept), so #wakeParkedWorker could not resume it. A worker-run conclusion is a
64
64
  // BOUNDED, lossless wake (a worker always concludes), so a hibernation awaiting one MUST return —
65
- // never deadlock. The drain honors the owed wake at the run's next park, closing the conclude-
65
+ // never deadlock. The drain honors the owed wake at the worker's next park, closing the conclude-
66
66
  // before-park race. (Only a live exec stream, unbounded absent a timeout, may hold a park open.)
67
67
  #owedWakes = new Set();
68
68
  constructor({ db, schemes, mimetypes, provider, nodeModulesPath, }) {
@@ -75,8 +75,8 @@ export default class Daemon {
75
75
  // in-repo tests. discover() takes a cwd and joins node_modules, so derive the parent.
76
76
  this.#nodeModulesPath = nodeModulesPath ?? resolve(process.cwd(), "node_modules");
77
77
  this.#discoveryCwd = dirname(this.#nodeModulesPath);
78
- // Mimetypes owns discovery + detection; we inject the tokenize fn (from the provider's
79
- // countTokens) and default to text/markdown.
78
+ // Mimetypes owns discovery + detection; default mimetype text/markdown. (Token counting
79
+ // is NOT wired here — the engine's ruler below is §tokenomics-agnostic-ruler.)
80
80
  this.#mimetypes = mimetypes ?? new Mimetypes({
81
81
  defaultMimetype: "text/markdown",
82
82
  discoverOptions: { cwd: this.#discoveryCwd },
@@ -84,34 +84,34 @@ export default class Daemon {
84
84
  this.#engine = new Engine({
85
85
  db, schemes: this.#schemes, mimetypes: this.#mimetypes,
86
86
  // §tokenomics-agnostic-ruler — the ONE model-facing token ruler (chars/2), NOT the
87
- // boot provider: token accounting is session-wide across many concurrent models, so
87
+ // boot provider: token accounting is workspace-wide across many concurrent models, so
88
88
  // the write-time + catalog counts must be model-independent. Exact per-model counting
89
89
  // lives only at the packet-materialization fit-gate.
90
90
  tokenize: rulerCount,
91
- streamEventNotify: (sessionId, event) => this.notifyStreamEvent(sessionId, event),
92
- wakeRunNotify: (payload) => { void this.#handleWakeRun(payload); },
93
- // run:// loop-start primitive — spawn/fork/irc deliver through
91
+ streamEventNotify: (workspaceId, event) => this.notifyStreamEvent(workspaceId, event),
92
+ wakeWorkerNotify: (payload) => { void this.#handleWakeWorker(payload); },
93
+ // worker:// loop-start primitive — spawn/fork/irc deliver through
94
94
  // Daemon.inject (active sister → fold; idle → enqueue + drain). The
95
- // daemon owns provider + the law-file system prompt; the run scheme
95
+ // daemon owns provider + the law-file system prompt; the worker scheme
96
96
  // handler carries neither. Fire-and-forget: the returned drain runs
97
- // independently (the sister is its own run). §machine-processes
98
- injectRun: async ({ sessionId, runId, prompt, flags }) => {
97
+ // independently (the sister is its own worker). §machine-processes
98
+ injectWorker: async ({ workspaceId, workerId, prompt, flags }) => {
99
99
  if (this.#provider === null)
100
- throw new Error("injectRun: no provider configured");
100
+ throw new Error("injectWorker: no provider configured");
101
101
  const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
102
- const { action, loopId } = await this.inject({ sessionId, runId, prompt, provider: this.#provider, systemPrompt, ...(flags === undefined ? {} : { flags }) });
102
+ const { action, loopId } = await this.inject({ workspaceId, workerId, prompt, provider: this.#provider, systemPrompt, ...(flags === undefined ? {} : { flags }) });
103
103
  return { action, loopId };
104
104
  },
105
- // run:// KILL (terminate) — abort any run's in-flight work by id. One
105
+ // worker:// KILL (terminate) — abort any worker's in-flight work by id. One
106
106
  // abort, one scope (Daemon.cancelDrain): the active loop closes 499,
107
107
  // background streams tear down. Whoever holds the address may end it.
108
- cancelRun: (runId) => this.cancelDrain(runId, "killed via run:// KILL"),
109
- telemetryEventNotify: (sessionId, payload) => this.notifyTelemetryEvent(sessionId, payload),
108
+ cancelWorker: (workerId) => this.cancelDrain(workerId, "killed via worker:// KILL"),
109
+ telemetryEventNotify: (workspaceId, payload) => this.notifyTelemetryEvent(workspaceId, payload),
110
110
  });
111
111
  // Wire proposal-pending events to the loop/proposal WS notification.
112
- // Sessionid scopes the broadcast to clients on the same session.
112
+ // Sessionid scopes the broadcast to clients on the same workspace.
113
113
  this.#engine.onProposalPending((event) => {
114
- this.#broadcast({ sessionId: event.sessionId }, "loop/proposal", {
114
+ this.#broadcast({ workspaceId: event.workspaceId }, "loop/proposal", {
115
115
  logEntryId: event.logEntryId,
116
116
  loopId: event.loopId,
117
117
  turnId: event.turnId,
@@ -135,25 +135,25 @@ export default class Daemon {
135
135
  NoProposals.attachNoProposals(this.#engine, this.#db);
136
136
  }
137
137
  // The client-interface seam (#355). A transport module subscribes to the daemon's in-process
138
- // event source: it receives every session-scoped engine event as `(sessionId, method, params)`
138
+ // event source: it receives every workspace-scoped engine event as `(workspaceId, method, params)`
139
139
  // and fans out to its OWN clients — core emits, it never fans out for the module. Returns an
140
- // unsubscribe. `sessionId` is the event's session, or null for a global event (e.g. session/created).
140
+ // unsubscribe. `workspaceId` is the event's workspace, or null for a global event (e.g. workspace/created).
141
141
  // The engine and its events are core; the fan-out belongs to the module.
142
142
  subscribeToEvents(handler) {
143
143
  this.#eventSubscribers.add(handler);
144
144
  return () => { this.#eventSubscribers.delete(handler); };
145
145
  }
146
146
  // The client-interface seam (#355) — proposal HITL. A transport module reads the stopped-world
147
- // proposals for a session (rendering each as a TOOL_CALL) and feeds back the human's decision. The
147
+ // proposals for a workspace (rendering each as a TOOL_CALL) and feeds back the human's decision. The
148
148
  // gate, validation, and applyResolution stay core (Engine.resolveProposal); the seam is the read +
149
149
  // the resolve, never the mechanism. `resolveProposal` throws for an unknown/already-resolved id.
150
- async pendingProposals(sessionId) {
151
- return this.#db.proposal_list_pending.all({ session_id: sessionId });
150
+ async pendingProposals(workspaceId) {
151
+ return this.#db.proposal_list_pending.all({ workspace_id: workspaceId });
152
152
  }
153
153
  resolveProposal(logEntryId, resolution) {
154
154
  this.#engine.resolveProposal(logEntryId, resolution);
155
155
  }
156
- // The client-interface seam (#355) — drive/steer a loop. The module supplies only session/run/prompt;
156
+ // The client-interface seam (#355) — drive/steer a loop. The module supplies only workspace/run/prompt;
157
157
  // the provider and the law-file system prompt are core's and stay inside. Returns immediately — the
158
158
  // loop runs async and its outcome arrives on the event source (loop/terminated). `cancelDrain` (public)
159
159
  // is the cancel hook. Both funnel through the unified `inject`, which owns the drain lifecycle.
@@ -168,16 +168,16 @@ export default class Daemon {
168
168
  throw new Error("runLoop: no provider configured");
169
169
  const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
170
170
  // §machine-processes — the model NEVER runs in a client-origin run (its packets would carry
171
- // client op.* rows). The module resolves the model run via ensureModelRun and passes it (or a
172
- // fork); a client run here is a caller error, refused loudly rather than silently rehomed.
173
- const target = await this.#db.envelope_get_run_by_id.get({ id: args.runId });
171
+ // client op.* rows). The module resolves the model worker via ensureModelWorker and passes it (or a
172
+ // fork); a client worker here is a caller error, refused loudly rather than silently rehomed.
173
+ const target = await this.#db.envelope_get_worker_by_id.get({ id: args.workerId });
174
174
  if (target === undefined)
175
- throw new Error(`runLoop: run ${args.runId} not found`);
175
+ throw new Error(`runLoop: run ${args.workerId} not found`);
176
176
  if (target.origin === "client")
177
- throw new Error(`runLoop: run ${args.runId} is a client run — loops run in model runs (§machine-processes); resolve one with ensureModelRun(sessionId)`);
177
+ throw new Error(`runLoop: run ${args.workerId} is a client worker — loops run in model workers (§machine-processes); resolve one with ensureModelWorker(workspaceId)`);
178
178
  // Pre-loop docs (both sets: operator/client mdDocs + the teaching docs the turn-1
179
179
  // FIND(plurnk://docs/**) foist discovers) — ONE truth shared with the legacy loop.run route.
180
- await LoopDocs.materialize(this.#engine, this.#db, args.sessionId);
180
+ await LoopDocs.materialize(this.#engine, this.#db, args.workspaceId);
181
181
  // §operator-config-max-turns-ceiling — the operator ceiling clamps a per-call maxTurns; a
182
182
  // seam caller must not bypass operator policy (inject only DEFAULTS from env, never clamps).
183
183
  const ceiling = Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "-1");
@@ -194,10 +194,10 @@ export default class Daemon {
194
194
  const spec = resolveLoopAlias(alias, model, parseAliasesFromEnv());
195
195
  return spec === null ? this.#provider : ProviderInstantiate.instantiateProvider(spec);
196
196
  }
197
- // §machine-processes — the session's model run (created on first use), distinct from the client
197
+ // §machine-processes — the workspace's model worker (created on first use), distinct from the client
198
198
  // run so the model's packets never carry client op.* rows. The module binds its threads to this.
199
- ensureModelRun(sessionId) {
200
- return Envelope.ensureModelRun(this.#db, sessionId);
199
+ ensureModelWorker(workspaceId) {
200
+ return Envelope.ensureModelWorker(this.#db, workspaceId);
201
201
  }
202
202
  // The op-dispatch hook (#355) — execute one parsed op on behalf of a client: journaled as a
203
203
  // client-origin turn (the log is core's, a client op is a first-class citizen), dispatched through
@@ -205,17 +205,17 @@ export default class Daemon {
205
205
  // family (read/edit/copy/find/fold/look/move/open/send/exec); the module parses at its edge with the
206
206
  // grammar package and hands over the statement, then fans the emitted entry out to its own clients.
207
207
  async dispatchAsClient(args) {
208
- const { sessionId, runId, statement } = args;
209
- const clientLoopId = await Envelope.ensureClientLoop(this.#db, runId);
208
+ const { workspaceId, workerId, statement } = args;
209
+ const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
210
210
  const turnId = await ClientTurn.insertClientTurn(this.#db, clientLoopId);
211
211
  const entryIds = [];
212
212
  const result = await this.#engine.dispatch({
213
- statement, sessionId, runId, loopId: clientLoopId, turnId, sequence: 1,
213
+ statement, workspaceId, workerId, loopId: clientLoopId, turnId, sequence: 1,
214
214
  origin: "client", onDispatch: (logEntryId) => { entryIds.push(logEntryId); },
215
215
  });
216
216
  for (const logEntryId of entryIds) {
217
217
  const entry = await LogEntry.fetchLogEntry(this.#db, logEntryId);
218
- this.#broadcast({ sessionId }, "log/entry", { entry });
218
+ this.#broadcast({ workspaceId }, "log/entry", { entry });
219
219
  }
220
220
  return result;
221
221
  }
@@ -225,23 +225,23 @@ export default class Daemon {
225
225
  // dispatchAsClient). Rides the client loop so log:/// coordinates resolve run-relative;
226
226
  // invisible to the model. Engine.look enforces READ-only.
227
227
  async look(args) {
228
- const { sessionId, runId, statement } = args;
229
- const clientLoopId = await Envelope.ensureClientLoop(this.#db, runId);
230
- return await this.#engine.look({ statement, sessionId, runId, loopId: clientLoopId });
228
+ const { workspaceId, workerId, statement } = args;
229
+ const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
230
+ return await this.#engine.look({ statement, workspaceId, workerId, loopId: clientLoopId });
231
231
  }
232
- // The log-read hook (#355) — a session's journal, the module's primary render input. The run is
233
- // ownership-verified against the session (a session reads only its own runs — the model run included,
232
+ // The log-read hook (#355) — a workspace's journal, the module's primary render input. The worker is
233
+ // ownership-verified against the workspace (a workspace reads only its own runs — the model worker included,
234
234
  // #214); entries filter by loop/turn/since-id or the full L/T/S display coordinate. Core owns the
235
235
  // journal + the invariant; the module shapes the entries into AG-UI messages at its edge.
236
236
  async readLog(args) {
237
- const { sessionId, runId } = args;
238
- const target = await this.#db.envelope_get_run_by_id.get({ id: runId });
237
+ const { workspaceId, workerId } = args;
238
+ const target = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
239
239
  if (target === undefined)
240
- throw new Error(`run ${runId} not found`);
241
- if (target.session_id !== sessionId)
242
- throw new Error(`run ${runId} is not in this session (${sessionId})`);
240
+ throw new Error(`run ${workerId} not found`);
241
+ if (target.workspace_id !== workspaceId)
242
+ throw new Error(`run ${workerId} is not in this workspace (${workspaceId})`);
243
243
  const rows = await this.#db.log_read_recent_ids.all({
244
- run_id: runId,
244
+ worker_id: workerId,
245
245
  loop_id: args.loopId ?? null, turn_id: args.turnId ?? null, since_id: args.sinceId ?? null,
246
246
  loop_seq: args.loopSeq ?? null, turn_seq: args.turnSeq ?? null, sequence: args.sequence ?? null,
247
247
  limit: Math.min(args.limit ?? 100, 1000),
@@ -260,78 +260,78 @@ export default class Daemon {
260
260
  const isActive = active !== null && active.alias === a.alias;
261
261
  return {
262
262
  alias: a.alias, provider: a.provider, model: a.model, active: isActive,
263
- // contextSize = the EFFECTIVE prompt budget (window minus reserves, #345) — the same
263
+ // promptBudget = the EFFECTIVE prompt budget (window minus reserves, #345; named honestly #481) — the same
264
264
  // denominator loop-usage reports; known for the active alias, null elsewhere.
265
- contextSize: isActive && this.#provider !== null ? this.#engine.promptBudgetFor(this.#provider) : null,
265
+ promptBudget: isActive && this.#provider !== null ? this.#engine.promptBudgetFor(this.#provider) : null,
266
266
  };
267
267
  }),
268
268
  };
269
269
  }
270
- listSessions() { return Envelope.listSessions(this.#db); }
271
- listRuns(sessionId) { return Envelope.listRunsForSession(this.#db, sessionId); }
272
- listPrompts(sessionId, limit = 100) { return Envelope.listPromptsForSession(this.#db, sessionId, limit); }
273
- listMembers(sessionId) { return GitMembership.resolveMembershipEffects(this.#db, sessionId, undefined); }
274
- listConstraints(sessionId) {
275
- return this.#db.crud_list_session_constraints.all({ session_id: sessionId });
270
+ listWorkspaces() { return Envelope.listWorkspaces(this.#db); }
271
+ listWorkers(workspaceId) { return Envelope.listWorkersForWorkspace(this.#db, workspaceId); }
272
+ listPrompts(workspaceId, limit = 100) { return Envelope.listPromptsForWorkspace(this.#db, workspaceId, limit); }
273
+ listMembers(workspaceId) { return GitMembership.resolveMembershipEffects(this.#db, workspaceId, undefined); }
274
+ listConstraints(workspaceId) {
275
+ return this.#db.crud_list_workspace_constraints.all({ workspace_id: workspaceId });
276
276
  }
277
- // Session lifecycle (#355): the module's session-management surface. Inputs arrive already validated
277
+ // Workspace lifecycle (#355): the module's workspace-management surface. Inputs arrive already validated
278
278
  // at the module's edge ("I am the wall" — settings as the stored JSON string, constraints as a typed
279
279
  // array, roots absolute); core owns the envelope, its reserved-name + name-uniqueness invariants,
280
- // membership resolution, warmSessionDerivations, and the session/created emit. No connection state
281
- // (which client is on which session) lives here — that's the module's.
282
- async createSession(args) {
280
+ // membership resolution, warmWorkspaceDerivations, and the workspace/created emit. No connection state
281
+ // (which client is on which workspace) lives here — that's the module's.
282
+ async createWorkspace(args) {
283
283
  // The SEAM fail-hards on malformed client input (#364 — validation flushed out of the
284
284
  // retired WS handlers so every module inherits it): settings bag (#231/#232/#249/#328),
285
285
  // constraints (#200), absolute projectRoot.
286
- const projectRoot = ClientInput.assertProjectRoot("session.create", args.projectRoot);
286
+ const projectRoot = ClientInput.assertProjectRoot("workspace.create", args.projectRoot);
287
287
  const settings = ClientInput.parseSettings(args.settings);
288
288
  const constraints = ClientInput.parseConstraints(args.constraints);
289
289
  const envelope = await Envelope.createClientEnvelope(this.#db, { name: args.name, projectRoot, settings });
290
290
  for (const { effect, glob } of constraints) {
291
- await this.#db.crud_insert_session_constraint.run({ session_id: envelope.sessionId, effect, glob });
291
+ await this.#db.crud_insert_workspace_constraint.run({ workspace_id: envelope.workspaceId, effect, glob });
292
292
  }
293
293
  if (constraints.length > 0)
294
- await GitMembership.resolveGitMembership(this.#db, envelope.sessionId, undefined);
295
- void this.#engine.warmSessionDerivations(envelope.sessionId).catch(() => { });
296
- this.#broadcast("all", "session/created", { id: envelope.sessionId, name: envelope.sessionName, projectRoot: envelope.projectRoot });
294
+ await GitMembership.resolveGitMembership(this.#db, envelope.workspaceId, undefined);
295
+ void this.#engine.warmWorkspaceDerivations(envelope.workspaceId).catch(() => { });
296
+ this.#broadcast("all", "workspace/created", { id: envelope.workspaceId, name: envelope.workspaceName, projectRoot: envelope.projectRoot });
297
297
  return envelope;
298
298
  }
299
- async attachSession(args) {
300
- // attachToSession owns the reserved-name + run-ownership invariants; the seam just delegates + warms.
301
- const envelope = await Envelope.attachToSession(this.#db, args.sessionId, { runId: args.runId, runName: args.runName });
302
- void this.#engine.warmSessionDerivations(envelope.sessionId).catch(() => { });
299
+ async attachWorkspace(args) {
300
+ // attachToWorkspace owns the reserved-name + run-ownership invariants; the seam just delegates + warms.
301
+ const envelope = await Envelope.attachToWorkspace(this.#db, args.workspaceId, { workerId: args.workerId, workerName: args.workerName });
302
+ void this.#engine.warmWorkspaceDerivations(envelope.workspaceId).catch(() => { });
303
303
  return envelope;
304
304
  }
305
- async renameSession(sessionId, name) {
305
+ async renameWorkspace(workspaceId, name) {
306
306
  if (typeof name !== "string" || name.length === 0)
307
- throw new Error("session.rename: name must be a non-empty string"); // seam fail-hard (#364)
308
- const taken = await this.#db.envelope_get_session_by_name.get({ name });
309
- if (taken !== undefined && taken.id !== sessionId)
310
- throw new Error(`a session named "${name}" already exists — pick another`);
311
- return { id: sessionId, name: await Envelope.updateSessionName(this.#db, sessionId, name) };
307
+ throw new Error("workspace.rename: name must be a non-empty string"); // seam fail-hard (#364)
308
+ const taken = await this.#db.envelope_get_workspace_by_name.get({ name });
309
+ if (taken !== undefined && taken.id !== workspaceId)
310
+ throw new Error(`a workspace named "${name}" already exists — pick another`);
311
+ return { id: workspaceId, name: await Envelope.updateWorkspaceName(this.#db, workspaceId, name) };
312
312
  }
313
- async constrain(sessionId, effect, glob) {
314
- ClientInput.assertConstraint("session.constrain", effect, glob);
315
- // Headless is FOREVER (owner ruling, 2026-07-11, matching the client SPEC): a session is
313
+ async constrain(workspaceId, effect, glob) {
314
+ ClientInput.assertConstraint("workspace.constrain", effect, glob);
315
+ // Headless is FOREVER (owner ruling, 2026-07-11, matching the client SPEC): a workspace is
316
316
  // born with its workspace pointer or never has one — so a 'repo' constraint on a headless
317
- // session can never resolve. Refuse legibly instead of recording a forever-pending lie.
317
+ // workspace can never resolve. Refuse legibly instead of recording a forever-pending lie.
318
318
  if (effect === "repo") {
319
- const s = await this.#db.envelope_get_session.get({ id: sessionId });
319
+ const s = await this.#db.envelope_get_workspace.get({ id: workspaceId });
320
320
  if (s?.project_root == null)
321
- throw new Error("session.constrain: this session is headless — and headless is forever (a workspace pointer is set at session.create or never). A 'repo' overlay needs a session created with projectRoot.");
321
+ throw new Error("workspace.constrain: this workspace is headless — and headless is forever (a workspace pointer is set at workspace.create or never). A 'repo' overlay needs a workspace created with projectRoot.");
322
322
  }
323
- await this.#db.crud_insert_session_constraint.run({ session_id: sessionId, effect, glob });
324
- await GitMembership.resolveGitMembership(this.#db, sessionId, undefined);
323
+ await this.#db.crud_insert_workspace_constraint.run({ workspace_id: workspaceId, effect, glob });
324
+ await GitMembership.resolveGitMembership(this.#db, workspaceId, undefined);
325
325
  // Members may have just landed — warm their derivations NOW (fire-and-forget, off the hot
326
- // path), exactly like createSession does (dogfood catch: '/repo' embeddings waited for a
326
+ // path), exactly like createWorkspace does (dogfood catch: '/repo' embeddings waited for a
327
327
  // later turn's pump).
328
- void this.#engine.warmSessionDerivations(sessionId).catch(() => { });
328
+ void this.#engine.warmWorkspaceDerivations(workspaceId).catch(() => { });
329
329
  return { effect, glob };
330
330
  }
331
- async unconstrain(sessionId, effect, glob) {
332
- ClientInput.assertConstraint("session.unconstrain", effect, glob);
333
- await this.#db.crud_delete_session_constraint.run({ session_id: sessionId, effect, glob });
334
- await GitMembership.resolveGitMembership(this.#db, sessionId, undefined);
331
+ async unconstrain(workspaceId, effect, glob) {
332
+ ClientInput.assertConstraint("workspace.unconstrain", effect, glob);
333
+ await this.#db.crud_delete_workspace_constraint.run({ workspace_id: workspaceId, effect, glob });
334
+ await GitMembership.resolveGitMembership(this.#db, workspaceId, undefined);
335
335
  return { effect, glob };
336
336
  }
337
337
  // The entry-shape hook (#355) — one entry's channels + tags + metadata at a path. With channel+offset,
@@ -345,7 +345,7 @@ export default class Daemon {
345
345
  throw new Error("readEntry: offset requires channel (which channel to slice)");
346
346
  const scheme = m[1];
347
347
  const pathname = m[2].split("#")[0];
348
- const row = await this.#db.entry_read_lookup.get({ session_id: args.sessionId, scheme, pathname });
348
+ const row = await this.#db.entry_read_lookup.get({ workspace_id: args.workspaceId, scheme, pathname });
349
349
  if (row === undefined)
350
350
  return { status: 404, entry: null };
351
351
  let channelRows;
@@ -360,52 +360,52 @@ export default class Daemon {
360
360
  for (const c of channelRows)
361
361
  channels[c.name] = { content: c.content, contentLength: c.contentLength, mimetype: c.mimetype, tokens: c.tokens, state: c.state };
362
362
  const tagRows = await this.#db.crud_read_tags.all({ entry_id: row.id });
363
- return { status: 200, entry: { id: row.id, scope: row.scope, sessionId: row.session_id, scheme: row.scheme, pathname: row.pathname, channels, tags: tagRows.map((t) => t.tag) } };
363
+ return { status: 200, entry: { id: row.id, scope: row.scope, workspaceId: row.workspace_id, scheme: row.scheme, pathname: row.pathname, channels, tags: tagRows.map((t) => t.tag) } };
364
364
  }
365
- // The fork hook (#355) — branch a run's log into a new run in the same session (#228), sharing the
366
- // session's world (entries + overlay), copying nothing of it. The module resolves the default (the
367
- // session's model run) from its own connection state and passes the concrete runId; the seam owns the
368
- // #366 — a fresh conversation run: AG-UI threads map to RUNS (§machine-processes — the session
369
- // is the workspace, the run is the conversation). ensureModelRun is the stable DEFAULT door,
370
- // forkRun the branching door (copies history); this is the fresh door — a named, empty-log,
365
+ // The fork hook (#355) — branch a worker's log into a new worker in the same workspace (#228), sharing the
366
+ // workspace's world (entries + overlay), copying nothing of it. The module resolves the default (the
367
+ // workspace's model worker) from its own connection state and passes the concrete workerId; the seam owns the
368
+ // #366 — a fresh conversation worker: AG-UI threads map to RUNS (§machine-processes — the workspace
369
+ // is the workspace, the worker is the conversation). ensureModelWorker is the stable DEFAULT door,
370
+ // forkWorker the branching door (copies history); this is the fresh door — a named, empty-log,
371
371
  // model-origin root that runLoop accepts. New chat = new conversation, same workspace.
372
- async createConversationRun(args) {
373
- const { sessionId, name } = args;
372
+ async createConversationWorker(args) {
373
+ const { workspaceId, name } = args;
374
374
  if (name !== undefined && (typeof name !== "string" || name.length === 0))
375
375
  throw new Error("run.create: name must be a non-empty string");
376
- const session = await this.#db.envelope_get_session.get({ id: sessionId });
377
- if (session === undefined)
378
- throw new Error(`run.create: session ${sessionId} not found`);
376
+ const workspace = await this.#db.envelope_get_workspace.get({ id: workspaceId });
377
+ if (workspace === undefined)
378
+ throw new Error(`run.create: workspace ${workspaceId} not found`);
379
379
  if (name !== undefined) {
380
380
  if (Envelope.RESERVED_RUN_NAMES.has(name.toLowerCase()))
381
381
  throw new Error(`run.create: name "${name}" is reserved for a non-client actor`);
382
- const taken = await this.#db.envelope_get_run_by_name.get({ session_id: sessionId, name });
382
+ const taken = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name });
383
383
  if (taken !== undefined)
384
- throw new Error(`run.create: a run named "${name}" already exists — run names are immutable, pick another`);
384
+ throw new Error(`run.create: a worker named "${name}" already exists — worker names are immutable, pick another`);
385
385
  }
386
- const run = await Envelope.createModelRun(this.#db, sessionId, name);
387
- return { runId: run.id, runName: run.name };
386
+ const run = await Envelope.createModelWorker(this.#db, workspaceId, name);
387
+ return { workerId: run.id, workerName: run.name };
388
388
  }
389
389
  // ownership check and the run-name namespace + uniqueness invariants (names are immutable — no rename).
390
- async forkRun(args) {
390
+ async forkWorker(args) {
391
391
  if (args.name !== undefined && (typeof args.name !== "string" || args.name.length === 0))
392
392
  throw new Error("run.fork: name must be a non-empty string"); // seam fail-hard (#364)
393
- const { sessionId, runId, name } = args;
394
- const owner = await this.#db.envelope_get_run_by_id.get({ id: runId });
393
+ const { workspaceId, workerId, name } = args;
394
+ const owner = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
395
395
  if (owner === undefined)
396
- throw new Error(`forkRun: run ${runId} not found`);
397
- if (owner.session_id !== sessionId)
398
- throw new Error(`forkRun: run ${runId} is not in session ${sessionId}`);
396
+ throw new Error(`forkWorker: run ${workerId} not found`);
397
+ if (owner.workspace_id !== workspaceId)
398
+ throw new Error(`forkWorker: run ${workerId} is not in workspace ${workspaceId}`);
399
399
  if (name !== undefined) {
400
400
  if (Envelope.RESERVED_RUN_NAMES.has(name.toLowerCase()))
401
- throw new Error(`forkRun: name "${name}" is reserved for a non-client actor`);
402
- const taken = await this.#db.envelope_get_run_by_name.get({ session_id: sessionId, name });
401
+ throw new Error(`forkWorker: name "${name}" is reserved for a non-client actor`);
402
+ const taken = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name });
403
403
  if (taken !== undefined)
404
- throw new Error(`forkRun: a run named "${name}" already exists — run names are immutable, pick another`);
404
+ throw new Error(`forkWorker: a worker named "${name}" already exists — worker names are immutable, pick another`);
405
405
  }
406
- const branchRunId = await Fork.fork(this.#db, runId, name);
407
- const branch = await this.#db.envelope_get_run_by_id.get({ id: branchRunId });
408
- return { runId: branchRunId, runName: branch?.name ?? null, parentRunId: runId };
406
+ const branchWorkerId = await Fork.fork(this.#db, workerId, name);
407
+ const branch = await this.#db.envelope_get_worker_by_id.get({ id: branchWorkerId });
408
+ return { workerId: branchWorkerId, workerName: branch?.name ?? null, parentWorkerId: workerId };
409
409
  }
410
410
  // The module-load hook (#355 / #289) — register a runtime into the live registry, driver-agnostic:
411
411
  // the kernel knows nothing about MCP or any specific driver. The struct is the booth window agreed
@@ -468,7 +468,7 @@ export default class Daemon {
468
468
  // to completion, (3) drain streaming schemes' background work
469
469
  // (exec spawn cleanup, channel writes). Only THEN close the DB
470
470
  // upstream — drain queries hit the DB right up until they exit.
471
- // Abort every run's cancellation scope — stops in-flight loops AND the
471
+ // Abort every worker's cancellation scope — stops in-flight loops AND the
472
472
  // streams (background execs) linked to them, so idle() doesn't block on
473
473
  // a long-running command. Covers runs whose drain already exited but
474
474
  // whose exec is still in flight.
@@ -476,13 +476,19 @@ export default class Daemon {
476
476
  // that will never arrive once clients are gone — allSettled(drains) below would deadlock
477
477
  // the stop forever (a daemon with a pending HITL proposal could not shut down).
478
478
  this.#engine.cancelAllProposals("daemon_stopping");
479
- for (const scope of this.#runAborts.values()) {
479
+ for (const scope of this.#workerAborts.values()) {
480
480
  if (!scope.signal.aborted)
481
481
  scope.abort("daemon_stopping");
482
482
  }
483
483
  for (const t of this.#pollTimers.values())
484
484
  clearTimeout(t); // drop pending hibernation poll-wakes
485
485
  this.#pollTimers.clear();
486
+ // …and the park-DEADLINE timers (#432): a bounded park's timer fires #wakeParkedWorker after
487
+ // stop/db-close if left pending — an unhandled rejection (SqlRite closed) that abnormally
488
+ // exits the worker under load. Symmetric with the poll-wakes above; both must be reaped.
489
+ for (const t of this.#parkTimers.values())
490
+ clearTimeout(t);
491
+ this.#parkTimers.clear();
486
492
  const drainPromises = [...this.#activeDrains.values()].map((d) => d.promise);
487
493
  await Promise.allSettled(drainPromises);
488
494
  await this.#drainStreamingSchemes();
@@ -496,25 +502,25 @@ export default class Daemon {
496
502
  await exec.idle();
497
503
  }
498
504
  /**
499
- * Emit a stream/event notification scoped to the session containing the
505
+ * Emit a stream/event notification scoped to the workspace containing the
500
506
  * entry. ChannelWrite helpers (src/core/ChannelWrite.ts) invoke this when
501
507
  * they update channel content or state. SPEC §notifications.
502
508
  */
503
- notifyStreamEvent(sessionId, event) {
504
- this.#broadcast({ sessionId }, "stream/event", event);
509
+ notifyStreamEvent(workspaceId, event) {
510
+ this.#broadcast({ workspaceId }, "stream/event", event);
505
511
  }
506
512
  /**
507
- * Emit a telemetry/event notification scoped to the session containing
513
+ * Emit a telemetry/event notification scoped to the workspace containing
508
514
  * the loop. TelemetryChannel.push invokes this for every TelemetryEvent
509
515
  * (parse_error, strike, cycle, sudden_death, no_ops, max_commands_exceeded,
510
516
  * action_failure) the moment it lands in the loop's telemetry buffer.
511
517
  * SPEC §telemetry.
512
518
  */
513
- notifyTelemetryEvent(sessionId, payload) {
514
- this.#broadcast({ sessionId }, "telemetry/event", payload);
519
+ notifyTelemetryEvent(workspaceId, payload) {
520
+ this.#broadcast({ workspaceId }, "telemetry/event", payload);
515
521
  }
516
522
  /**
517
- * Inject a prompt into a run. Two paths:
523
+ * Inject a prompt into a worker. Two paths:
518
524
  * - Active drain: writes a plurnk://prompt/<run>/<loop>/<next-turn> entry
519
525
  * via Engine.inject. Current loop sees the new prompt at its next
520
526
  * turn. Returns immediately with {action: "injected_next_turn"}.
@@ -530,12 +536,12 @@ export default class Daemon {
530
536
  // to: an inject carrying flags that DIFFER from the target loop's effective flags is refused
531
537
  // legibly (cancel the loop or omit the flags), never a silent posture discard. Identical or
532
538
  // absent flags fold clean.
533
- async #assertFoldPosture(runId, flags, loopId) {
539
+ async #assertFoldPosture(workerId, flags, loopId) {
534
540
  if (flags === undefined || Object.keys(flags).length === 0)
535
541
  return;
536
542
  const row = loopId !== undefined
537
543
  ? await this.#db.engine_get_loop_flags.get({ loop_id: loopId })
538
- : await this.#db.drain_active_loop_flags.get({ run_id: runId });
544
+ : await this.#db.drain_active_loop_flags.get({ worker_id: workerId });
539
545
  const effective = { ...DEFAULT_LOOP_FLAGS, ...JSON.parse(row?.flags ?? "{}") };
540
546
  const conflicts = Object.entries(flags).filter(([k, v]) => v !== undefined && effective[k] !== v).map(([k, v]) => `${k}: ${JSON.stringify(effective[k])} → ${JSON.stringify(v)}`);
541
547
  if (conflicts.length > 0) {
@@ -543,41 +549,41 @@ export default class Daemon {
543
549
  }
544
550
  }
545
551
  async inject(args) {
546
- const { sessionId, runId, prompt } = args;
552
+ const { workspaceId, workerId, prompt } = args;
547
553
  // Active loop (status=102)? Fold the wake/prompt into its next turn.
548
554
  // engine.inject returns null when no loop is currently executing, so
549
555
  // we enqueue a fresh loop below and ensure a drain claims it.
550
- if (this.#activeDrains.has(runId)) {
551
- await this.#assertFoldPosture(runId, args.flags); // #368 — a fold never silently discards intent
552
- const result = await this.#engine.inject(runId, prompt);
556
+ if (this.#activeDrains.has(workerId)) {
557
+ await this.#assertFoldPosture(workerId, args.flags); // #368 — a fold never silently discards intent
558
+ const result = await this.#engine.inject(workerId, prompt);
553
559
  if (result !== null) {
554
560
  return { action: "injected_next_turn", loopId: result.loopId, turnSeq: result.turnSeq };
555
561
  }
556
562
  }
557
- // #55 — a run PARKED at 202 RESUMES that slept loop in place: the voice door (irc / loop.inject)
563
+ // #55 — a worker PARKED at 202 RESUMES that slept loop in place: the voice door (irc / loop.inject)
558
564
  // is a wake edge like a stream/child conclusion, not a fresh loop that orphans the parked one
559
- // (which would leave the run non-quiescent forever). engine.inject writes the message as the
565
+ // (which would leave the worker non-quiescent forever). engine.inject writes the message as the
560
566
  // slept loop's next-turn prompt (the directed message — distinct from the env door, which
561
- // resumes promptless); then re-queue + drain it. §run-lifecycle-wake-liveness.
562
- if (!this.#activeDrains.has(runId)) {
563
- const slept = await this.#db.drain_find_slept_loop.get({ run_id: runId });
567
+ // resumes promptless); then re-queue + drain it. §worker-lifecycle-wake-liveness.
568
+ if (!this.#activeDrains.has(workerId)) {
569
+ const slept = await this.#db.drain_find_slept_loop.get({ worker_id: workerId });
564
570
  if (slept !== undefined) {
565
- await this.#assertFoldPosture(runId, args.flags, slept.id); // #368 — the resume path drops nothing silently either
566
- const injected = await this.#engine.inject(runId, prompt);
571
+ await this.#assertFoldPosture(workerId, args.flags, slept.id); // #368 — the resume path drops nothing silently either
572
+ const injected = await this.#engine.inject(workerId, prompt);
567
573
  await this.#db.drain_resume_slept_loop.run({ loop_id: slept.id });
568
574
  const started = await this.#ensureDrain({
569
- sessionId, runId, provider: args.provider, systemPrompt: args.systemPrompt,
575
+ workspaceId, workerId, provider: args.provider, systemPrompt: args.systemPrompt,
570
576
  maxTurns: args.maxTurns ?? Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "50"),
571
577
  });
572
578
  return { action: "injected_next_turn", loopId: slept.id, ...(injected?.turnSeq !== undefined ? { turnSeq: injected.turnSeq } : {}), ...(started ?? {}) };
573
579
  }
574
580
  }
575
581
  // Enqueue a fresh loop. Persist flags on the row.
576
- const seqRow = await this.#db.loop_run_next_sequence.get({ run_id: runId });
582
+ const seqRow = await this.#db.loop_run_next_sequence.get({ worker_id: workerId });
577
583
  if (seqRow === undefined)
578
584
  throw new Error("inject: next-sequence query returned no row");
579
585
  const loopRow = await this.#db.drain_enqueue_loop.get({
580
- run_id: runId, sequence: seqRow.next, prompt,
586
+ worker_id: workerId, sequence: seqRow.next, prompt,
581
587
  });
582
588
  if (loopRow === undefined)
583
589
  throw new Error("inject: loop enqueue returned no row");
@@ -595,14 +601,14 @@ export default class Daemon {
595
601
  });
596
602
  }
597
603
  // Guarantee a drain claims the loop we just enqueued. #ensureDrain runs its
598
- // check-and-start UNDER the per-run drain lock (§run-lifecycle-single-drain),
604
+ // check-and-start UNDER the per-worker drain lock (§worker-lifecycle-single-drain),
599
605
  // serialized against a draining sibling's teardown relinquish so the two can't
600
606
  // both register a drain (R4). A live drain re-claims the loop in its own
601
607
  // iteration or its lock-held exit re-claim, so it's never stranded.
602
608
  // firstLoopPromise is present only when THIS call started the drain — loop.run
603
609
  // keys its fast-path response on that.
604
610
  const started = await this.#ensureDrain({
605
- sessionId, runId, provider: args.provider,
611
+ workspaceId, workerId, provider: args.provider,
606
612
  systemPrompt: args.systemPrompt,
607
613
  maxTurns: args.maxTurns ?? Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "50"),
608
614
  });
@@ -612,7 +618,7 @@ export default class Daemon {
612
618
  * Start a drain for the given run. The drain claims queued loops via
613
619
  * drain_claim_next_loop (atomic 100→102 flip), executes each via
614
620
  * Engine.runLoop, and re-checks. Stream-aware: when the queue is empty
615
- * but the run has active subscriptions, the drain parks on a
621
+ * but the worker has active subscriptions, the drain parks on a
616
622
  * #drainPokes signal — wake-on-completion → inject() wakes it. Drain
617
623
  * exits when queue is empty AND no active subscriptions remain.
618
624
  *
@@ -622,10 +628,10 @@ export default class Daemon {
622
628
  * (resolves only when the whole drain finishes, queue+subs settled).
623
629
  */
624
630
  #startDrain(opts) {
625
- const { sessionId, runId, provider, systemPrompt, maxTurns } = opts;
626
- // The drain runs under the run's cancellation scope (shared with the
631
+ const { workspaceId, workerId, provider, systemPrompt, maxTurns } = opts;
632
+ // The drain runs under the worker's cancellation scope (shared with the
627
633
  // execs its loops spawn), so loop.cancel/shutdown abort it as a unit.
628
- const controller = this.#runSignal(runId);
634
+ const controller = this.#workerSignal(workerId);
629
635
  const handle = {
630
636
  controller, promise: Promise.resolve(),
631
637
  };
@@ -636,7 +642,7 @@ export default class Daemon {
636
642
  rejectFirst = rej;
637
643
  });
638
644
  let firstSettled = false;
639
- const claim = () => this.#db.drain_claim_next_loop.get({ run_id: runId });
645
+ const claim = () => this.#db.drain_claim_next_loop.get({ worker_id: workerId });
640
646
  const drainPromise = (async () => {
641
647
  let loopsDrained = 0;
642
648
  let lastResult = null;
@@ -646,16 +652,16 @@ export default class Daemon {
646
652
  controller.signal.throwIfAborted();
647
653
  let loopRow = await claim();
648
654
  if (loopRow === undefined) {
649
- // Queue empty → teardown UNDER the per-run drain lock (R4 / I1),
655
+ // Queue empty → teardown UNDER the per-worker drain lock (R4 / I1),
650
656
  // serialized against #ensureDrain so a concurrent inject can't
651
657
  // start a 2nd drain in the gap. Re-claim while holding the lock;
652
658
  // relinquish the registry slot only if it's empty too. A loop
653
659
  // that raced in is returned and run — we stay registered, so
654
660
  // there's no transient delete for #ensureDrain to catch.
655
- loopRow = await this.#withDrainLock(runId, async () => {
661
+ loopRow = await this.#withDrainLock(workerId, async () => {
656
662
  const claimed = await claim();
657
- if (claimed === undefined && this.#activeDrains.get(runId) === handle) {
658
- this.#activeDrains.delete(runId);
663
+ if (claimed === undefined && this.#activeDrains.get(workerId) === handle) {
664
+ this.#activeDrains.delete(workerId);
659
665
  }
660
666
  return claimed;
661
667
  });
@@ -666,11 +672,11 @@ export default class Daemon {
666
672
  const onDispatch = (logEntryId) => {
667
673
  void (async () => {
668
674
  const entry = await LogEntry.fetchLogEntry(this.#db, logEntryId);
669
- this.#broadcast({ sessionId }, "log/entry", { entry });
675
+ this.#broadcast({ workspaceId }, "log/entry", { entry });
670
676
  })();
671
677
  };
672
678
  const result = await this.#engine.runLoop({
673
- provider, sessionId, runId, loopId: loopRow.id, maxTurns,
679
+ provider, workspaceId, workerId, loopId: loopRow.id, maxTurns,
674
680
  messages: [
675
681
  { role: "system", content: systemPrompt },
676
682
  { role: "user", content: loopRow.prompt },
@@ -682,9 +688,9 @@ export default class Daemon {
682
688
  if (result.finalStatus === 202) {
683
689
  // The loop SLEPT (parked via [102]<T>/<-1>) — suspended, not terminated. Leave it at 202
684
690
  // (resumable); no loop/terminated, no orphan-reconcile. A stream conclusion
685
- // (#handleWakeRun) re-queues it; and if it holds a polled stream, a poll timer
686
- // wakes it every P to inspect (§exec-poll). §run-lifecycle-wake-liveness.
687
- void this.#schedulePollWake(sessionId, runId, provider, systemPrompt);
691
+ // (#handleWakeWorker) re-queues it; and if it holds a polled stream, a poll timer
692
+ // wakes it every P to inspect (§exec-poll). §worker-lifecycle-wake-liveness.
693
+ void this.#schedulePollWake(workspaceId, workerId, provider, systemPrompt).catch((err) => console.error("poll-wake scheduling failed:", err instanceof Error ? err.message : String(err)));
688
694
  // §send-premature-terminate/[102]<T> — the park DEADLINE (grammar 0.75.0): the
689
695
  // dispatcher recorded the marker's seconds; a bounded park is woken at T
690
696
  // regardless of arrivals, so a park always has a next turn. -1 (indefinite:
@@ -693,25 +699,25 @@ export default class Daemon {
693
699
  if (currentLoopId !== null) {
694
700
  const deadline = this.#engine.parkDeadlines.get(currentLoopId);
695
701
  this.#engine.parkDeadlines.delete(currentLoopId);
696
- const prior = this.#parkTimers.get(runId);
702
+ const prior = this.#parkTimers.get(workerId);
697
703
  if (prior !== undefined) {
698
704
  clearTimeout(prior);
699
- this.#parkTimers.delete(runId);
705
+ this.#parkTimers.delete(workerId);
700
706
  }
701
707
  if (deadline !== undefined && deadline > 0) {
702
708
  const t = setTimeout(() => {
703
- this.#parkTimers.delete(runId);
704
- void this.#wakeParkedRun(sessionId, runId, provider, systemPrompt);
709
+ this.#parkTimers.delete(workerId);
710
+ void this.#wakeParkedWorker(workspaceId, workerId, provider, systemPrompt).catch((err) => console.error("park-deadline wake failed:", err instanceof Error ? err.message : String(err)));
705
711
  }, deadline * 1000);
706
712
  t.unref();
707
- this.#parkTimers.set(runId, t);
713
+ this.#parkTimers.set(workerId, t);
708
714
  }
709
715
  }
710
- // Honor an OWED wake (§run-lifecycle-child-wake): a child/stream concluded while
711
- // this run was mid-turn, before it slept — resume in place rather than park blind,
716
+ // Honor an OWED wake (§worker-lifecycle-child-wake): a child/stream concluded while
717
+ // this worker was mid-turn, before it slept — resume in place rather than park blind,
712
718
  // so a worker-run hibernation always returns. The loop is 202 here; reset to
713
719
  // claimable and the drain re-runs it on the next claim below.
714
- if (this.#owedWakes.delete(runId)) {
720
+ if (this.#owedWakes.delete(workerId)) {
715
721
  await this.#db.drain_resume_slept_loop.run({ loop_id: loopRow.id });
716
722
  continue;
717
723
  }
@@ -720,9 +726,9 @@ export default class Daemon {
720
726
  // conclude-before-block race). An idle wait never reaches here — it concluded at dispatch.
721
727
  continue;
722
728
  }
723
- this.#owedWakes.delete(runId); // the loop concluded (non-202) — no park to honor a held wake at
729
+ this.#owedWakes.delete(workerId); // the loop concluded (non-202) — no park to honor a held wake at
724
730
  const usage = await this.#engine.loopUsage(loopRow.id);
725
- this.#broadcast({ sessionId }, "loop/terminated", {
731
+ this.#broadcast({ workspaceId }, "loop/terminated", {
726
732
  loopId: loopRow.id,
727
733
  finalStatus: result.finalStatus,
728
734
  hitMaxTurns: result.hitMaxTurns,
@@ -745,7 +751,7 @@ export default class Daemon {
745
751
  // A next-turn prompt this loop ended before consuming (a
746
752
  // wake conclusion or a loop.run-while-active) is promoted to
747
753
  // a fresh queued loop so it's never silently dropped.
748
- await this.#reconcileOrphanedWake(runId, loopRow.id);
754
+ await this.#reconcileOrphanedWake(workerId, loopRow.id);
749
755
  }
750
756
  }
751
757
  catch (err) {
@@ -754,7 +760,7 @@ export default class Daemon {
754
760
  // is the loop's TERMINAL state (499), delivered via loop/terminated (loop.run no
755
761
  // longer blocks to return it). A genuine error rejects firstLoopPromise.
756
762
  const usage = currentLoopId === null
757
- ? { promptTokens: 0, completionTokens: 0, costPico: 0, contextTokens: 0, contextSize: null, meta: {} }
763
+ ? { promptTokens: 0, completionTokens: 0, costPico: 0, contextTokens: 0, promptBudget: null, meta: {} }
758
764
  : await this.#engine.loopUsage(currentLoopId);
759
765
  if (currentLoopId !== null) {
760
766
  // #380 (owner ruling) — the cancel is allowed but provenanced: the ROW goes
@@ -764,7 +770,7 @@ export default class Daemon {
764
770
  // loop.cancel reason (cancelDrain threads it through scope.abort).
765
771
  const message = String(controller.signal.reason ?? "user_cancelled").slice(0, 500);
766
772
  await this.#db.engine_loop_cancel_external.run({ loop_id: currentLoopId, message });
767
- this.#broadcast({ sessionId }, "loop/terminated", {
773
+ this.#broadcast({ workspaceId }, "loop/terminated", {
768
774
  loopId: currentLoopId, finalStatus: 499, hitMaxTurns: false, turnIds: [], usage, message,
769
775
  });
770
776
  }
@@ -787,7 +793,7 @@ export default class Daemon {
787
793
  const message = (err instanceof Error ? err.message : String(err)).slice(0, 500);
788
794
  await this.#db.engine_loop_set_status.run({ loop_id: currentLoopId, status: 500, message });
789
795
  const usage = await this.#engine.loopUsage(currentLoopId);
790
- this.#broadcast({ sessionId }, "loop/terminated", {
796
+ this.#broadcast({ workspaceId }, "loop/terminated", {
791
797
  loopId: currentLoopId, finalStatus: 500, hitMaxTurns: false, turnIds: [], usage, message,
792
798
  });
793
799
  }
@@ -803,47 +809,47 @@ export default class Daemon {
803
809
  firstSettled = true;
804
810
  rejectFirst(new Error("drain exited without producing a result"));
805
811
  }
806
- if (this.#activeDrains.get(runId) === handle)
807
- this.#activeDrains.delete(runId);
812
+ if (this.#activeDrains.get(workerId) === handle)
813
+ this.#activeDrains.delete(workerId);
808
814
  }
809
815
  return { loopsDrained, lastResult };
810
816
  })();
811
817
  handle.promise = drainPromise;
812
- this.#activeDrains.set(runId, handle);
813
- // Topology join (§run-lifecycle): when this drain exits having CONCLUDED the run, wake its parent
818
+ this.#activeDrains.set(workerId, handle);
819
+ // Topology join (§run-lifecycle): when this drain exits having CONCLUDED the worker, wake its parent
814
820
  // if parked. Runs after the drain fully tears down (settled promise) so the quiescence check sees
815
- // final state; speculative (#onDrainExit no-ops unless the run concluded AND the parent is parked).
816
- drainPromise.then(() => this.#onDrainExit(sessionId, runId, provider, systemPrompt)).catch(() => { });
821
+ // final state; speculative (#onDrainExit no-ops unless the worker concluded AND the parent is parked).
822
+ drainPromise.then(() => this.#onDrainExit(workspaceId, workerId, provider, systemPrompt)).catch(() => { });
817
823
  // Swallow unhandled rejections (drain aborts with no awaiter); the
818
824
  // error already surfaced via firstLoopPromise or was logged inside.
819
825
  drainPromise.catch(() => { });
820
826
  firstLoopPromise.catch(() => { });
821
827
  return { firstLoopPromise, drainPromise };
822
828
  }
823
- // Per-run drain-transition lock (R4 / §run-lifecycle-single-drain). #ensureDrain's
829
+ // Per-run drain-transition lock (R4 / §worker-lifecycle-single-drain). #ensureDrain's
824
830
  // start and a drain's teardown relinquish both run under it, serialized, so the two
825
- // can't interleave and register two drains for one run. The critical section is the
831
+ // can't interleave and register two drains for one worker. The critical section is the
826
832
  // registry decision only (never a loop's work) — a sub-ms hop at drain boundaries.
827
833
  // A promise-chain mutex: each caller awaits the prior holder; the tail self-prunes
828
834
  // when idle so the Map stays bounded to runs mid-transition.
829
- #withDrainLock(runId, fn) {
830
- const prev = this.#drainLocks.get(runId) ?? Promise.resolve();
835
+ #withDrainLock(workerId, fn) {
836
+ const prev = this.#drainLocks.get(workerId) ?? Promise.resolve();
831
837
  const run = prev.then(fn, fn);
832
838
  const tail = run.catch(() => { });
833
- this.#drainLocks.set(runId, tail);
834
- void tail.then(() => { if (this.#drainLocks.get(runId) === tail)
835
- this.#drainLocks.delete(runId); });
839
+ this.#drainLocks.set(workerId, tail);
840
+ void tail.then(() => { if (this.#drainLocks.get(workerId) === tail)
841
+ this.#drainLocks.delete(workerId); });
836
842
  return run;
837
843
  }
838
- // The drain guarantee, serialized per run via #withDrainLock so it can't race a
844
+ // The drain guarantee, serialized per worker via #withDrainLock so it can't race a
839
845
  // sibling drain's teardown relinquish into a double-drain (R4). A live drain
840
846
  // (registered, NOT aborting) will claim the just-enqueued loop in its own iteration
841
847
  // or its lock-held exit re-claim → return null. A registered-but-ABORTING drain is
842
848
  // in teardown and won't claim, so we don't defer to it — start fresh, or the loop
843
849
  // strands on a cancel/resume race (I6 no-lost-loop). Otherwise start one.
844
850
  #ensureDrain(opts) {
845
- return this.#withDrainLock(opts.runId, async () => {
846
- const existing = this.#activeDrains.get(opts.runId);
851
+ return this.#withDrainLock(opts.workerId, async () => {
852
+ const existing = this.#activeDrains.get(opts.workerId);
847
853
  if (existing !== undefined && !existing.controller.signal.aborted)
848
854
  return null;
849
855
  return this.#startDrain(opts);
@@ -854,17 +860,17 @@ export default class Daemon {
854
860
  // that landed on a turn the loop didn't reach — into a fresh queued loop.
855
861
  // The drain claims it on its next iteration, so a conclusion or client
856
862
  // prompt is never silently dropped. Inherits the ended loop's flags.
857
- async #reconcileOrphanedWake(runId, endedLoopId) {
863
+ async #reconcileOrphanedWake(workerId, endedLoopId) {
858
864
  const endedSeq = (await this.#db.engine_loop_sequence.get({ loop_id: endedLoopId }))?.sequence ?? endedLoopId;
859
- const prefix = promptLoopPrefix(runId, endedSeq);
865
+ const prefix = promptLoopPrefix(workerId, endedSeq);
860
866
  const orphan = await this.#db.drain_orphaned_prompt_for_loop.get({ loop_id: endedLoopId, pattern: `${prefix}%`, prefix_len: prefix.length });
861
867
  if (orphan === undefined)
862
868
  return;
863
- const seqRow = await this.#db.loop_run_next_sequence.get({ run_id: runId });
869
+ const seqRow = await this.#db.loop_run_next_sequence.get({ worker_id: workerId });
864
870
  if (seqRow === undefined)
865
871
  throw new Error("reconcileOrphanedWake: next-sequence query returned no row");
866
872
  const fresh = await this.#db.drain_enqueue_loop.get({
867
- run_id: runId, sequence: seqRow.next, prompt: orphan.body,
873
+ worker_id: workerId, sequence: seqRow.next, prompt: orphan.body,
868
874
  });
869
875
  if (fresh === undefined)
870
876
  throw new Error("reconcileOrphanedWake: enqueue returned no row");
@@ -872,89 +878,89 @@ export default class Daemon {
872
878
  await this.#db.engine_set_loop_flags.run({ loop_id: fresh.id, flags: orphan.flags });
873
879
  }
874
880
  }
875
- // The run's cancellation scope — lazily created, and replaced once aborted
881
+ // The worker's cancellation scope — lazily created, and replaced once aborted
876
882
  // so a later loop.run gets a live signal. The drain and the execs its loops
877
883
  // spawn all run under it.
878
- #runSignal(runId) {
879
- const existing = this.#runAborts.get(runId);
884
+ #workerSignal(workerId) {
885
+ const existing = this.#workerAborts.get(workerId);
880
886
  if (existing !== undefined && !existing.signal.aborted)
881
887
  return existing;
882
888
  const fresh = new AbortController();
883
- this.#runAborts.set(runId, fresh);
889
+ this.#workerAborts.set(workerId, fresh);
884
890
  return fresh;
885
891
  }
886
892
  /**
887
- * Cancel the run's in-flight work (loop.cancel). One abort, one scope: the
893
+ * Cancel the worker's in-flight work (loop.cancel). One abort, one scope: the
888
894
  * run signal stops the running loop's turn generation AND tears down every
889
895
  * stream linked to it — a background exec that outlived its loop, or even a
890
896
  * spawn that registers after this abort (it self-aborts against the aborted
891
897
  * signal). Returns cancelled iff there was work. Queued loops stay enqueued.
892
898
  */
893
- cancelDrain(runId, reason = "user_cancelled") {
894
- const hadDrain = this.#activeDrains.has(runId);
895
- const hadWork = hadDrain || this.#runHasActiveStreams(runId);
896
- // A cancel is deliberate — kill any pending hibernation poll-wake so it can't resurrect the run.
897
- const pollTimer = this.#pollTimers.get(runId);
899
+ cancelDrain(workerId, reason = "user_cancelled") {
900
+ const hadDrain = this.#activeDrains.has(workerId);
901
+ const hadWork = hadDrain || this.#workerHasActiveStreams(workerId);
902
+ // A cancel is deliberate — kill any pending hibernation poll-wake so it can't resurrect the worker.
903
+ const pollTimer = this.#pollTimers.get(workerId);
898
904
  if (pollTimer !== undefined) {
899
905
  clearTimeout(pollTimer);
900
- this.#pollTimers.delete(runId);
906
+ this.#pollTimers.delete(workerId);
901
907
  }
902
- // Stop the active drain's turn-generation (its loop closes 499). The run
908
+ // Stop the active drain's turn-generation (its loop closes 499). The worker
903
909
  // signal is the optimization path — the fast, listener-driven reap.
904
- const scope = this.#runAborts.get(runId);
910
+ const scope = this.#workerAborts.get(workerId);
905
911
  if (scope !== undefined && !scope.signal.aborted)
906
912
  scope.abort(reason);
907
913
  // #380, the PARKED case — a 202-blocked loop has no drain to observe the abort (the
908
914
  // drain tears down on 202), so before this a cancelled park stayed 202 forever with no
909
- // terminal and no broadcast. Terminalize the run's live loops (102/202; queued 100 stays
915
+ // terminal and no broadcast. Terminalize the worker's live loops (102/202; queued 100 stays
910
916
  // enqueued) with provenance and broadcast each. With a live drain its abort catch does
911
917
  // this instead — skipping here keeps the broadcast single.
912
918
  if (!hadDrain) {
913
919
  void (async () => {
914
920
  const message = reason.slice(0, 500);
915
- const dead = await this.#db.engine_run_cancel_live_loops.all({ run_id: runId, message });
921
+ const dead = await this.#db.engine_worker_cancel_live_loops.all({ worker_id: workerId, message });
916
922
  if (dead.length === 0)
917
923
  return;
918
- const srow = await this.#db.drain_get_run_session.get({ run_id: runId });
924
+ const srow = await this.#db.drain_get_worker_workspace.get({ worker_id: workerId });
919
925
  if (srow === undefined)
920
926
  return;
921
927
  for (const { id } of dead) {
922
928
  const usage = await this.#engine.loopUsage(id);
923
- this.#broadcast({ sessionId: srow.session_id }, "loop/terminated", {
929
+ this.#broadcast({ workspaceId: srow.workspace_id }, "loop/terminated", {
924
930
  loopId: id, finalStatus: 499, hitMaxTurns: false, turnIds: [], usage, message,
925
931
  });
926
932
  }
927
933
  })().catch((err) => {
928
- console.error(`cancelDrain(${runId}) live-loop terminalize failed:`, err);
934
+ console.error(`cancelDrain(${workerId}) live-loop terminalize failed:`, err);
929
935
  });
930
936
  }
931
- // Total reap by the REGISTRY (§run-lifecycle-total-reap): the durable source
932
- // of truth. Every open subscription the run holds, aborted via its owning
937
+ // Total reap by the REGISTRY (§worker-lifecycle-total-reap): the durable source
938
+ // of truth. Every open subscription the worker holds, aborted via its owning
933
939
  // scheme — independent of the signal-listener timing, so an exec mid-spawn
934
940
  // (registry row written before it is killable) is reaped too. A late spawn
935
941
  // (registering after this) self-aborts against its captured, now-aborted
936
942
  // epoch (§exec-timeout). Idempotent; fire-and-forget (the
937
943
  // abort is sync, the registry read async; the 499 conclusion surfaces async).
938
- void this.#reapRunStreams(runId).catch((err) => {
939
- console.error(`reapRunStreams(${runId}) failed:`, err);
944
+ void this.#reapWorkerStreams(workerId).catch((err) => {
945
+ console.error(`reapWorkerStreams(${workerId}) failed:`, err);
940
946
  });
941
947
  return hadWork;
942
948
  }
943
- // Does the run have an in-flight stream (a background exec)? Used only for
949
+ // Does the worker have an in-flight stream (a background exec)? Used only for
944
950
  // loop.cancel's cancelled=true/false answer; the teardown itself rides the
945
951
  // run signal. Duck-typed like #drainStreamingSchemes.
946
- #runHasActiveStreams(runId) {
952
+ #workerHasActiveStreams(workerId) {
947
953
  const exec = this.#schemes.get("exec");
948
- return exec?.hasActiveSpawns?.(runId) ?? false;
954
+ return exec?.hasActiveSpawns?.(workerId) ?? false;
949
955
  }
950
- // The registry-routed reap (§run-lifecycle-total-reap): every open subscription
951
- // the run holds, aborted via its owning scheme. The durable answer to "reap
956
+ // The registry-routed reap (§worker-lifecycle-total-reap): every open subscription
957
+ // the worker holds, aborted via its owning scheme. The durable answer to "reap
952
958
  // everything" — the in-process AbortSignal listener is the optimization, this is
953
959
  // the source of truth: an exec mid-spawn (registry row written before it is
954
960
  // killable) or a background exec from any past loop is caught regardless of
955
961
  // listener timing. Idempotent — a stream the signal already reaped is a no-op.
956
- async #reapRunStreams(runId) {
957
- const open = await ChannelWrite.findOpenSubscriptionsForRun(this.#db, runId);
962
+ async #reapWorkerStreams(workerId) {
963
+ const open = await ChannelWrite.findOpenSubscriptionsForWorker(this.#db, workerId);
958
964
  for (const { id, scheme } of open) {
959
965
  const handler = this.#schemes.get(scheme);
960
966
  handler?.abortSubscription?.(id);
@@ -962,7 +968,7 @@ export default class Daemon {
962
968
  }
963
969
  /**
964
970
  * Wake-on-completion handler. Streaming schemes call this when a
965
- * subscription closes. If the run has an active loop, the channel
971
+ * subscription closes. If the worker has an active loop, the channel
966
972
  * transition will surface at that loop's next turn boundary — no new
967
973
  * loop needed. Otherwise we open a fresh loop with the synthetic
968
974
  * summary as the user prompt so the model gets a chance to react.
@@ -973,65 +979,53 @@ export default class Daemon {
973
979
  *
974
980
  * Rummy parallel: plugins/stream/stream.js stream/completed wake:true.
975
981
  */
976
- async #handleWakeRun(payload) {
982
+ async #handleWakeWorker(payload) {
977
983
  // §search-gate — settle the dedup registration: promote on a 200 conclusion, drop on
978
984
  // failure (a dead search must never serve as a duplicate). No-op for non-search streams.
979
985
  this.#engine.searchGate.settle(payload.target.replace(/^[a-z+.-]+:\/\//, "/").replace(/^\/+/, "/"), payload.closeStatus);
980
- // §search-prefetch — page conclusions from the pass's own fan-out are sync-write
981
- // internals, never wake edges; swallow them (still broadcast for client telemetry).
982
- if (this.#engine.searchPrefetch.ownsConclusion(payload)) {
983
- this.#broadcast({ sessionId: payload.sessionId }, "stream/concluded", { ...payload, wakeAction: "prefetch-page" });
984
- return;
985
- }
986
- // §search-prefetch — a search runtime concluded: materialize the candidates and rewrite
987
- // #results as the survivor render BEFORE the wake proceeds, so the fold-back the model
988
- // reads is post-prefetch truth (the Engine hold loop holds while the pass runs).
989
- if (this.#engine.searchPrefetch.isSearchConclusion(payload)) {
990
- await this.#engine.searchPrefetch.onSearchConcluded(payload, (s, r) => this.#engine.prefetchCtx(s, r));
991
- }
992
986
  // Aborted streams don't wake — the abort was deliberate.
993
987
  if (payload.closeStatus === 499) {
994
- this.#broadcast({ sessionId: payload.sessionId }, "stream/concluded", {
988
+ this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
995
989
  ...payload, wakeAction: "skipped-aborted",
996
990
  });
997
991
  return;
998
992
  }
999
- // No resurrection (§run-lifecycle-no-resurrection): a non-499 completion whose
993
+ // No resurrection (§worker-lifecycle-no-resurrection): a non-499 completion whose
1000
994
  // run was CANCELLED (idle + its scope aborted) must not start a fresh drain —
1001
995
  // the cancel was deliberate. The deliverable is already in the channel/log and
1002
- // surfaces as a `collect` environment delta (§env-delta) if the run is read or
996
+ // surfaces as a `collect` environment delta (§env-delta) if the worker is read or
1003
997
  // resumed; we just don't inject a turn. (An active run folds the wake into its
1004
998
  // next turn via inject below; a resumed run is active, never aborted, so it is
1005
999
  // unaffected.)
1006
- const scope = this.#runAborts.get(payload.runId);
1007
- if (scope?.signal.aborted === true && !this.#activeDrains.has(payload.runId)) {
1008
- this.#broadcast({ sessionId: payload.sessionId }, "stream/concluded", {
1000
+ const scope = this.#workerAborts.get(payload.workerId);
1001
+ if (scope?.signal.aborted === true && !this.#activeDrains.has(payload.workerId)) {
1002
+ this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
1009
1003
  ...payload, wakeAction: "skipped-cancelled",
1010
1004
  });
1011
1005
  return;
1012
1006
  }
1013
1007
  if (this.#provider === null) {
1014
- this.#broadcast({ sessionId: payload.sessionId }, "stream/concluded", {
1008
+ this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
1015
1009
  ...payload, wakeAction: "skipped-no-provider",
1016
1010
  });
1017
1011
  return;
1018
1012
  }
1019
1013
  try {
1020
1014
  const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
1021
- // A slept (202) loop means the run PARKED ([102]<T>/<-1>) → RESUME it IN PLACE: re-queue
1015
+ // A slept (202) loop means the worker PARKED ([102]<T>/<-1>) → RESUME it IN PLACE: re-queue
1022
1016
  // it (202→100) so the drain re-claims and CONTINUES it (seq>1 → no re-foist). Checked
1023
- // FIRST: the slept status is the run's true disposition regardless of a draining
1017
+ // FIRST: the slept status is the worker's true disposition regardless of a draining
1024
1018
  // sibling mid-teardown (the #ensureDrain lock serializes the re-claim). No fresh loop,
1025
1019
  // no summary-as-prompt — the resumed loop reads the concluded stream's own state from
1026
- // the manifest. §run-lifecycle-wake-liveness.
1027
- const slept = await this.#db.drain_find_slept_loop.get({ run_id: payload.runId });
1020
+ // the manifest. §worker-lifecycle-wake-liveness.
1021
+ const slept = await this.#db.drain_find_slept_loop.get({ worker_id: payload.workerId });
1028
1022
  if (slept !== undefined) {
1029
1023
  await this.#db.drain_resume_slept_loop.run({ loop_id: slept.id });
1030
1024
  const started = await this.#ensureDrain({
1031
- sessionId: payload.sessionId, runId: payload.runId, provider: this.#provider,
1025
+ workspaceId: payload.workspaceId, workerId: payload.workerId, provider: this.#provider,
1032
1026
  systemPrompt, maxTurns: Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "50"),
1033
1027
  });
1034
- this.#broadcast({ sessionId: payload.sessionId }, "stream/concluded", {
1028
+ this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
1035
1029
  ...payload, wakeAction: "resumed-loop", wakeLoopId: slept.id,
1036
1030
  });
1037
1031
  started?.drainPromise?.catch((err) => {
@@ -1043,15 +1037,15 @@ export default class Daemon {
1043
1037
  // environment-observation injector (§exec-stream) on its next turn — there is no prompt
1044
1038
  // to inject and NO task to overwrite. The obsolete "automated environment update"
1045
1039
  // synthesis (which clobbered the model's actual goal) is retired; just tell the client.
1046
- if (this.#activeDrains.has(payload.runId)) {
1047
- this.#broadcast({ sessionId: payload.sessionId }, "stream/concluded", {
1040
+ if (this.#activeDrains.has(payload.workerId)) {
1041
+ this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
1048
1042
  ...payload, wakeAction: "no-op-active-loop",
1049
1043
  });
1050
1044
  return;
1051
1045
  }
1052
1046
  // No slept loop, no active drain — nothing to resume (e.g. a SEND[200]-done run whose
1053
1047
  // streams were swept). Surface the conclusion without opening a loop.
1054
- this.#broadcast({ sessionId: payload.sessionId }, "stream/concluded", {
1048
+ this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
1055
1049
  ...payload, wakeAction: "no-loop",
1056
1050
  });
1057
1051
  }
@@ -1061,18 +1055,18 @@ export default class Daemon {
1061
1055
  }
1062
1056
  /**
1063
1057
  * grammar 0.74.20 EXEC `<T,P>` — schedule a hibernation poll-wake. Called when a loop parks at
1064
- * a park; if the run holds an open polled stream, arm a timer for its tightest cadence P that
1058
+ * a park; if the worker holds an open polled stream, arm a timer for its tightest cadence P that
1065
1059
  * resumes the slept loop so the model inspects progress. While the loop is ACTIVE there is no
1066
1060
  * poll work — ambient folded stream deltas already surface progress (§exec-stream); the wake
1067
1061
  * matters only across hibernation. A wake-edge-less 202 (no polled stream) gets no timer. §exec-poll
1068
1062
  */
1069
- async #schedulePollWake(sessionId, runId, provider, systemPrompt) {
1070
- const existing = this.#pollTimers.get(runId);
1063
+ async #schedulePollWake(workspaceId, workerId, provider, systemPrompt) {
1064
+ const existing = this.#pollTimers.get(workerId);
1071
1065
  if (existing !== undefined) {
1072
1066
  clearTimeout(existing);
1073
- this.#pollTimers.delete(runId);
1067
+ this.#pollTimers.delete(workerId);
1074
1068
  }
1075
- const row = await this.#db.drain_run_min_poll.get({ run_id: runId });
1069
+ const row = await this.#db.drain_worker_min_poll.get({ worker_id: workerId });
1076
1070
  const pollSec = row?.poll_seconds ?? null;
1077
1071
  if (pollSec === null || pollSec <= 0)
1078
1072
  return; // no polled stream → the 202 just sleeps (woken only by conclusion)
@@ -1080,71 +1074,71 @@ export default class Daemon {
1080
1074
  // faster than a turn settles — §exec-poll.
1081
1075
  const execWaitMs = Number(process.env.PLURNK_SERVICE_EXEC_WAIT_MS ?? "0");
1082
1076
  const timer = setTimeout(() => {
1083
- this.#pollTimers.delete(runId);
1084
- void this.#wakeParkedRun(sessionId, runId, provider, systemPrompt);
1077
+ this.#pollTimers.delete(workerId);
1078
+ void this.#wakeParkedWorker(workspaceId, workerId, provider, systemPrompt);
1085
1079
  }, Math.max(pollSec * 1000, execWaitMs));
1086
1080
  timer.unref();
1087
- this.#pollTimers.set(runId, timer);
1081
+ this.#pollTimers.set(workerId, timer);
1088
1082
  }
1089
- /** Resume `runId`'s slept (202) loop in place — the same 202→100 resume #handleWakeRun uses, minus a
1083
+ /** Resume `workerId`'s slept (202) loop in place — the same 202→100 resume #handleWakeWorker uses, minus a
1090
1084
  * wake payload. The shared wake primitive: a poll cadence (§exec-poll), a watched stream concluding,
1091
- * or a child run finishing (§run-lifecycle topology join) all call this. A no-op if the run was
1085
+ * or a child worker finishing (§run-lifecycle topology join) all call this. A no-op if the worker was
1092
1086
  * cancelled or isn't actually parked (no slept loop) — so calling it speculatively is safe. */
1093
- async #wakeParkedRun(sessionId, runId, provider, systemPrompt) {
1094
- const scope = this.#runAborts.get(runId);
1095
- if (scope?.signal.aborted === true && !this.#activeDrains.has(runId))
1087
+ async #wakeParkedWorker(workspaceId, workerId, provider, systemPrompt) {
1088
+ const scope = this.#workerAborts.get(workerId);
1089
+ if (scope?.signal.aborted === true && !this.#activeDrains.has(workerId))
1096
1090
  return; // cancelled — no resurrection
1097
- const slept = await this.#db.drain_find_slept_loop.get({ run_id: runId });
1091
+ const slept = await this.#db.drain_find_slept_loop.get({ worker_id: workerId });
1098
1092
  if (slept === undefined) {
1099
- // Not parked. If a drain is still ACTIVE, the run is mid-turn and about to park — the
1093
+ // Not parked. If a drain is still ACTIVE, the worker is mid-turn and about to park — the
1100
1094
  // conclusion that fired this wake arrived before the 202 committed (the conclude-before-park
1101
1095
  // race). OWE the wake: the drain honors it at park so a worker-run hibernation never deadlocks.
1102
1096
  // (No active drain → already concluded/running; nothing to wake.)
1103
- if (this.#activeDrains.has(runId))
1104
- this.#owedWakes.add(runId);
1097
+ if (this.#activeDrains.has(workerId))
1098
+ this.#owedWakes.add(workerId);
1105
1099
  return;
1106
1100
  }
1107
1101
  await this.#db.drain_resume_slept_loop.run({ loop_id: slept.id });
1108
1102
  const started = await this.#ensureDrain({
1109
- sessionId, runId, provider, systemPrompt,
1103
+ workspaceId, workerId, provider, systemPrompt,
1110
1104
  maxTurns: Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "50"),
1111
1105
  });
1112
1106
  started?.drainPromise?.catch((err) => {
1113
1107
  console.error("wake-parked resume drain failed:", err instanceof Error ? err.message : String(err));
1114
1108
  });
1115
1109
  }
1116
- /** A run's drain exited. If the run truly CONCLUDED — no 202-blocked loop, no open stream — then
1110
+ /** A worker's drain exited. If the worker truly CONCLUDED — no 202-blocked loop, no open stream — then
1117
1111
  * wake its PARENT in place if the parent is blocked on the join (the structured-concurrency join — a
1118
- * child finishing is the wake edge for a parent that waited on it, §run-lifecycle-child-wake). A run
1112
+ * child finishing is the wake edge for a parent that waited on it, §worker-lifecycle-child-wake). A worker
1119
1113
  * blocked at 202, or still holding a stream, is NOT concluded — its own wake edges drive it, not this.
1120
- * The parent reads the child's deliverable from its own log (the §run-scheme-collect delta) on
1114
+ * The parent reads the child's deliverable from its own log (the §worker-scheme-collect delta) on
1121
1115
  * resume — control edge here, never an injected prompt. Recurses up via the parent's own drain-exit. */
1122
- async #onDrainExit(sessionId, runId, provider, systemPrompt) {
1123
- const slept = await this.#db.drain_find_slept_loop.get({ run_id: runId });
1116
+ async #onDrainExit(workspaceId, workerId, provider, systemPrompt) {
1117
+ const slept = await this.#db.drain_find_slept_loop.get({ worker_id: workerId });
1124
1118
  if (slept !== undefined)
1125
- return; // parked at 202 — not concluded, the run is still alive
1126
- const openSubs = await this.#db.find_open_subscriptions_for_run.all({ run_id: runId });
1119
+ return; // parked at 202 — not concluded, the worker is still alive
1120
+ const openSubs = await this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId });
1127
1121
  if (openSubs.length > 0)
1128
1122
  return; // a stream still runs — its conclusion re-evaluates, not this exit
1129
- const parent = await this.#db.run_parent_id.get({ run_id: runId });
1130
- if (parent?.parent_run_id == null)
1123
+ const parent = await this.#db.worker_parent_id.get({ worker_id: workerId });
1124
+ if (parent?.parent_worker_id == null)
1131
1125
  return; // a root run — nobody to wake
1132
- await this.#wakeParkedRun(sessionId, parent.parent_run_id, provider, systemPrompt);
1126
+ await this.#wakeParkedWorker(workspaceId, parent.parent_worker_id, provider, systemPrompt);
1133
1127
  }
1134
1128
  #broadcast(target, method, params) {
1135
1129
  if (target === "all") {
1136
- // A global engine event (e.g. session/created) — emitted to the seam with sessionId null (#355).
1130
+ // A global engine event (e.g. workspace/created) — emitted to the seam with workspaceId null (#355).
1137
1131
  for (const sub of this.#eventSubscribers)
1138
1132
  sub(null, method, params);
1139
1133
  return;
1140
1134
  }
1141
- const sessionId = target.sessionId;
1135
+ const workspaceId = target.workspaceId;
1142
1136
  // Publish the raw event to the in-process source first (#355) — transport modules subscribe
1143
1137
  // here (plurnk-agui renders to AG-UI+). Each subscriber owns its own fan-out; core just emits.
1144
1138
  for (const sub of this.#eventSubscribers)
1145
- sub(sessionId, method, params);
1146
- // Scope-stamping onto the notification envelope (§notifications-envelope-carries-sessionid)
1147
- // is each subscriber's edge concern now — the seam hands (sessionId, method, params) raw.
1139
+ sub(workspaceId, method, params);
1140
+ // Scope-stamping onto the notification envelope (§notifications-envelope-carries-workspaceid)
1141
+ // is each subscriber's edge concern now — the seam hands (workspaceId, method, params) raw.
1148
1142
  }
1149
1143
  }
1150
1144
  //# sourceMappingURL=Daemon.js.map