@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
@@ -1,12 +1,14 @@
1
1
  import { emitEvent } from '../../events/emit.js';
2
2
  import { operationIdContext } from '../../events/operation-id.js';
3
+ import { parkSummaryGraceMs } from '../../../shared/env.js';
4
+ import { headlessTurn } from '../headless-pi.js';
5
+ import { completeBrokerPark, recordBrokerParkActivity } from './daemon-ops.js';
3
6
  import { promptWithAdvertisedCommandInvocation } from '../advertised-command-invocation.js';
4
7
  import { heldDeferredInboxPromptJoin } from './held-deferred-inbox.js';
5
8
  import { announceMessageOrigin } from '../stamp/channel.js';
6
- import { clearTurnVisibility, currentTurnVisibility, setTurnVisibility, } from '../turn-visibility.js';
7
9
  import { chooseGuidanceDeliveryMode, promptWithNextTurnGuidance, resolveEngineRoute, resolveSteerCall, } from './engine-routing.js';
8
10
  export function createEngineDriver(deps) {
9
- const { registry, replies, session, dispatches, memoryRefs, authReloadGate, rebind, projection, toolGroups, turnAdmission, } = deps;
11
+ const { nodeId, registry, replies, session, dispatches, memoryRefs, authReloadGate, rebind, projection, toolGroups, turnAdmission, } = deps;
10
12
  const { ackTo, engineErrorTo } = replies;
11
13
  const errorRelay = (client) => {
12
14
  let failure;
@@ -36,32 +38,62 @@ export function createEngineDriver(deps) {
36
38
  error: err,
37
39
  });
38
40
  });
