@north-light/crouter 0.3.281 → 0.3.283

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 (188) hide show
  1. package/dist/api/client.d.ts +19 -3
  2. package/dist/api/client.js +34 -0
  3. package/dist/api/dto/broker-ops.d.ts +11 -0
  4. package/dist/api/dto/broker.d.ts +3 -8
  5. package/dist/api/dto/common.d.ts +1 -1
  6. package/dist/api/dto/messages.d.ts +2 -2
  7. package/dist/api/dto/node-outcomes.d.ts +80 -0
  8. package/dist/api/dto/node-outcomes.js +2 -0
  9. package/dist/api/dto/nodes.d.ts +9 -0
  10. package/dist/api/dto/profiles.d.ts +5 -0
  11. package/dist/api/dto/reports.d.ts +45 -0
  12. package/dist/api/index.d.ts +1 -0
  13. package/dist/api/index.js +1 -0
  14. package/dist/api/plugin-manifest-schema.d.ts +208 -0
  15. package/dist/api/plugin-manifest-schema.js +23 -0
  16. package/dist/api/routes.d.ts +7 -0
  17. package/dist/api/routes.js +7 -0
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +24 -3
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +31 -4
  20. package/dist/cli.js +8 -2
  21. package/dist/clients/attach/overlays/graph.d.ts +5 -0
  22. package/dist/clients/attach/overlays/graph.js +65 -47
  23. package/dist/clients/attach/overlays/pickers.js +8 -9
  24. package/dist/clients/attach/render/card-presentation.js +1 -1
  25. package/dist/clients/attach/viewer.js +1286 -1260
  26. package/dist/commands/api-client.js +22 -23
  27. package/dist/commands/node/create.js +38 -3
  28. package/dist/commands/node/inspect.js +3 -7
  29. package/dist/commands/node/message.js +3 -3
  30. package/dist/commands/node/outcome.d.ts +2 -0
  31. package/dist/commands/node/outcome.js +134 -0
  32. package/dist/commands/node.js +2 -1
  33. package/dist/commands/pkg/browse/catalog.js +1 -1
  34. package/dist/commands/pkg/plugin-manage.d.ts +46 -1
  35. package/dist/commands/pkg/plugin-manage.js +170 -20
  36. package/dist/commands/profile/pause.js +17 -14
  37. package/dist/commands/push.js +102 -1
  38. package/dist/commands/sys/__tests__/setup-core.test.js +4 -1
  39. package/dist/commands/sys/panels/broker-limits-panel.js +7 -1
  40. package/dist/commands/sys/panels/models-panel.js +2 -2
  41. package/dist/commands/sys/settings-shell.d.ts +1 -1
  42. package/dist/commands/sys/settings-shell.js +6 -0
  43. package/dist/commands/sys/settings.js +3 -3
  44. package/dist/core/__tests__/fixtures/fake-engine.d.ts +2 -0
  45. package/dist/core/__tests__/fixtures/fake-engine.js +42 -4
  46. package/dist/core/__tests__/helpers/harness.d.ts +1 -0
  47. package/dist/core/__tests__/helpers/harness.js +9 -0
  48. package/dist/core/__tests__/integration/plugin-revalidate.test.d.ts +1 -0
  49. package/dist/core/__tests__/integration/plugin-revalidate.test.js +355 -0
  50. package/dist/core/__tests__/integration/profile-pause.test.d.ts +1 -0
  51. package/dist/core/__tests__/integration/profile-pause.test.js +115 -0
  52. package/dist/core/__tests__/node-outcome.test.d.ts +1 -0
  53. package/dist/core/__tests__/node-outcome.test.js +184 -0
  54. package/dist/core/__tests__/revive-parked-fresh.test.js +13 -20
  55. package/dist/core/__tests__/seam/broker-provider-retry.test.js +6 -2
  56. package/dist/core/__tests__/seam/dormancy-release.test.js +3 -3
  57. package/dist/core/__tests__/seam/node-outcome-delivery.test.d.ts +1 -0
  58. package/dist/core/__tests__/seam/node-outcome-delivery.test.js +153 -0
  59. package/dist/core/bash-jobs.d.ts +6 -0
  60. package/dist/core/bash-jobs.js +22 -0
  61. package/dist/core/canvas/canvas.d.ts +41 -3
  62. package/dist/core/canvas/canvas.js +153 -39
  63. package/dist/core/canvas/crons.js +2 -1
  64. package/dist/core/canvas/index.d.ts +1 -0
  65. package/dist/core/canvas/index.js +1 -0
  66. package/dist/core/canvas/migrations.js +47 -0
  67. package/dist/core/canvas/node-outcome-deliveries.d.ts +80 -0
  68. package/dist/core/canvas/node-outcome-deliveries.js +163 -0
  69. package/dist/core/canvas/pid.d.ts +7 -0
  70. package/dist/core/canvas/pid.js +17 -0
  71. package/dist/core/canvas/render-source.js +2 -2
  72. package/dist/core/canvas/types.d.ts +42 -1
  73. package/dist/core/canvas/types.js +2 -1
  74. package/dist/core/command-manifests/registry.d.ts +3 -1
  75. package/dist/core/command-manifests/schema.d.ts +2 -75
  76. package/dist/core/command-manifests/schema.js +5 -0
  77. package/dist/core/command-plugins/compose.js +1 -1
  78. package/dist/core/command-plugins/revalidate.d.ts +18 -0
  79. package/dist/core/command-plugins/revalidate.js +152 -0
  80. package/dist/core/command-plugins/transport/http-fetch.d.ts +29 -4
  81. package/dist/core/command-plugins/transport/http-fetch.js +18 -8
  82. package/dist/core/command-plugins/transport/http-invoke.js +7 -0
  83. package/dist/core/command.d.ts +8 -0
  84. package/dist/core/command.js +19 -3
  85. package/dist/core/config.d.ts +4 -1
  86. package/dist/core/config.js +25 -1
  87. package/dist/core/exclusive-lock.d.ts +12 -0
  88. package/dist/core/exclusive-lock.js +29 -0
  89. package/dist/core/feed/feed.d.ts +8 -0
  90. package/dist/core/feed/feed.js +25 -4
  91. package/dist/core/feed/inbox.d.ts +5 -0
  92. package/dist/core/feed/inbox.js +13 -0
  93. package/dist/core/installed-plugins.js +31 -0
  94. package/dist/core/keybindings/catalog.d.ts +1 -1
  95. package/dist/core/keybindings/catalog.js +3 -0
  96. package/dist/core/manifest-recovery.d.ts +15 -0
  97. package/dist/core/manifest-recovery.js +72 -0
  98. package/dist/core/manifest-stale.d.ts +18 -0
  99. package/dist/core/manifest-stale.js +39 -0
  100. package/dist/core/plugin-swap-lock.d.ts +9 -0
  101. package/dist/core/plugin-swap-lock.js +31 -0
  102. package/dist/core/profiles/manifest.d.ts +3 -1
  103. package/dist/core/profiles/manifest.js +6 -1
  104. package/dist/core/runtime/broker/daemon-ops.d.ts +3 -1
  105. package/dist/core/runtime/broker/daemon-ops.js +6 -0
  106. package/dist/core/runtime/broker/engine-drive.d.ts +2 -3
  107. package/dist/core/runtime/broker/engine-drive.js +74 -27
  108. package/dist/core/runtime/broker/frame-dispatch.js +1 -0
  109. package/dist/core/runtime/broker/read-ops.js +14 -0
  110. package/dist/core/runtime/broker/rebind.js +0 -7
  111. package/dist/core/runtime/broker-protocol.d.ts +1 -6
  112. package/dist/core/runtime/fleet.d.ts +7 -0
  113. package/dist/core/runtime/headless-pi.d.ts +13 -0
  114. package/dist/core/runtime/headless-pi.js +10 -2
  115. package/dist/core/runtime/lifecycle.d.ts +6 -1
  116. package/dist/core/runtime/lifecycle.js +50 -1
  117. package/dist/core/runtime/nodes.d.ts +5 -1
  118. package/dist/core/runtime/nodes.js +2 -1
  119. package/dist/core/runtime/outcome-document.d.ts +70 -0
  120. package/dist/core/runtime/outcome-document.js +97 -0
  121. package/dist/core/runtime/revive.js +16 -21
  122. package/dist/core/runtime/spawn.d.ts +5 -1
  123. package/dist/core/runtime/spawn.js +18 -7
  124. package/dist/core/runtime/stamp/channel.d.ts +3 -5
  125. package/dist/core/runtime/stamp/channel.js +3 -10
  126. package/dist/core/runtime/stop-guard.js +2 -2
  127. package/dist/core/runtime/structured-output.d.ts +24 -3
  128. package/dist/core/runtime/structured-output.js +42 -3
  129. package/dist/core/runtime/tmux-bindings.js +4 -0
  130. package/dist/core/runtime/warm-pool.d.ts +5 -2
  131. package/dist/core/runtime/warm-pool.js +5 -5
  132. package/dist/core/subscription-state.js +7 -5
  133. package/dist/core/substrate/render.js +10 -10
  134. package/dist/core/user-settings.d.ts +18 -1
  135. package/dist/core/user-settings.js +37 -0
  136. package/dist/daemon/__tests__/node-deadline-lane.test.d.ts +1 -0
  137. package/dist/daemon/__tests__/node-deadline-lane.test.js +76 -0
  138. package/dist/daemon/__tests__/node-outcome-birth-invariants.test.d.ts +1 -0
  139. package/dist/daemon/__tests__/node-outcome-birth-invariants.test.js +122 -0
  140. package/dist/daemon/__tests__/node-outcome-delivery.test.d.ts +1 -0
  141. package/dist/daemon/__tests__/node-outcome-delivery.test.js +86 -0
  142. package/dist/daemon/api/__tests__/broker-settle-park.test.js +86 -18
  143. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +10 -1
  144. package/dist/daemon/api/__tests__/seam/node-outcome-api.test.d.ts +1 -0
  145. package/dist/daemon/api/__tests__/seam/node-outcome-api.test.js +159 -0
  146. package/dist/daemon/api/__tests__/submit-result.test.d.ts +1 -0
  147. package/dist/daemon/api/__tests__/submit-result.test.js +214 -0
  148. package/dist/daemon/api/handlers/broker-ops.js +53 -24
  149. package/dist/daemon/api/handlers/canvas.js +2 -2
  150. package/dist/daemon/api/handlers/messages.js +12 -2
  151. package/dist/daemon/api/handlers/node-outcomes.d.ts +2 -0
  152. package/dist/daemon/api/handlers/node-outcomes.js +136 -0
  153. package/dist/daemon/api/handlers/nodes.js +67 -1
  154. package/dist/daemon/api/handlers/profiles.js +53 -1
  155. package/dist/daemon/api/handlers/reports.js +197 -33
  156. package/dist/daemon/api/map.d.ts +7 -2
  157. package/dist/daemon/api/map.js +78 -4
  158. package/dist/daemon/api/server.js +2 -0
  159. package/dist/daemon/crtrd.js +12 -0
  160. package/dist/daemon/fleet.d.ts +4 -8
  161. package/dist/daemon/fleet.js +48 -9
  162. package/dist/daemon/messaging/node-message.js +2 -1
  163. package/dist/daemon/node-outcome/deliver-outcome.d.ts +14 -0
  164. package/dist/daemon/node-outcome/deliver-outcome.js +155 -0
  165. package/dist/daemon/park-activity.d.ts +9 -0
  166. package/dist/daemon/park-activity.js +30 -0
  167. package/dist/daemon/park-pending.d.ts +10 -5
  168. package/dist/daemon/park-pending.js +18 -8
  169. package/dist/daemon/reconcilers/bash-deadline.js +6 -0
  170. package/dist/daemon/reconcilers/broker-supervision.d.ts +3 -7
  171. package/dist/daemon/reconcilers/broker-supervision.js +29 -33
  172. package/dist/daemon/reconcilers/node-deadline.d.ts +5 -0
  173. package/dist/daemon/reconcilers/node-deadline.js +82 -0
  174. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +9 -1
  175. package/dist/daemon/reconcilers/node-lifecycle/tick.js +12 -3
  176. package/dist/daemon/reconcilers/node-outcome-delivery-lane.d.ts +10 -0
  177. package/dist/daemon/reconcilers/node-outcome-delivery-lane.js +41 -0
  178. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +66 -66
  179. package/dist/pi-extensions/canvas-structured-output.d.ts +9 -5
  180. package/dist/pi-extensions/canvas-structured-output.js +23 -297
  181. package/dist/shared/env.d.ts +3 -0
  182. package/dist/shared/env.js +6 -0
  183. package/dist/shared/generated-context.d.ts +3 -9
  184. package/dist/shared/generated-context.js +8 -16
  185. package/dist/types.d.ts +19 -0
  186. package/dist/types.js +1 -0
  187. package/package.json +9 -8
  188. package/runtime.lock.json +227 -383
@@ -343,6 +343,25 @@ function mergeSpawnEnv(raw, base = []) {
343
343
  }
344
344
  return { allow: [...new Set([...base, ...extra])] };
345
345
  }
346
+ /** Per-provider options, merged SHALLOWLY per provider id so a nearer scope
347
+ * that sets one option does not erase a sibling option from a farther scope. */
348
+ function mergeProviderOptions(raw, base = {}) {
349
+ const merged = {};
350
+ for (const [providerId, options] of Object.entries(base))
351
+ merged[providerId] = { ...options };
352
+ if (raw !== null && typeof raw === 'object') {
353
+ for (const [providerId, options] of Object.entries(raw)) {
354
+ if (options === null || typeof options !== 'object')
355
+ continue;
356
+ const next = { ...merged[providerId] };
357
+ const fastMode = options.fastMode;
358
+ if (typeof fastMode === 'boolean')
359
+ next.fastMode = fastMode;
360
+ merged[providerId] = next;
361
+ }
362
+ }
363
+ return merged;
364
+ }
346
365
  /** The one bare-command-name rule, shared by the read path (`normalizeBin`)
347
366
  * and the strict install gate (`invalidPluginBinReasons`): a name a shell can
348
367
  * invoke verbatim and a filesystem can hold as a single symlink entry — no
@@ -761,6 +780,7 @@ function mergeConfig(partial) {
761
780
  const keybindings = normalizeKeybindings(partial.keybindings);
762
781
  const tmux_passthrough = normalizeTmuxPassthrough(partial.tmux_passthrough);
763
782
  const modelLadders = mergeModelLadders(partial.modelLadders);
783
+ const providerOptions = mergeProviderOptions(partial.providerOptions, defaults.providerOptions);
764
784
  const routes = resolveModelRoutes(mergeModelRoutePatches(partial.modelRoutes));
765
785
  const modelRoutes = routes.complete;
766
786
  const modelRouting = mergeModelRouting(partial.modelRouting, undefined);
@@ -769,7 +789,7 @@ function mergeConfig(partial) {
769
789
  const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
770
790
  const bin = normalizeBin(partial.bin);
771
791
  const humanActions = normalizeHumanActions(partial.humanActions);
772
- return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, lifecycle, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, keybindings, tmux_passthrough, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
792
+ return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, lifecycle, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, keybindings, tmux_passthrough, modelLadders, providerOptions, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
773
793
  }
774
794
  /** Raw (un-defaulted) partial config for one scope, or null if the scope has
775
795
  * no root or no config.json. Used by `readMergedLaunchConfig` to layer
@@ -858,12 +878,14 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
858
878
  // (ladders/routes/spawnEnv) have no plugin channel.
859
879
  let kinds = mergeKinds(userRaw?.kinds, layerPluginKinds('user', scopeRoot('user'), defaults.kinds));
860
880
  let modelLadders = mergeModelLadders(userRaw?.modelLadders, defaults.modelLadders);
881
+ let providerOptions = mergeProviderOptions(userRaw?.providerOptions, defaults.providerOptions);
861
882
  let modelRoutePatches = mergeModelRoutePatches(userRaw?.modelRoutes);
862
883
  let modelRouting = mergeModelRouting(userRaw?.modelRouting, undefined);
863
884
  let spawnEnv = mergeSpawnEnv(userRaw?.spawnEnv, defaults.spawnEnv?.allow);
864
885
  if (profileRaw !== null) {
865
886
  kinds = mergeKinds(profileRaw.kinds, kinds);
866
887
  modelLadders = mergeModelLadders(profileRaw.modelLadders, modelLadders);
888
+ providerOptions = mergeProviderOptions(profileRaw.providerOptions, providerOptions);
867
889
  modelRoutePatches = mergeModelRoutePatches(profileRaw.modelRoutes, modelRoutePatches);
868
890
  modelRouting = mergeModelRouting(profileRaw.modelRouting, modelRouting);
869
891
  spawnEnv = mergeSpawnEnv(profileRaw.spawnEnv, spawnEnv.allow);
@@ -873,6 +895,7 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
873
895
  if (raw === null)
874
896
  continue;
875
897
  modelLadders = mergeModelLadders(raw.modelLadders, modelLadders);
898
+ providerOptions = mergeProviderOptions(raw.providerOptions, providerOptions);
876
899
  modelRoutePatches = mergeModelRoutePatches(raw.modelRoutes, modelRoutePatches);
877
900
  modelRouting = mergeModelRouting(raw.modelRouting, modelRouting);
878
901
  spawnEnv = mergeSpawnEnv(raw.spawnEnv, spawnEnv.allow);
@@ -881,6 +904,7 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
881
904
  return {
882
905
  kinds,
883
906
  modelLadders,
907
+ providerOptions,
884
908
  ...(Object.keys(routes.complete).length > 0 ? { modelRoutes: routes.complete } : {}),
885
909
  ...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}),
886
910
  ...(modelRouting !== undefined ? { modelRouting } : {}),
@@ -4,6 +4,18 @@
4
4
  * work" apart from "my child is queued behind someone else's work" without
5
5
  * any new bookkeeping. */