41
+ let parkingSummaryInFlight = false;
42
+ const recordParkActivityBeforeInput = async () => {
43
+ if (!parkingSummaryInFlight)
44
+ return;
45
+ const result = await recordBrokerParkActivity(nodeId);
46
+ if (result.activity !== 'recorded') {
47
+ throw new Error('parking completion took priority before input admission');
48
+ }
49
+ };
50
+ const runParkTurn = (text) => {
51
+ parkingSummaryInFlight = true;
52
+ const liveSession = session();
53
+ const model = liveSession.model;
54
+ const source = liveSession.sessionManager;
55
+ const run = model === undefined || typeof model.provider !== 'string' || typeof model.id !== 'string'
56
+ ? Promise.resolve('failed')
57
+ : headlessTurn({
58
+ systemPrompt: liveSession.systemPrompt,
59
+ userPrompt: text,
60
+ modelSpec: `${model.provider}/${model.id}`,
61
+ timeoutMs: parkSummaryGraceMs(),
62
+ seedMessages: source.buildSessionContext().messages,
63
+ sessionId: source.getSessionId(),
64
+ toolPolicy: 'builtin',
65
+ });
66
+ void run
67
+ .catch(() => 'failed')
68
+ .then((outcome) => completeBrokerPark(nodeId, { outcome }))
69
+ .then((directive) => {
70
+ if (directive.action === 'shutdown')
71
+ rebind.requestShutdown();
72
+ })
73
+ .catch((error) => {
74
+ emitEvent({ level: 'error', event: 'broker.park.complete_failed', error });
75
+ })
76
+ .finally(() => {
77
+ parkingSummaryInFlight = false;
78
+ });
79
+ };
39
80
  const drivePromptOrFollowUp = (client, frame, errors) => {
40
81
  const relayError = errors.relay;
41
82
  // Settle on the engine call's outcome: the failure is set by relayError
42
83
  // before the caught promise resolves, so reading it after the chain is
43
84
  // the call's verdict.
44
85
  const settle = (p) => p.then(() => errors.failure());
45
- const dispatch = () => turnAdmission.admit((release) => {
86
+ const drive = (release) => {
46
87
  // A prior prompt can be in asynchronous preflight while isStreaming is
47
88
  // false. Admission waits for that preflight, then routes against the
48
89
  // state it actually established instead of its stale snapshot.
49
90
  const route = resolveEngineRoute(frame, session().isStreaming);
50
91
  const promptOptions = route.call === 'prompt' ? route.options : undefined;
51
92
  const guidance = memoryRefs.guidanceFor(frame.text);
52
- const deliveryIsInternal = frame.delivery?.origin?.type === 'runtime'
53
- && frame.delivery.origin.kind === 'park'
54
- && route.call === 'prompt'
55
- && route.options.streamingBehavior === undefined;
56
93
  const announceDelivery = () => {
57
- if (frame.delivery === undefined)
94
+ if (frame.delivery?.origin === undefined)
58
95
  return;
59
- const visibility = deliveryIsInternal ? 'internal' : undefined;
60
- if (frame.delivery.origin === undefined) {
61
- setTurnVisibility('visible');
62
- return;
63
- }
64
- announceMessageOrigin(frame.text, frame.delivery.origin, visibility);
96
+ announceMessageOrigin(frame.text, frame.delivery.origin);
65
97
  };
66
98
  const prompt = async () => {
67
99
  // Both human surfaces converge at this admitted boundary. The watcher
@@ -77,18 +109,7 @@ export function createEngineDriver(deps) {
77
109
  }
78
110
  }
79
111
  announceDelivery();
80
- registry.broadcast({
81
- type: 'turn_visibility',
82
- visibility: currentTurnVisibility(),
83
- });
84
- try {
85
- await session().prompt(frame.text, { ...promptOptions, preflightResult: release });
86
- }
87
- catch (error) {
88
- if (deliveryIsInternal)
89
- clearTurnVisibility();
90
- throw error;
91
- }
112
+ await session().prompt(frame.text, { ...promptOptions, preflightResult: release });
92
113
  };
93
114
  if (guidance === null) {
94
115
  if (route.call === 'prompt') {
@@ -125,6 +146,19 @@ export function createEngineDriver(deps) {
125
146
  .followUp(frame.text, frame.images)
126
147
  .catch(relayError)
127
148
  .then(() => sendGuidance(guidance, deliverAs)));
149
+ };
150
+ const dispatch = () => turnAdmission.admit((release) => {
151
+ // Do not make the normal path async: prompt admission and auth reload
152
+ // ordering depend on Pi seeing this call in the current stack.
153
+ if (!parkingSummaryInFlight)
154
+ return drive(release);
155
+ // This operation is the input-admission half of an in-flight parking
156
+ // summary: it completes before Pi receives the prompt, so completion
157
+ // cannot tear down a viewer or live delivery that won admission.
158
+ return recordParkActivityBeforeInput().then(() => drive(release), (error) => {
159
+ relayError(error);
160
+ return errors.failure();
161
+ });
128
162
  });
129
163
  // /login acknowledges before its asynchronous local refresh completes.
130
164
  // An idle turn must not overtake that refresh and launch on the stale
@@ -180,7 +214,7 @@ export function createEngineDriver(deps) {
180
214
  */
181
215
  const driveSteer = (frame, errors) => {
182
216
  const relayError = errors.relay;
183
- void turnAdmission.admit((release) => {
217
+ const drive = (release) => {
184
218
  if (resolveSteerCall(frame.text, session()) === 'prompt') {
185
219
  return promptWithAdvertisedCommandInvocation(session(), frame.text, () => session().prompt(frame.text, {
186
220
  images: frame.images,
@@ -196,6 +230,13 @@ export function createEngineDriver(deps) {
196
230
  return session()
197
231
  .steer(frame.text, frame.images)
198
232
  .then(() => sendGuidance(guidance, 'steer'));
233
+ };
234
+ void turnAdmission.admit((release) => {
235
+ if (!parkingSummaryInFlight)
236
+ return drive(release);
237
+ return recordParkActivityBeforeInput().then(() => drive(release), (error) => {
238
+ relayError(error);
239
+ });
199
240
  }).catch(relayError);
200
241
  };
201
242
  /**
@@ -285,6 +326,12 @@ export function createEngineDriver(deps) {
285
326
  * and returns `undefined`.
286
327
  */
287
328
  return (client, frame) => {
329
+ if ((frame.type === 'prompt' || frame.type === 'follow_up')
330
+ && frame.delivery?.origin?.type === 'runtime'
331
+ && frame.delivery.origin.kind === 'park') {
332
+ runParkTurn(frame.text);
333
+ return undefined;
334
+ }
288
335
  const errors = errorRelay(client);
289
336
  switch (frame.type) {
290
337
  case 'prompt':
@@ -38,6 +38,7 @@ export function createFrameDispatchContext(deps) {
38
38
  turnAdmission,
39
39
  });
40
40
  const driveEngine = createEngineDriver({
41
+ nodeId,
41
42
  registry,
42
43
  replies,
43
44
  session: currentSession,
@@ -104,8 +104,19 @@ export function buildSettingsData(id, session) {
104
104
  catch {
105
105
  /* loader without themes — viewer falls back to its local theme registry */
106
106
  }
107
+ const defaultProvider = manager.getDefaultProvider();
108
+ const defaultModelId = manager.getDefaultModel();
109
+ let availableDefaultModels = [];
110
+ try {
111
+ availableDefaultModels = session.modelRuntime.getAvailableSnapshot();
112
+ }
113
+ catch {
114
+ /* the snapshot touches auth config; degrade to none known available */
115
+ }
107
116
  const settings = {
108
117
  autoCompact: session.autoCompactionEnabled,
118
+ defaultModel: defaultProvider && defaultModelId ? `${defaultProvider}/${defaultModelId}` : 'not set',
119
+ availableDefaultModels,
109
120
  showImages: manager.getShowImages(),
110
121
  imageWidthCells: manager.getImageWidthCells(),
111
122
  autoResizeImages: manager.getImageAutoResize(),
@@ -117,6 +128,7 @@ export function buildSettingsData(id, session) {
117
128
  httpIdleTimeoutMs: manager.getHttpIdleTimeoutMs(),
118
129
  thinkingLevel: session.thinkingLevel,
119
130
  availableThinkingLevels: session.getAvailableThinkingLevels(),
131
+ modelThinkingLevels: manager.getAllModelThinkingLevels(),
120
132
  currentTheme: manager.getTheme() ?? '',
121
133
  terminalTheme: 'dark',
122
134
  availableThemes,
@@ -133,7 +145,9 @@ export function buildSettingsData(id, session) {
133
145
  clearOnShrink: manager.getClearOnShrink(),
134
146
  showTerminalProgress: manager.getShowTerminalProgress(),
135
147
  tuiMode: manager.getTuiMode(),
148
+ fullscreenExitOutput: manager.getFullscreenExitOutput(),
136
149
  fullscreenScrollbar: manager.getFullscreenScrollbar(),
150
+ fullscreenCopyOnSelect: manager.getFullscreenCopyOnSelect(),
137
151
  warnings: manager.getWarnings(),
138
152
  defaultProjectTrust: manager.getDefaultProjectTrust(),
139
153
  showCacheMissNotices: manager.getShowCacheMissNotices(),
@@ -6,7 +6,6 @@ import { operationIdContext } from '../../events/operation-id.js';
6
6
  import { installPersonaTransitionGate } from '../broker-persona-guidance.js';
7
7
  import { abortSessionFromExtension } from './extension-abort.js';
8
8
  import { clearFault, invalidateProviderRetryEpisode, readFault } from '../fault.js';
9
- import { clearTurnVisibility } from '../turn-visibility.js';
10
9
  const MAX_ADMITTED_REPLACEMENTS = 2;
11
10
  const REPLACEMENT_DEADLINE_MS = 30_000;
12
11
  const REPLACEMENT_DEADLINE_EXCEEDED = Symbol('replacement-deadline-exceeded');
@@ -303,12 +302,6 @@ export class RebindDriver {
303
302
  catch (err) {
304
303
  emitEvent({ level: 'debug', event: 'broker.runtime.diagnostic', fields: { message: `event relay threw: ${String(err)}` } });
305
304
  }
306
- finally {
307
- // Projection sees the settlement while the run is still internal; every
308
- // later input and output fails open to visible after this boundary.
309
- if (event.type === 'agent_settled')
310
- clearTurnVisibility();
311
- }
312
305
  }
313
306
  ackTo(client, op, ok = true, detail, id) {
314
307
  this.deps.registry.sendFrame(client, { type: 'ack', for: op, ok, ...(detail !== undefined ? { detail } : {}), ...(id !== undefined ? { id } : {}) });
@@ -402,11 +402,6 @@ export interface WelcomeFrame {
402
402
  * the other four learn nothing); the PAYLOAD is pi's — `Model`, exactly what
403
403
  * pi's own `set_model` response carries and what `snapshot.state.model` holds.
404
404
  * Undefined when the engine has none. */
405
- export interface TurnVisibilityFrame {
406
- type: 'turn_visibility';
407
- /** Presentation visibility of the run whose native events follow. */
408
- visibility: MessageVisibility;
409
- }
410
405
  export interface WorkingActivityFrame {
411
406
  type: 'working_activity';
412
407
  /** Full display label selected from the user's configured gerunds. */
@@ -652,7 +647,7 @@ export interface ToolGroupSummaryFrame {
652
647
  /** Everything the broker can send. Live `AgentSessionEvent`s are relayed
653
648
  * verbatim (the broker adds nothing); the broker's own control frames carry
654
649
  * non-colliding `type` discriminants. */
655
- export type BrokerToClient = WelcomeFrame | TurnVisibilityFrame | WorkingActivityFrame | ModelChangedFrame | ResourcesReloadedFrame | ErrorFrame | NodeNamedFrame | AckFrame | BrokerDataFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | ToolGroupSummaryFrame | AgentSessionEvent;
650
+ export type BrokerToClient = WelcomeFrame | WorkingActivityFrame | ModelChangedFrame | ResourcesReloadedFrame | ErrorFrame | NodeNamedFrame | AckFrame | BrokerDataFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | ToolGroupSummaryFrame | AgentSessionEvent;
656
651
  /** Encode one frame as a single newline-terminated JSON line. */
657
652
  export declare function encodeFrame(frame: ClientToBroker | BrokerToClient): string;
658
653
  /** Byte bounds for a {@link FrameDecoder} (C5). */
@@ -58,6 +58,13 @@ export interface FleetRegistry {
58
58
  /** Give back a slot claimed by `reserveSlot` and not consumed by a launch. */
59
59
  releaseReservation(nodeId: string): void;
60
60
  entries(): Iterable<[string, FleetEntry]>;
61
+ /** Mark this exact broker's pause interruption before directly sending it
62
+ * SIGTERM. An explicit pid is the identity-checked fallback when the daemon
63
+ * has not registered a handle yet. */
64
+ interruptForProfilePause(nodeId: string, pid?: number): void;
65
+ /** Whether the next dead-row policy pass must absorb this pause interruption
66
+ * before a later pass may recover the row normally. */
67
+ hasProfilePauseInterruption(nodeId: string): boolean;
61
68
  /** Drop an entry without policy (terminal exits + close paths). */
62
69
  forget(nodeId: string): void;
63
70
  /** Wired by `headlessBrokerHost.launch` to the handle's `exited` promise.
@@ -1,4 +1,5 @@
1
1
  import type { ConstrainedSamplingConfig } from '@earendil-works/pi-ai';
2
+ import type { AgentMessage } from '@earendil-works/pi-agent-core';
2
3
  import type { TSchema } from 'typebox';
3
4
  export interface HeadlessTool {
4
5
  name: string;
@@ -15,6 +16,12 @@ export interface HeadlessRequest {
15
16
  modelIntent?: string;
16
17
  timeoutMs: number;
17
18
  tool?: HeadlessTool;
19
+ /** Exact context to seed into an isolated in-memory session. */
20
+ seedMessages?: readonly AgentMessage[];
21
+ /** Reuses a provider cache identity without binding or persisting a session. */
22
+ sessionId?: string;
23
+ /** Omit `noTools` to load Pi's builtin tool suite. */
24
+ toolPolicy?: 'builtin';
18
25
  }
19
26
  /** Run a one-shot call whose only output channel is one structured tool call. */
20
27
  export declare function headlessToolCall(request: HeadlessRequest & {
@@ -22,3 +29,9 @@ export declare function headlessToolCall(request: HeadlessRequest & {
22
29
  }): Promise<unknown>;
23
30
  /** Run a one-shot call and return the final assistant text. */
24
31
  export declare function headlessText(request: HeadlessRequest): Promise<string>;
32
+ /** Run a tool-capable, extension-free in-memory turn against seeded context. */
33
+ export declare function headlessTurn(request: HeadlessRequest & {
34
+ seedMessages: readonly AgentMessage[];
35
+ sessionId: string;
36
+ toolPolicy: 'builtin';
37
+ }): Promise<'completed' | 'failed'>;
@@ -112,13 +112,17 @@ async function runHeadless(request, fallback, collect) {
112
112
  };
113
113
  },
114
114
  });
115
+ const sessionManager = sdk.SessionManager.inMemory(nodeCwd, request.sessionId === undefined ? undefined : { id: request.sessionId });
116
+ for (const message of request.seedMessages ?? []) {
117
+ sessionManager.appendMessage(message);
118
+ }
115
119
  ({ session } = await sdk.createAgentSessionFromServices({
116
120
  services,
117
- sessionManager: sdk.SessionManager.inMemory(nodeCwd),
121
+ sessionManager,
118
122
  model: selected.model,
119
123
  thinkingLevel: 'off',
120
124
  ...(tool === undefined
121
- ? { noTools: 'all' }
125
+ ? request.toolPolicy === 'builtin' ? {} : { noTools: 'all' }
122
126
  : { tools: [tool.name], customTools: [customTool] }),
123
127
  }));
124
128
  if (timedOut)
@@ -151,3 +155,7 @@ export function headlessToolCall(request) {
151
155
  export function headlessText(request) {
152
156
  return runHeadless(request, '', (session) => lastAssistantText(session.messages));
153
157
  }
158
+ /** Run a tool-capable, extension-free in-memory turn against seeded context. */
159
+ export function headlessTurn(request) {
160
+ return runHeadless(request, 'failed', () => 'completed');
161
+ }
@@ -1,8 +1,12 @@
1
- import type { NodeMeta, NodeStatus, ExitIntent, TerminalReason } from '../canvas/types.js';
1
+ import type { NodeMeta, NodeOutcomeDetailV1, NodeStatus, ExitIntent, TerminalReason } from '../canvas/types.js';
2
2
  /** The lifecycle events — the only vocabulary for moving a node's status/intent.
3
3
  * Each maps (in the table below) to a target status and/or intent plus the set
4
4
  * of from-statuses it is legal from. */
5
5
  export type LifecycleEvent = 'finish' | 'park' | 'cancel' | 'crash' | 'yield' | 'release' | 'refresh-launch' | 'revive';
6
+ export interface TerminalOutcomeInput {
7
+ /** Omit to derive: a lifecycle terminal transition is always a failure. */
8
+ detail?: NodeOutcomeDetailV1;
9
+ }
6
10
  /** Enact a lifecycle event on a node: validate the from-status against the
7
11
  * table, then write status+intent in ONE atomic statement (so they can never
8
12
  * disagree). Returns the hydrated node view after the write.
@@ -11,6 +15,7 @@ export type LifecycleEvent = 'finish' | 'park' | 'cancel' | 'crash' | 'yield' |
11
15
  * legality authoritative at SQLite write time rather than at a stale read. */
12
16
  export declare function transition(nodeId: string, event: LifecycleEvent, opts?: {
13
17
  reason?: TerminalReason;
18
+ outcome?: TerminalOutcomeInput;
14
19
  }): NodeMeta;
15
20
  /** Whether this row is a PARKED node — terminalized by the unattended clock
16
21
  * rather than by finishing its own work. The one read of the park marker:
@@ -31,7 +31,7 @@
31
31
  // unknown-node/illegal-move/already-finalized-cancel branching — stays here;
32
32
  // only the raw conditional UPDATE lives in canvas.ts, keeping every `.prepare(`
33
33
  // inside `core/canvas/`).
34
- import { getNode, cancelCronsOnWake, transitionStatusIntent } from '../canvas/index.js';
34
+ import { getNode, cancelCronsOnWake, composeOutcomeDeliveryDocument, readOutcomeDelivery, transitionStatusIntent } from '../canvas/index.js';
35
35
  const ANY = '*';
36
36
  const TERMINAL_REASONS = {
37
37
  finish: 'finished',
@@ -72,6 +72,29 @@ const TRANSITIONS = {
72
72
  // reviveNode ordinary launch · resetRoot · stophook boot confirmation.
73
73
  revive: { status: 'active', intent: null, from: ANY },
74
74
  };
75
+ function firstLineAtMost(value, maxBytes) {
76
+ const line = value.split('\n')[0] ?? '';
77
+ let bytes = 0;
78
+ let result = '';
79
+ for (const character of line) {
80
+ const next = Buffer.byteLength(character, 'utf8');
81
+ if (bytes + next > maxBytes)
82
+ break;
83
+ bytes += next;
84
+ result += character;
85
+ }
86
+ return result;
87
+ }
88
+ function detailJson(detail) {
89
+ if (detail === undefined)
90
+ return null;
91
+ const message = detail.message === undefined ? undefined : firstLineAtMost(detail.message, 500);
92
+ const bounded = { ...detail, ...(message === undefined ? {} : { message }) };
93
+ const serialized = JSON.stringify(bounded);
94
+ if (Buffer.byteLength(serialized, 'utf8') <= 4 * 1024)
95
+ return serialized;
96
+ return JSON.stringify({ schema: 'crtr.node-outcome-detail/v1', truncated: true, ...(message === undefined ? {} : { message }) });
97
+ }
75
98
  /** Enact a lifecycle event on a node: validate the from-status against the
76
99
  * table, then write status+intent in ONE atomic statement (so they can never
77
100
  * disagree). Returns the hydrated node view after the write.
@@ -90,10 +113,36 @@ export function transition(nodeId, event, opts) {
90
113
  fields.intent = spec.intent ?? null;
91
114
  if (reason !== undefined)
92
115
  fields.terminal_reason = reason;
116
+ const terminal = event === 'finish' || event === 'park' || event === 'cancel' || event === 'crash';
117
+ const current = terminal ? getNode(nodeId) : null;
118
+ const recordsOutcome = terminal && current?.kind !== 'human';
119
+ const outcomeReason = reason;
120
+ const outcomeAt = terminal ? new Date().toISOString() : null;
121
+ const outcomeDetailJson = detailJson(opts?.outcome?.detail);
122
+ let documentJson;
123
+ if (recordsOutcome && current !== null && current.outcome_at === null) {
124
+ const delivery = readOutcomeDelivery(nodeId);
125
+ if (delivery?.state === 'armed') {
126
+ documentJson = composeOutcomeDeliveryDocument({
127
+ nodeId,
128
+ outcome: { kind: 'failure', reason: outcomeReason, at: outcomeAt, revision: (current.outcome_revision ?? 0) + 1, detailJson: outcomeDetailJson },
129
+ action: { name: delivery.actionName, payload: delivery.payload },
130
+ });
131
+ }
132
+ }
93
133
  // A committed canonical result is protected from a concurrent close.
94
134
  const changes = transitionStatusIntent(nodeId, fields, {
95
135
  fromStatuses: spec.from === ANY ? undefined : spec.from,
96
136
  requireNoFinalReport: event === 'cancel',
137
+ ...(recordsOutcome ? {
138
+ outcome: {
139
+ kind: 'failure',
140
+ reason: outcomeReason,
141
+ detailJson: outcomeDetailJson,
142
+ at: outcomeAt,
143
+ ...(documentJson === undefined ? {} : { documentJson }),
144
+ },
145
+ } : {}),
97
146
  });
98
147
  if (changes === 0) {
99
148
  const cur = getNode(nodeId);
@@ -1,4 +1,4 @@
1
- import { type NodeMeta, type Mode, type Lifecycle, type LaunchSpec, type ManagedWorktree, type ReviewBinding } from '../canvas/index.js';
1
+ import { type NodeMeta, type Mode, type Lifecycle, type LaunchSpec, type ManagedWorktree, type OutcomeDeliveryBinding, type ReviewBinding } from '../canvas/index.js';
2
2
  /** Explicitly set the resolved install id. crtrd calls this once at boot,
3
3
  * immediately after `resolveInstallId()` — see crtrd.ts — so a provisioning
4
4
  * mismatch fails the daemon loudly at STARTUP rather than lazily on first
@@ -97,6 +97,10 @@ export interface SpawnNodeOpts {
97
97
  profile_id?: string | null;
98
98
  /** Managed worktree already created for this node, if any. */
99
99
  managedWorktree?: ManagedWorktree;
100
+ /** Absolute wall-clock deadline persisted with the row at birth. */
101
+ deadlineAt?: string;
102
+ /** Delivery target persisted with the row before any broker can launch. */
103
+ outcomeDelivery?: OutcomeDeliveryBinding;
100
104
  /** Override the generated id — either an internally pre-allocated id
101
105
  * (worktree naming; always valid+fresh) or an external caller's explicit id
102
106
  * (`--node-id`), which is format-validated
@@ -264,6 +264,7 @@ export function spawnNode(opts) {
264
264
  passive_default: opts.passiveDefault ?? false,
265
265
  ...(opts.managedWorktree !== undefined ? { managed_worktree: opts.managedWorktree } : {}),
266
266
  intent: null,
267
+ deadline_at: opts.deadlineAt ?? null,
267
268
  pi_session_id: null,
268
269
  pi_session_file: null,
269
270
  launch: opts.launch,
@@ -278,7 +279,7 @@ export function spawnNode(opts) {
278
279
  throw new Error(`cannot spawn from unknown creator node: ${creator}`);
279
280
  }
280
281
  assertProfileAvailableForBirth(meta.profile_id ?? null);
281
- createNode(meta);
282
+ createNode(meta, opts.outcomeDelivery === undefined ? undefined : { outcomeDelivery: opts.outcomeDelivery });
282
283
  // Create the node-local memory directory so substrate docs can be written
283
284
  // directly into it without a separate mkdir. Skip for ephemeral human-bridge
284
285
  // rows (kind 'human') — they are not agent nodes.
@@ -0,0 +1,70 @@
1
+ import type { DeclinedResult, NodeMeta, NodeOutcomeDetailV1, NodeOutcomeKind, TerminalReason } from '../canvas/types.js';
2
+ export interface NodeOutcomeDocumentV1 {
3
+ schema: 'crtr.node-outcome/v1';
4
+ outcomeId: string;
5
+ nodeId: string;
6
+ revision: number;
7
+ kind: NodeOutcomeKind;
8
+ reason: TerminalReason;
9
+ settledAt: string;
10
+ node: {
11
+ name: string;
12
+ description?: string;
13
+ kind: string;
14
+ mode: 'base' | 'orchestrator';
15
+ lifecycle: 'terminal' | 'resident';
16
+ status: 'active' | 'idle' | 'done' | 'dead' | 'canceled';
17
+ terminalReason: TerminalReason | null;
18
+ cwd: string;
19
+ createdAt: string;
20
+ parent: string | null;
21
+ spawnedBy: string | null;
22
+ };
23
+ result?: {
24
+ finalReport: {
25
+ basename: string;
26
+ path: string;
27
+ };
28
+ structuredResult?: unknown;
29
+ structuredResultOmitted?: true;
30
+ structuredResultPath?: string;
31
+ };
32
+ failure?: {
33
+ message: string | null;
34
+ faultKind: NodeOutcomeDetailV1['fault_kind'] | null;
35
+ errorClass: NodeOutcomeDetailV1['error_class'] | null;
36
+ respawnFailures: number | null;
37
+ deadline: {
38
+ deadlineAt: string;
39
+ elapsedMs: number;
40
+ } | null;
41
+ /** Set only for reason `declined`: the reason, requester-classified code, and retryable claim. */
42
+ declined: DeclinedResult | null;
43
+ };
44
+ action: {
45
+ name: string;
46
+ payload: unknown;
47
+ };
48
+ delivery: {
49
+ attempt: number;
50
+ };
51
+ }
52
+ /** Compose a document at outcome time; delivery only replaces `delivery.attempt`. */
53
+ export declare function composeNodeOutcomeDocument(args: {
54
+ node: NodeMeta;
55
+ outcome: {
56
+ kind: NodeOutcomeKind;
57
+ reason: TerminalReason;
58
+ at: string;
59
+ revision: number;
60
+ detailJson: string | null;
61
+ };
62
+ action: {
63
+ name: string;
64
+ payload: unknown;
65
+ };
66
+ /** Required for a result before the final-report pointer has committed. */
67
+ finalReport?: string | null;
68
+ /** Lets the outcome transaction supply a parsed result without a second read. */
69
+ structuredResult?: unknown;
70
+ }): NodeOutcomeDocumentV1;
@@ -0,0 +1,97 @@
1
+ // The immutable outcome document handed to a registered delivery target.
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { contextDir, reportsDir } from '../canvas/paths.js';
4
+ import { outputResultPath } from './structured-output.js';
5
+ const MAX_DOCUMENT_BYTES = 256 * 1024;
6
+ function structuredResult(nodeId) {
7
+ const path = outputResultPath(nodeId);
8
+ try {
9
+ return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : undefined;
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ function parseDetail(detailJson) {
16
+ if (detailJson === null)
17
+ return null;
18
+ try {
19
+ return JSON.parse(detailJson);
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /** Preserve valid Unicode while bounding a display-only field by UTF-8 bytes. */
26
+ function truncateDescription(value) {
27
+ const maxBytes = 1024;
28
+ if (Buffer.byteLength(value, 'utf8') <= maxBytes)
29
+ return value;
30
+ const ellipsis = '…';
31
+ let bytes = Buffer.byteLength(ellipsis, 'utf8');
32
+ let result = '';
33
+ for (const character of value) {
34
+ const next = Buffer.byteLength(character, 'utf8');
35
+ if (bytes + next > maxBytes)
36
+ break;
37
+ bytes += next;
38
+ result += character;
39
+ }
40
+ return `${result}${ellipsis}`;
41
+ }
42
+ /** Compose a document at outcome time; delivery only replaces `delivery.attempt`. */
43
+ export function composeNodeOutcomeDocument(args) {
44
+ const { node, outcome, action } = args;
45
+ const detail = parseDetail(outcome.detailJson);
46
+ const finalReport = args.finalReport ?? node.final_report ?? '';
47
+ const name = truncateDescription(node.name);
48
+ const description = node.description === undefined ? undefined : truncateDescription(node.description);
49
+ const result = outcome.kind === 'result'
50
+ ? args.structuredResult === undefined ? structuredResult(node.node_id) : args.structuredResult
51
+ : undefined;
52
+ const document = {
53
+ schema: 'crtr.node-outcome/v1',
54
+ outcomeId: `${node.node_id}:${outcome.revision}`,
55
+ nodeId: node.node_id,
56
+ revision: outcome.revision,
57
+ kind: outcome.kind,
58
+ reason: outcome.reason,
59
+ settledAt: outcome.at,
60
+ node: {
61
+ name,
62
+ ...(description === undefined ? {} : { description }),
63
+ kind: node.kind,
64
+ mode: node.mode,
65
+ lifecycle: node.lifecycle,
66
+ status: node.status,
67
+ terminalReason: node.terminal_reason ?? null,
68
+ cwd: node.cwd,
69
+ createdAt: node.created,
70
+ parent: node.parent ?? null,
71
+ spawnedBy: node.spawned_by ?? null,
72
+ },
73
+ ...(outcome.kind === 'result' ? {
74
+ result: {
75
+ finalReport: { basename: finalReport, path: `${reportsDir(node.node_id)}/${finalReport}` },
76
+ ...(result === undefined ? {} : { structuredResult: result }),
77
+ },
78
+ } : {
79
+ failure: {
80
+ message: detail?.message ?? null,
81
+ faultKind: detail?.fault_kind ?? null,
82
+ errorClass: detail?.error_class ?? null,
83
+ respawnFailures: detail?.respawn_failures ?? null,
84
+ deadline: detail?.deadline === undefined ? null : { deadlineAt: detail.deadline.deadline_at, elapsedMs: detail.deadline.elapsed_ms },
85
+ declined: detail?.declined ?? null,
86
+ },
87
+ }),
88
+ action,
89
+ delivery: { attempt: 0 },
90
+ };
91
+ if (Buffer.byteLength(JSON.stringify(document), 'utf8') > MAX_DOCUMENT_BYTES && document.result?.structuredResult !== undefined) {
92
+ delete document.result.structuredResult;
93
+ document.result.structuredResultOmitted = true;
94
+ document.result.structuredResultPath = `${contextDir(node.node_id)}/result.json`;
95
+ }
96
+ return document;
97
+ }