6
6
  export declare function exclusiveLockOwnerPid(path: string): number | null;
7
+ /**
8
+ * Block until `path` is no longer held by a LIVE other process, or the wait
9
+ * runs out. Unlike `withExclusiveDirectoryLock*` this never CLAIMS the lock —
10
+ * it is for a reader that only needs the holder's transition to be over before
11
+ * it looks at the directory the holder is rewriting.
12
+ *
13
+ * Returns immediately when the lock is free, when its marker names this very
14
+ * process (a caller waiting on its own lock would otherwise deadlock), or when
15
+ * the named owner is gone — a dead holder's transition is already over, however
16
+ * it ended.
17
+ */
18
+ export declare function awaitExclusiveLockRelease(path: string, timeoutMs: number): void;
7
19
  export interface ExclusiveLockOptions {
8
20
  /** Grace before a lock that names no owner at all is cleared. */
9
21
  staleMs?: number;
@@ -103,6 +103,35 @@ export function exclusiveLockOwnerPid(path) {
103
103
  const pid = Number(token.split('.', 1)[0]);
104
104
  return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
105
105
  }
106
+ /**
107
+ * Block until `path` is no longer held by a LIVE other process, or the wait
108
+ * runs out. Unlike `withExclusiveDirectoryLock*` this never CLAIMS the lock —
109
+ * it is for a reader that only needs the holder's transition to be over before
110
+ * it looks at the directory the holder is rewriting.
111
+ *
112
+ * Returns immediately when the lock is free, when its marker names this very
113
+ * process (a caller waiting on its own lock would otherwise deadlock), or when
114
+ * the named owner is gone — a dead holder's transition is already over, however
115
+ * it ended.
116
+ */
117
+ export function awaitExclusiveLockRelease(path, timeoutMs) {
118
+ const deadline = Date.now() + timeoutMs;
119
+ let pollMs = POLL_MS;
120
+ for (;;) {
121
+ const token = observedMarkerToken(path);
122
+ if (token === null)
123
+ return;
124
+ if (Number(token.split('.', 1)[0]) === process.pid)
125
+ return;
126
+ if (!ownerIsAlive(token))
127
+ return;
128
+ const remaining = deadline - Date.now();
129
+ if (remaining <= 0)
130
+ return;
131
+ pause(Math.min(pollMs, remaining));
132
+ pollMs = nextPollMs(pollMs);
133
+ }
134
+ }
106
135
  function tryAcquire(path, staleMs) {
107
136
  const token = `${process.pid}.${randomUUID()}`;
108
137
  try {
@@ -9,6 +9,14 @@ export interface PushOpts {
9
9
  from?: string;
10
10
  /** Attribution snapshotted by the human-ticket settlement paths; absent means an ordinary report. */
11
11
  disposition?: InboxDisposition;
12
+ /** Final only. The outcome the canonical final records; absent means `result`/`finalized`. */
13
+ outcome?: FinalOutcome;
14
+ }
15
+ /** A declined structured result: the node finishes, but its latch records a failure. */
16
+ export interface FinalOutcome {
17
+ kind: 'failure';
18
+ reason: 'declined';
19
+ detailJson: string | null;
12
20
  }
13
21
  export interface PushResult {
14
22
  reportPath: string;
@@ -2,7 +2,7 @@
2
2
  import { existsSync, linkSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
- import { openDb, reportsDir, subscribersOf, cancelCronsOnWake, getNode } from '../canvas/index.js';
5
+ import { openDb, reportsDir, subscribersOf, cancelCronsOnWake, composeOutcomeDeliveryDocument, getNode, readOutcomeDelivery, resetOutcomeDeliveryForSupersede } from '../canvas/index.js';
6
6
  import { appendInbox } from './inbox.js';
7
7
  import { appendPassive } from './passive.js';
8
8
  import { operationIdContext } from '../events/operation-id.js';
@@ -77,7 +77,7 @@ function deliver(subscribers, from, fromName, kind, requestedTier, reportRef, bo
77
77
  return { deliveredTo, failedTo };
78
78
  }
79
79
  /** Claim the one canonical final while holding SQLite's write lock. */
80
- function commitCanonicalFinal(nodeId, staged) {
80
+ function commitCanonicalFinal(nodeId, staged, outcome) {
81
81
  const db = openDb();
82
82
  let installed = false;
83
83
  db.exec('BEGIN IMMEDIATE');
@@ -92,9 +92,28 @@ function commitCanonicalFinal(nodeId, staged) {
92
92
  if (row.status !== 'active' && row.status !== 'idle' && row.status !== 'done')
93
93
  throw new FinalizationError('finalization_forbidden');
94
94
  const subscribers = db.prepare(`SELECT from_id AS node_id, active, created FROM edges WHERE type = 'subscribes_to' AND to_id = ? ORDER BY created`).all(nodeId).map((row) => ({ node_id: row['node_id'], active: row['active'] === 1, created: row['created'] }));
95
+ const delivery = readOutcomeDelivery(nodeId);
96
+ const node = getNode(nodeId);
97
+ const recordsOutcome = node?.kind !== 'human';
98
+ const revision = (node?.outcome_revision ?? 0) + 1;
99
+ const outcomeKind = outcome?.kind ?? 'result';
100
+ const outcomeReason = outcome?.reason ?? 'finalized';
101
+ const outcomeDetailJson = outcome?.detailJson ?? null;
102
+ const documentJson = !recordsOutcome || delivery === null || node === null ? null : composeOutcomeDeliveryDocument({
103
+ nodeId,
104
+ outcome: { kind: outcomeKind, reason: outcomeReason, at: staged.ts, revision, detailJson: outcomeDetailJson },
105
+ action: { name: delivery.actionName, payload: delivery.payload },
106
+ finalReport: staged.basename,
107
+ });
95
108
  install(staged);
96
109
  installed = true;
97
- db.prepare("UPDATE nodes SET status = 'done', intent = 'done', terminal_reason = 'finalized', final_report = ?, finalized_at = ? WHERE node_id = ? AND final_report IS NULL").run(staged.basename, staged.ts, nodeId);
110
+ if (recordsOutcome) {
111
+ db.prepare("UPDATE nodes SET status = 'done', intent = 'done', terminal_reason = ?, final_report = ?, finalized_at = ?, outcome_kind = ?, outcome_reason = ?, outcome_at = ?, outcome_revision = outcome_revision + 1, outcome_detail_json = ? WHERE node_id = ? AND final_report IS NULL").run(outcomeReason, staged.basename, staged.ts, outcomeKind, outcomeReason, staged.ts, outcomeDetailJson, nodeId);
112
+ resetOutcomeDeliveryForSupersede(nodeId, documentJson, Date.now());
113
+ }
114
+ else {
115
+ db.prepare("UPDATE nodes SET status = 'done', intent = 'done', terminal_reason = 'finalized', final_report = ?, finalized_at = ? WHERE node_id = ? AND final_report IS NULL").run(staged.basename, staged.ts, nodeId);
116
+ }
98
117
  db.exec('COMMIT');
99
118
  return { subscribers };
100
119
  }
@@ -113,11 +132,13 @@ async function pushWithContext(nodeId, opts) {
113
132
  throw new Error(`push: empty nodeId (kind=${opts.kind}) — a report must belong to a node`);
114
133
  if (opts.kind === 'final' && opts.deliveryTier !== undefined)
115
134
  throw new Error('push final does not accept a delivery tier');
135
+ if (opts.kind !== 'final' && opts.outcome !== undefined)
136
+ throw new Error('push outcome is final-only');
116
137
  const staged = stageReport(nodeId, opts.kind, opts.body);
117
138
  const from = opts.from ?? nodeId;
118
139
  const fromName = getNode(from)?.name;
119
140
  if (opts.kind === 'final') {
120
- const { subscribers } = commitCanonicalFinal(nodeId, staged);
141
+ const { subscribers } = commitCanonicalFinal(nodeId, staged, opts.outcome);
121
142
  // The finalization latch ends every wait this node was holding. It flips the
122
143
  // row to done DIRECTLY (not through lifecycle's `finish` event), so the
123
144
  // deadline cleanup must ride here too: an armed deadline outliving the final
@@ -87,6 +87,11 @@ export declare function writeCursor(nodeId: string, entryId: string): void;
87
87
  * (do not park, do not shut down, do not resume quietly), so report it and
88
88
  * return that. */
89
89
  export declare function hasUnseenWakeMail(nodeId: string): boolean;
90
+ /** Whether wake-capable mail arrived at or after a parking turn began. Unlike
91
+ * `hasUnseenWakeMail`, this deliberately includes mail the main engine already
92
+ * consumed: a shadow completion must not undo activity that occurred while it
93
+ * ran. A failed scan conservatively keeps the node live. */
94
+ export declare function hasWakeMailSince(nodeId: string, sinceMs: number): boolean;
90
95
  /** Clip a body to a bounded preview, reporting whether anything was dropped. */
91
96
  export declare function clipBody(body: string): {
92
97
  text: string;
@@ -315,6 +315,19 @@ export function hasUnseenWakeMail(nodeId) {
315
315
  return true;
316
316
  }
317
317
  }
318
+ /** Whether wake-capable mail arrived at or after a parking turn began. Unlike
319
+ * `hasUnseenWakeMail`, this deliberately includes mail the main engine already
320
+ * consumed: a shadow completion must not undo activity that occurred while it
321
+ * ran. A failed scan conservatively keeps the node live. */
322
+ export function hasWakeMailSince(nodeId, sinceMs) {
323
+ try {
324
+ return readPhysicalInbox(nodeId).some((entry) => entry.tier !== 'deferred' && Date.parse(entry.ts) >= sinceMs);
325
+ }
326
+ catch (error) {
327
+ emitEvent({ level: 'warn', event: 'inbox.wake_scan.failed', node_id: nodeId, error });
328
+ return true;
329
+ }
330
+ }
318
331
  // Coalesce
319
332
  /** Bounds for inlining a message body's preview in the digest. */
320
333
  const BODY_MAX_LINES = 12;
@@ -11,15 +11,46 @@ import { join } from 'node:path';
11
11
  import { CONFIG_FILE } from '../types.js';
12
12
  import { listDirs, pathExists, readJsonIfExists } from './fs-utils.js';
13
13
  import { readPluginManifest } from './manifest.js';
14
+ import { awaitBundleSwap, bundleSwapInFlightElsewhere } from './plugin-swap-lock.js';
14
15
  function pluginConfigForRoot(root) {
15
16
  const cfg = readJsonIfExists(join(root, CONFIG_FILE));
16
17
  return cfg && cfg.plugins && typeof cfg.plugins === 'object' ? cfg.plugins : {};
17
18
  }
19
+ /**
20
+ * A bundle-plugin swap replaces `<plugins>/<name>` with two renames, so there
21
+ * is a window in which the config names a plugin whose directory is not there.
22
+ * A list taken inside that window would drop a plugin that is installed and
23
+ * about to be present again — the caller would build a command tree missing it.
24
+ *
25
+ * That exact signal — named in config, absent on disk — is what we re-check,
26
+ * and only when another process's swap lock says a swap is actually running: a
27
+ * genuinely deleted directory holds no lock and is reported as gone at once.
28
+ * Every normal invocation, where each config-named plugin has its directory,
29
+ * pays one Set lookup per name and nothing else.
30
+ */
31
+ function swapsToWaitOut(scopeRootPath, cfg, present) {
32
+ const names = Object.keys(cfg);
33
+ if (names.every((name) => present.has(name)))
34
+ return [];
35
+ return names.filter((name) => !present.has(name) && bundleSwapInFlightElsewhere(scopeRootPath, name));
36
+ }
18
37
  export function listInstalledPluginsInRoot(scope, scopeRootPath) {
19
38
  const dir = join(scopeRootPath, 'plugins');
20
39
  if (!pathExists(dir))
21
40
  return [];
22
41
  const cfg = pluginConfigForRoot(scopeRootPath);
42
+ const first = collectInstalledPlugins(scope, dir, cfg);
43
+ const waiting = swapsToWaitOut(scopeRootPath, cfg, new Set(first.map((plugin) => plugin.name)));
44
+ if (waiting.length === 0)
45
+ return first;
46
+ for (const name of waiting)
47
+ awaitBundleSwap(scopeRootPath, name);
48
+ // One re-list, never a loop: the second read is taken after every swap we
49
+ // observed has finished. A swap that starts after it is a package this
50
+ // invocation was never going to see anyway.
51
+ return collectInstalledPlugins(scope, dir, cfg);
52
+ }
53
+ function collectInstalledPlugins(scope, dir, cfg) {
23
54
  const out = [];
24
55
  for (const name of listDirs(dir)) {
25
56
  const root = join(dir, name);
@@ -1,6 +1,6 @@
1
1
  import type { BindingDefinition, SafetyRequirement } from './types.js';
2
2
  import type { AttachBindingId, AttachMenuBindingId } from './attach-control.js';
3
- type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'fold-tools' | 'metadata' | 'promote' | 'demote-mode' | 'resume' | 'fork' | 'demote' | 'make-resident' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'context-admin' | 'providers' | 'plugins' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
3
+ type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'provider-options' | 'fold-tools' | 'metadata' | 'promote' | 'demote-mode' | 'resume' | 'fork' | 'demote' | 'make-resident' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'context-admin' | 'providers' | 'plugins' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
4
4
  type TmuxMenuEntryAction = Exclude<TmuxMenuAction, 'open' | 'attach'> | `issue.${'idea' | 'bug'}`;
5
5
  type GraphAction = 'down' | 'up' | 'first' | 'last' | 'collapse-or-manager' | 'expand-or-child' | 'focus' | 'revive' | 'focus-manager' | 'close-subtree' | 'dismiss';
6
6
  type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4 | 5}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'profile' | 'residents' | 'preview' | 'report' | 'pin' | 'search' | 'revive-now' | 'focus' | 'focus-background' | 'yank-id';
@@ -55,6 +55,9 @@ export const BINDING_CATALOG = Object.freeze([
55
55
  ...menuEntry('metadata', 'Show metadata', 'Show the focused node’s metadata dossier.', ['m'], ['tmux.menu.show']),
56
56
  ...menuEntry('context', 'Browse context', 'Browse the focused node context.', ['c'], ['tmux.menu.show']),
57
57
  ...menuEntry('providers', 'Open settings', 'Open the crouter settings panel.', [','], ['tmux.menu']),
58
+ // Next to the settings comma: the same page, opened on its provider-options
59
+ // destination.
60
+ ...menuEntry('provider-options', 'Provider options', 'Open provider options — how an authenticated provider serves your turns, including Codex fast mode.', ['.'], ['tmux.menu']),
58
61
  // j and k steer the menu cursor, so no selector may claim either.
59
62
  ...menuEntry('plugins', 'Browse plugins', 'Open the crouter plugin browser.', ['l'], ['tmux.menu']),
60
63
  // Lowercase c opens the create submenu, so the context surface takes the
@@ -0,0 +1,15 @@
1
+ import type { RootDef } from './command.js';
2
+ /**
3
+ * Run `argv` against `root`, and if the backend answers `manifest_stale`,
4
+ * refetch that plugin's package and run the ORIGINAL argv again against a
5
+ * freshly resolved tree — exactly once.
6
+ *
7
+ * Re-parsing the original argv, rather than re-running the leaf that failed, is
8
+ * what makes a RENAMED verb recoverable: the old leaf no longer exists in the
9
+ * refreshed tree, and only the dispatcher can answer that against the fresh
10
+ * tree's own siblings.
11
+ *
12
+ * Never throws: every outcome is rendered through the dispatcher's own
13
+ * `handle`, so a caller can mark the end of dispatch unconditionally.
14
+ */
15
+ export declare function dispatchWithStaleRecovery(root: RootDef, argv: string[]): Promise<void>;
@@ -0,0 +1,72 @@
1
+ // Dispatch with recovery from an out-of-date command description.
2
+ //
3
+ // Extracted from the cli entry so the whole behaviour — rethrow, refetch,
4
+ // re-parse, exactly-once — can be driven by a test through `runCli` and a real
5
+ // error off the wire, rather than only by launching the binary.
6
+ import { resolveRoot } from '../build-root.js';
7
+ import { runCli } from './command.js';
8
+ import { forceRevalidateBundlePlugin } from './command-plugins/revalidate.js';
9
+ import { handle } from './io.js';
10
+ import { staleManifestPlugin } from './manifest-stale.js';
11
+ /**
12
+ * Run `argv` against `root`, and if the backend answers `manifest_stale`,
13
+ * refetch that plugin's package and run the ORIGINAL argv again against a
14
+ * freshly resolved tree — exactly once.
15
+ *
16
+ * Re-parsing the original argv, rather than re-running the leaf that failed, is
17
+ * what makes a RENAMED verb recoverable: the old leaf no longer exists in the
18
+ * refreshed tree, and only the dispatcher can answer that against the fresh
19
+ * tree's own siblings.
20
+ *
21
+ * Never throws: every outcome is rendered through the dispatcher's own
22
+ * `handle`, so a caller can mark the end of dispatch unconditionally.
23
+ */
24
+ export async function dispatchWithStaleRecovery(root, argv) {
25
+ try {
26
+ // The dispatcher renders every other failure itself; a `manifest_stale`
27
+ // error is the one it rethrows, because recovery needs this frame.
28
+ await runCli(root, argv);
29
+ return;
30
+ }
31
+ catch (error) {
32
+ if (!(await refetchStalePlugin(error))) {
33
+ // Nothing newer to run against: report the backend's own complaint
34
+ // exactly as the dispatcher would have.
35
+ handle(error);
36
+ return;
37
+ }
38
+ try {
39
+ // The refreshed tree, re-resolved: the package this invocation dispatches
40
+ // against is not the one it was built from. Replaying the argv is only
41
+ // safe because a server that sets the flag has promised it — the two
42
+ // preconditions are recorded on `ManifestStaleEnvelope` in
43
+ // `src/api/plugin-manifest-schema.ts`, and crtr cannot check either one.
44
+ await runCli(await resolveRoot(argv[2]), argv);
45
+ }
46
+ catch (retryError) {
47
+ // Exactly once. A second `manifest_stale` against a package just
48
+ // refetched is the server's answer, not another refresh cycle.
49
+ handle(retryError);
50
+ }
51
+ }
52
+ }
53
+ /**
54
+ * A backend that answers `manifest_stale` is saying this client invoked from an
55
+ * out-of-date description of its commands. Refetch that plugin's package and
56
+ * report whether it actually changed. An unchanged package means the operation
57
+ * is genuinely gone, so the server's own error, which already names what it
58
+ * does serve, is the honest answer and stands.
59
+ */
60
+ async function refetchStalePlugin(error) {
61
+ const plugin = staleManifestPlugin(error);
62
+ if (plugin === undefined)
63
+ return false;
64
+ try {
65
+ return await forceRevalidateBundlePlugin(plugin);
66
+ }
67
+ catch {
68
+ // The refetch is the recovery. If it fails there is nothing newer to retry
69
+ // against, and the server's original complaint is what the caller needs.
70
+ return false;
71
+ }
72
+ }
@@ -0,0 +1,18 @@
1
+ import type { ManifestStaleEnvelope } from '../api/plugin-manifest-schema.js';
2
+ /** What crtr attaches to the `CrtrError` it raises for a stale answer. */
3
+ type StaleErrorDetails = Required<ManifestStaleEnvelope> & {
4
+ plugin: string;
5
+ };
6
+ /** Did the backend's own error envelope claim staleness? Reads the parsed JSON
7
+ * body, before it becomes a `CrtrError`. */
8
+ export declare function envelopeClaimsStale(envelope: Record<string, unknown>): boolean;
9
+ /** The details a transport adds when it converts a stale envelope into the
10
+ * error the dispatcher rethrows. */
11
+ export declare function staleErrorDetails(plugin: string): StaleErrorDetails;
12
+ /** Is this the one error class the dispatcher rethrows instead of rendering? */
13
+ export declare function isManifestStaleError(error: unknown): boolean;
14
+ /** The plugin to refetch, or `undefined` when this error is not a recoverable
15
+ * stale answer. A stale flag with no plugin name is not actionable: nothing
16
+ * identifies the package whose description went out of date. */
17
+ export declare function staleManifestPlugin(error: unknown): string | undefined;
18
+ export {};
@@ -0,0 +1,39 @@
1
+ // The one place in crtr that names the `manifest_stale` wire field.
2
+ //
3
+ // The field itself is declared in the published format subpath
4
+ // (`src/api/plugin-manifest-schema.ts`), which is what a plugin backend compiles
5
+ // against. This module binds crtr's readers and writer to that declaration, so
6
+ // renaming the field there fails the build here rather than silently splitting
7
+ // the two sides: a backend that keeps setting a field nobody reads, and a client
8
+ // that never recovers.
9
+ import { CrtrError } from './errors.js';
10
+ /** Bound to the published declaration: `keyof` is the field's literal type, so
11
+ * a rename over there makes this assignment a type error. */
12
+ const STALE_FIELD = 'manifest_stale';
13
+ /** The plugin whose package produced the out-of-date description, carried
14
+ * alongside the flag so the recovery frame knows what to refetch. The backend
15
+ * does not send this — the transport that made the call names it. */
16
+ const PLUGIN_FIELD = 'plugin';
17
+ /** Did the backend's own error envelope claim staleness? Reads the parsed JSON
18
+ * body, before it becomes a `CrtrError`. */
19
+ export function envelopeClaimsStale(envelope) {
20
+ return envelope[STALE_FIELD] === true;
21
+ }
22
+ /** The details a transport adds when it converts a stale envelope into the
23
+ * error the dispatcher rethrows. */
24
+ export function staleErrorDetails(plugin) {
25
+ return { [STALE_FIELD]: true, [PLUGIN_FIELD]: plugin };
26
+ }
27
+ /** Is this the one error class the dispatcher rethrows instead of rendering? */
28
+ export function isManifestStaleError(error) {
29
+ return error instanceof CrtrError && error.details?.[STALE_FIELD] === true;
30
+ }
31
+ /** The plugin to refetch, or `undefined` when this error is not a recoverable
32
+ * stale answer. A stale flag with no plugin name is not actionable: nothing
33
+ * identifies the package whose description went out of date. */
34
+ export function staleManifestPlugin(error) {
35
+ if (!isManifestStaleError(error))
36
+ return undefined;
37
+ const plugin = error.details?.[PLUGIN_FIELD];
38
+ return typeof plugin === 'string' ? plugin : undefined;
39
+ }
@@ -0,0 +1,9 @@
1
+ /** Under the scope root's `tmp/`, alongside the staging and `.prev` dirs the
2
+ * swap itself uses, so one directory holds the whole transition. */
3
+ export declare function bundleSwapLockPath(scopeRootPath: string, name: string): string;
4
+ /** Whether ANOTHER live process currently holds this plugin's swap lock. Our
5
+ * own lock does not count: the swapper reads the plugin list from inside its
6
+ * own critical section. */
7
+ export declare function bundleSwapInFlightElsewhere(scopeRootPath: string, name: string): boolean;
8
+ /** Wait out another process's swap of this plugin, bounded. */
9
+ export declare function awaitBundleSwap(scopeRootPath: string, name: string): void;
@@ -0,0 +1,31 @@
1
+ // The one name for the lock a bundle-plugin package swap is held under, and the
2
+ // two questions a reader asks about it.
3
+ //
4
+ // Two modules need it and neither may own it: the swapper lives in
5
+ // `commands/pkg/plugin-manage.ts` (the whole command graph), the reader in
6
+ // `core/installed-plugins.ts` (deliberately leaf-safe: types, fs-utils,
7
+ // manifest). This module keeps both dependency-light — `node:path` plus the
8
+ // generic lock primitive, which itself imports only `node:fs` and
9
+ // `node:crypto`.
10
+ import { join } from 'node:path';
11
+ import { awaitExclusiveLockRelease, exclusiveLockOwnerPid } from './exclusive-lock.js';
12
+ /** How long a reader waits for another process's swap to finish before it
13
+ * reports what it can see. A swap is one rename of an already-staged, already-
14
+ * validated tree, so a wait this long means the holder is wedged, not slow. */
15
+ const READER_WAIT_MS = 2_000;
16
+ /** Under the scope root's `tmp/`, alongside the staging and `.prev` dirs the
17
+ * swap itself uses, so one directory holds the whole transition. */
18
+ export function bundleSwapLockPath(scopeRootPath, name) {
19
+ return join(scopeRootPath, 'tmp', `${name}.swap.lock`);
20
+ }
21
+ /** Whether ANOTHER live process currently holds this plugin's swap lock. Our
22
+ * own lock does not count: the swapper reads the plugin list from inside its
23
+ * own critical section. */
24
+ export function bundleSwapInFlightElsewhere(scopeRootPath, name) {
25
+ const pid = exclusiveLockOwnerPid(bundleSwapLockPath(scopeRootPath, name));
26
+ return pid !== null && pid !== process.pid;
27
+ }
28
+ /** Wait out another process's swap of this plugin, bounded. */
29
+ export function awaitBundleSwap(scopeRootPath, name) {
30
+ awaitExclusiveLockRelease(bundleSwapLockPath(scopeRootPath, name), READER_WAIT_MS);
31
+ }
@@ -114,7 +114,9 @@ export declare function isProfilePaused(profileId: string | null | undefined): b
114
114
  export declare function assertProfileEntryActive(entry: ProfileEntry): void;
115
115
  export declare function assertProfileActive(profileId: string | null | undefined): ProfileEntry | null;
116
116
  export declare function pauseProfile(profileId: string): ProfileEntry;
117
- export declare function resumeProfile(profileId: string): ProfileEntry;
117
+ export declare function resumeProfile(profileId: string): ProfileEntry & {
118
+ pausedAt: string | null;
119
+ };
118
120
  export declare function setProfileDefaultKind(profileId: string, defaultKind: string): ProfileEntry;
119
121
  /** Merge `set` entries over the stored map and drop `unset` keys; the
120
122
  * `metadata` field is omitted entirely when the result is empty. */
@@ -592,7 +592,12 @@ export function pauseProfile(profileId) {
592
592
  return mutateManifest(profileId, (m) => ({ ...m, paused_at: m.paused_at ?? nowIso() }));
593
593
  }
594
594
  export function resumeProfile(profileId) {
595
- return mutateManifest(profileId, (m) => ({ ...m, paused_at: null }));
595
+ let pausedAt = null;
596
+ const entry = mutateManifest(profileId, (m) => {
597
+ pausedAt = m.paused_at;
598
+ return { ...m, paused_at: null };
599
+ });
600
+ return { ...entry, pausedAt };
596
601
  }
597
602
  export function setProfileDefaultKind(profileId, defaultKind) {
598
603
  return mutateManifest(profileId, (m) => ({ ...m, default_kind: defaultKind }));
@@ -1,8 +1,10 @@
1
- import type { BrokerExtensionStateDTO, BrokerGeneratedNameRequest, BrokerGeneratedNameResultDTO, BrokerInboxCursorDirective, BrokerInboxCursorRequest, BrokerModelCommitRequest, BrokerModelCommitResultDTO, BrokerPersonaAckRequest, BrokerPersonaAckResultDTO, BrokerSessionBoundRequest, BrokerSessionBoundResultDTO, BrokerSettleDirective, BrokerSettleRequest } from '../../../api/dto/broker-ops.js';
1
+ import type { BrokerExtensionStateDTO, BrokerGeneratedNameRequest, BrokerGeneratedNameResultDTO, BrokerInboxCursorDirective, BrokerInboxCursorRequest, BrokerModelCommitRequest, BrokerModelCommitResultDTO, BrokerParkActivityResultDTO, BrokerParkCompleteRequest, BrokerPersonaAckRequest, BrokerPersonaAckResultDTO, BrokerSessionBoundRequest, BrokerSessionBoundResultDTO, BrokerSettleDirective, BrokerSettleRequest } from '../../../api/dto/broker-ops.js';
2
2
  import type { RelaunchRootResultDTO } from '../../../api/dto/lifecycle.js';
3
3
  import type { PushReportRequest } from '../../../api/dto/reports.js';
4
4
  export declare function bindBrokerSession(nodeId: string, request: BrokerSessionBoundRequest): Promise<BrokerSessionBoundResultDTO>;
5
5
  export declare function settleBroker(nodeId: string, request: BrokerSettleRequest): Promise<BrokerSettleDirective>;
6
+ export declare function completeBrokerPark(nodeId: string, request: BrokerParkCompleteRequest): Promise<BrokerSettleDirective>;
7
+ export declare function recordBrokerParkActivity(nodeId: string): Promise<BrokerParkActivityResultDTO>;
6
8
  export declare function advanceBrokerInboxCursor(nodeId: string, request: BrokerInboxCursorRequest): Promise<BrokerInboxCursorDirective>;
7
9
  /** Fixed canvas projection for broker extension rendering. It deliberately
8
10
  * exposes no generic node/database facade. */
@@ -12,6 +12,12 @@ export function bindBrokerSession(nodeId, request) {
12
12
  export function settleBroker(nodeId, request) {
13
13
  return daemonClient().settleBroker(nodeId, request);
14
14
  }
15
+ export function completeBrokerPark(nodeId, request) {
16
+ return daemonClient().completeBrokerPark(nodeId, request);
17
+ }
18
+ export function recordBrokerParkActivity(nodeId) {
19
+ return daemonClient().recordBrokerParkActivity(nodeId);
20
+ }
15
21
  export function advanceBrokerInboxCursor(nodeId, request) {
16
22
  return daemonClient().advanceBrokerInboxCursor(nodeId, request);
17
23
  }
@@ -10,9 +10,7 @@ import type { RebindDriver } from './rebind.js';
10
10
  import type { ToolGroupTracker } from './tool-groups.js';
11
11
  import type { PromptDispatchTracker } from './turn-ignition.js';
12
12
  import type { TurnAdmissionGate } from './turn-admission.js';
13
- /** The frames that drive the live engine. A daemon delivery carries its
14
- * provenance through admission because only the resolved route can decide
15
- * whether the resulting run is internal. */
13
+ /** The frames that drive the live engine. */
16
14
  type DeliveredInput = {
17
15
  delivery?: {
18
16
  origin?: MessageOrigin;
@@ -25,6 +23,7 @@ export type DriveFrame = (Extract<ClientToBroker, {
25
23
  }>;
26
24
  export type EngineDriver = (client: BrokerClient, frame: DriveFrame) => Promise<unknown> | undefined;
27
25
  export declare function createEngineDriver(deps: {
26
+ nodeId: string;
28
27
  registry: BrokerClientRegistry;
29
28
  replies: FrameReplies;
30
29
  session: () => BrokerSession;