@north-light/crouter 0.3.263 → 0.3.270

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 (171) hide show
  1. package/dist/api/client.d.ts +5 -1
  2. package/dist/api/client.js +6 -0
  3. package/dist/api/dto/broker.d.ts +11 -2
  4. package/dist/api/dto/broker.js +2 -2
  5. package/dist/api/dto/canvas.d.ts +53 -2
  6. package/dist/api/dto/nodes.d.ts +10 -2
  7. package/dist/api/routes.d.ts +1 -0
  8. package/dist/api/routes.js +1 -0
  9. package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +2 -2
  10. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +50 -0
  11. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +46 -6
  12. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +6 -2
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +7 -2
  14. package/dist/clients/attach/session/whip-achievements.d.ts +4 -0
  15. package/dist/clients/attach/session/whip-achievements.js +145 -0
  16. package/dist/clients/attach/slash/dispatch.js +5 -3
  17. package/dist/clients/attach/viewer.js +607 -606
  18. package/dist/commands/canvas-history/grep.js +3 -22
  19. package/dist/commands/canvas-history/read.js +5 -3
  20. package/dist/commands/canvas-history/search.js +3 -22
  21. package/dist/commands/canvas-history/shared.d.ts +8 -0
  22. package/dist/commands/canvas-history/shared.js +64 -1
  23. package/dist/commands/canvas-history/stats.d.ts +1 -0
  24. package/dist/commands/canvas-history/stats.js +68 -0
  25. package/dist/commands/canvas-history.js +4 -3
  26. package/dist/commands/node/create.js +2 -2
  27. package/dist/commands/pkg/plugin-inspect.js +2 -2
  28. package/dist/commands/pkg/plugin-manage.js +3 -3
  29. package/dist/commands/sys/config.js +19 -2
  30. package/dist/commands/sys/doctor.js +4 -3
  31. package/dist/commands/sys/panels/models-panel.js +3 -1
  32. package/dist/core/__tests__/human-deliver.test.js +1 -1
  33. package/dist/core/__tests__/integration/lifecycle-hooks.test.d.ts +1 -0
  34. package/dist/core/__tests__/integration/lifecycle-hooks.test.js +182 -0
  35. package/dist/core/__tests__/integration/revive.test.js +19 -17
  36. package/dist/core/__tests__/model-pin-durability.test.js +25 -1
  37. package/dist/core/__tests__/push-final-guard.test.js +1 -1
  38. package/dist/core/__tests__/relaunch-root.test.js +17 -0
  39. package/dist/core/__tests__/revive-capacity.test.js +20 -20
  40. package/dist/core/__tests__/revive-parked-fresh.test.js +14 -14
  41. package/dist/core/__tests__/seam/broker-cap-freeze.test.js +2 -2
  42. package/dist/core/__tests__/seam/broker-provider-retry.test.js +19 -4
  43. package/dist/core/__tests__/seam/dormancy-release.test.js +7 -7
  44. package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +1 -1
  45. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
  46. package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +2 -2
  47. package/dist/core/broker-client/__tests__/transport-relay.test.js +3 -1
  48. package/dist/core/broker-client/transport-relay.js +5 -1
  49. package/dist/core/canvas/__tests__/history-transcript.test.d.ts +1 -0
  50. package/dist/core/canvas/__tests__/history-transcript.test.js +102 -0
  51. package/dist/core/canvas/browse/app.d.ts +1 -1
  52. package/dist/core/canvas/browse/app.js +13 -10
  53. package/dist/core/canvas/browse/model.js +16 -1
  54. package/dist/core/canvas/browse/render.d.ts +23 -3
  55. package/dist/core/canvas/browse/render.js +429 -64
  56. package/dist/core/canvas/canvas.js +12 -8
  57. package/dist/core/canvas/extensions.d.ts +1 -1
  58. package/dist/core/canvas/extensions.js +7 -1
  59. package/dist/core/canvas/history.d.ts +36 -1
  60. package/dist/core/canvas/history.js +328 -7
  61. package/dist/core/canvas/labels.d.ts +2 -2
  62. package/dist/core/canvas/migrations.js +32 -0
  63. package/dist/core/canvas/node-recap.d.ts +23 -0
  64. package/dist/core/canvas/node-recap.js +67 -0
  65. package/dist/core/canvas/render-source.d.ts +33 -1
  66. package/dist/core/canvas/render-source.js +80 -22
  67. package/dist/core/canvas/types.d.ts +9 -0
  68. package/dist/core/command-hooks/discovery.d.ts +27 -1
  69. package/dist/core/command-hooks/discovery.js +52 -0
  70. package/dist/core/command-hooks/index.d.ts +4 -3
  71. package/dist/core/command-hooks/index.js +2 -1
  72. package/dist/core/command-hooks/lifecycle-catalog.d.ts +3 -0
  73. package/dist/core/command-hooks/lifecycle-catalog.js +4 -0
  74. package/dist/core/command-hooks/report.d.ts +11 -2
  75. package/dist/core/command-hooks/report.js +10 -1
  76. package/dist/core/command-hooks/schema.d.ts +16 -1
  77. package/dist/core/command-hooks/schema.js +128 -40
  78. package/dist/core/command-hooks/transport/exec-lifecycle.d.ts +12 -0
  79. package/dist/core/command-hooks/transport/exec-lifecycle.js +152 -0
  80. package/dist/core/config.js +13 -5
  81. package/dist/core/human/feedback-companion.js +1 -1
  82. package/dist/core/keybindings/catalog.d.ts +2 -2
  83. package/dist/core/keybindings/catalog.js +2 -1
  84. package/dist/core/model-routes.js +1 -1
  85. package/dist/core/review/realize.js +1 -1
  86. package/dist/core/runtime/broker/auth-reload.js +4 -1
  87. package/dist/core/runtime/broker/engine-drive.d.ts +14 -3
  88. package/dist/core/runtime/broker/engine-drive.js +29 -1
  89. package/dist/core/runtime/broker/fault-retry.js +7 -2
  90. package/dist/core/runtime/broker/frame-client.js +3 -0
  91. package/dist/core/runtime/broker/frame-dispatch.js +7 -0
  92. package/dist/core/runtime/broker/inbox.d.ts +12 -2
  93. package/dist/core/runtime/broker/inbox.js +68 -38
  94. package/dist/core/runtime/broker/rebind.js +7 -0
  95. package/dist/core/runtime/broker-protocol.d.ts +16 -1
  96. package/dist/core/runtime/broker.d.ts +1 -1
  97. package/dist/core/runtime/broker.js +27 -3
  98. package/dist/core/runtime/canvas-extensions.d.ts +1 -0
  99. package/dist/core/runtime/canvas-extensions.js +2 -0
  100. package/dist/core/runtime/interactive-deliver.d.ts +8 -2
  101. package/dist/core/runtime/interactive-deliver.js +12 -3
  102. package/dist/core/runtime/launch-target.js +1 -1
  103. package/dist/core/runtime/launch.d.ts +12 -6
  104. package/dist/core/runtime/launch.js +19 -17
  105. package/dist/core/runtime/model-swap.d.ts +5 -4
  106. package/dist/core/runtime/model-swap.js +7 -5
  107. package/dist/core/runtime/node-read.d.ts +2 -0
  108. package/dist/core/runtime/node-read.js +18 -9
  109. package/dist/core/runtime/promote.js +2 -1
  110. package/dist/core/runtime/reset.js +4 -2
  111. package/dist/core/runtime/revive-all.d.ts +1 -1
  112. package/dist/core/runtime/revive-all.js +2 -2
  113. package/dist/core/runtime/revive.d.ts +2 -2
  114. package/dist/core/runtime/revive.js +71 -13
  115. package/dist/core/runtime/session-visibility.d.ts +10 -14
  116. package/dist/core/runtime/session-visibility.js +43 -30
  117. package/dist/core/runtime/spawn.d.ts +3 -3
  118. package/dist/core/runtime/spawn.js +20 -3
  119. package/dist/core/runtime/stamp/channel.d.ts +22 -0
  120. package/dist/core/runtime/stamp/channel.js +83 -0
  121. package/dist/core/runtime/stamp/protocol.d.ts +90 -0
  122. package/dist/core/runtime/stamp/protocol.js +113 -0
  123. package/dist/core/runtime/tmux-driver.d.ts +11 -1
  124. package/dist/core/runtime/tmux-driver.js +27 -11
  125. package/dist/core/runtime/turn-visibility.d.ts +10 -0
  126. package/dist/core/runtime/turn-visibility.js +31 -0
  127. package/dist/core/self-update.js +2 -5
  128. package/dist/core/tui/draw.d.ts +1 -0
  129. package/dist/core/tui/draw.js +5 -1
  130. package/dist/core/tui/markdown.d.ts +17 -0
  131. package/dist/core/tui/markdown.js +407 -0
  132. package/dist/core/user-settings.d.ts +4 -0
  133. package/dist/core/user-settings.js +1 -0
  134. package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -0
  135. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +2 -2
  136. package/dist/daemon/api/bridge.d.ts +3 -2
  137. package/dist/daemon/api/bridge.js +34 -9
  138. package/dist/daemon/api/handlers/attach.js +4 -4
  139. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  140. package/dist/daemon/api/handlers/canvas.js +62 -16
  141. package/dist/daemon/api/handlers/messages.js +14 -3
  142. package/dist/daemon/api/handlers/nodes.js +17 -7
  143. package/dist/daemon/api/map.js +2 -0
  144. package/dist/daemon/cron/sinks.js +3 -3
  145. package/dist/daemon/fleet.js +1 -1
  146. package/dist/daemon/messaging/node-message.js +16 -2
  147. package/dist/daemon/profile-delete.js +1 -1
  148. package/dist/daemon/reconcilers/broker-supervision.js +26 -9
  149. package/dist/daemon/reconcilers/live-obligation.d.ts +8 -1
  150. package/dist/daemon/reconcilers/live-obligation.js +17 -0
  151. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +6 -6
  152. package/dist/hook-authoring.d.ts +31 -1
  153. package/dist/hook-authoring.js +42 -5
  154. package/dist/index.d.ts +1 -0
  155. package/dist/index.js +3 -0
  156. package/dist/pi-extensions/canvas-inbox-watcher.js +27 -2
  157. package/dist/pi-extensions/canvas-recap.js +4 -0
  158. package/dist/pi-extensions/canvas-stamp.d.ts +14 -0
  159. package/dist/pi-extensions/canvas-stamp.js +77 -0
  160. package/dist/pi-extensions/canvas-stophook.js +3 -0
  161. package/dist/shared/__tests__/env-boundary.test.js +4 -2
  162. package/dist/shared/crtr-version.d.ts +2 -0
  163. package/dist/shared/crtr-version.js +18 -0
  164. package/dist/shared/env.d.ts +10 -0
  165. package/dist/shared/env.js +19 -4
  166. package/dist/shared/generated-context.d.ts +1 -2
  167. package/dist/shared/generated-context.js +3 -4
  168. package/dist/types.d.ts +10 -0
  169. package/dist/types.js +4 -0
  170. package/package.json +1 -1
  171. package/runtime.lock.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import { isRecord } from '../../shared/predicates.js';
2
+ import { LIFECYCLE_EVENTS, isLifecycleEvent } from './lifecycle-catalog.js';
2
3
  function typeName(value) {
3
4
  if (value === null)
4
5
  return 'null';
@@ -6,6 +7,98 @@ function typeName(value) {
6
7
  return 'array';
7
8
  return typeof value;
8
9
  }
10
+ function validateCommandDeclaration(value, index, issue) {
11
+ const path = `hooks[${index}]`;
12
+ if (!isRecord(value)) {
13
+ issue('hook_manifest_invalid', 'hook must be an object', typeName(value), '{ target, phase, op, description, effects }', 'Fix the hook declaration.', path);
14
+ return undefined;
15
+ }
16
+ const keys = Object.keys(value).filter((key) => !['target', 'phase', 'op', 'description', 'effects'].includes(key));
17
+ if (keys.length > 0) {
18
+ issue('hook_manifest_invalid', 'unknown hook keys', keys.join(', '), 'only: target, phase, op, description, effects', 'Remove the unknown keys.', path);
19
+ return undefined;
20
+ }
21
+ for (const field of ['target', 'op', 'description']) {
22
+ if (typeof value[field] !== 'string' || value[field].length === 0) {
23
+ issue('hook_manifest_invalid', `${field} must be a non-empty string`, typeName(value[field]), 'a non-empty string', `Set ${field}.`, `${path}.${field}`);
24
+ return undefined;
25
+ }
26
+ }
27
+ if (value['phase'] !== 'before' && value['phase'] !== 'after' && value['phase'] !== 'replace') {
28
+ issue('hook_manifest_invalid', 'phase must be before|after|replace', String(value['phase']), 'before | after | replace', 'Set phase to a supported hook phase.', `${path}.phase`);
29
+ return undefined;
30
+ }
31
+ if (!isEffects(value['effects'])) {
32
+ issue('hook_manifest_invalid', 'effects must be a non-empty string array', typeName(value['effects']), 'a non-empty array of non-empty strings', 'Declare every persistent effect.', `${path}.effects`);
33
+ return undefined;
34
+ }
35
+ return {
36
+ target: value['target'],
37
+ phase: value['phase'],
38
+ op: value['op'],
39
+ description: value['description'],
40
+ effects: value['effects'],
41
+ };
42
+ }
43
+ function validateLifecycleDeclaration(value, index, issue) {
44
+ const path = `lifecycle[${index}]`;
45
+ if (!isRecord(value)) {
46
+ issue('hook_manifest_invalid', 'lifecycle hook must be an object', typeName(value), '{ event, phase, op, description, effects }', 'Fix the lifecycle declaration.', path);
47
+ return undefined;
48
+ }
49
+ const keys = Object.keys(value).filter((key) => !['event', 'phase', 'op', 'description', 'effects'].includes(key));
50
+ if (keys.length > 0) {
51
+ issue('hook_manifest_invalid', 'unknown lifecycle hook keys', keys.join(', '), 'only: event, phase, op, description, effects', 'Remove the unknown keys.', path);
52
+ return undefined;
53
+ }
54
+ for (const field of ['event', 'op', 'description']) {
55
+ if (typeof value[field] !== 'string' || value[field].length === 0) {
56
+ issue('hook_manifest_invalid', `${field} must be a non-empty string`, typeName(value[field]), 'a non-empty string', `Set ${field}.`, `${path}.${field}`);
57
+ return undefined;
58
+ }
59
+ }
60
+ if (!isLifecycleEvent(value['event'])) {
61
+ issue('hook_manifest_invalid', 'event must name a supported lifecycle event', String(value['event']), LIFECYCLE_EVENTS.join(' | '), `Set event to one of: ${LIFECYCLE_EVENTS.join(', ')}.`, `${path}.event`);
62
+ return undefined;
63
+ }
64
+ if (value['phase'] !== 'on') {
65
+ issue('hook_manifest_invalid', 'phase must be on', String(value['phase']), 'on', 'Set phase to on.', `${path}.phase`);
66
+ return undefined;
67
+ }
68
+ if (!isEffects(value['effects'])) {
69
+ issue('hook_manifest_invalid', 'effects must be a non-empty string array', typeName(value['effects']), 'a non-empty array of non-empty strings', 'Declare every persistent effect.', `${path}.effects`);
70
+ return undefined;
71
+ }
72
+ return {
73
+ event: value['event'],
74
+ phase: 'on',
75
+ op: value['op'],
76
+ description: value['description'],
77
+ effects: value['effects'],
78
+ };
79
+ }
80
+ function isEffects(value) {
81
+ return Array.isArray(value) && value.length > 0 && value.every((effect) => typeof effect === 'string' && effect.length > 0);
82
+ }
83
+ function validateV1(raw, issue) {
84
+ const unknown = Object.keys(raw).filter((key) => key !== 'schemaVersion' && key !== 'hooks');
85
+ if (unknown.length > 0) {
86
+ issue('hook_manifest_invalid', 'unknown top-level keys', unknown.join(', '), 'only: schemaVersion, hooks', 'Remove the unknown keys.');
87
+ return { issues: [] };
88
+ }
89
+ if (!Array.isArray(raw['hooks']) || raw['hooks'].length === 0) {
90
+ issue('hook_manifest_invalid', 'hooks must be a non-empty array', typeName(raw['hooks']), 'a non-empty array of hook declarations', 'Declare at least one hook.', 'hooks');
91
+ return { issues: [] };
92
+ }
93
+ const hooks = [];
94
+ for (let index = 0; index < raw['hooks'].length; index++) {
95
+ const hook = validateCommandDeclaration(raw['hooks'][index], index, issue);
96
+ if (hook === undefined)
97
+ return { issues: [] };
98
+ hooks.push(hook);
99
+ }
100
+ return { manifest: { schemaVersion: 1, hooks }, issues: [] };
101
+ }
9
102
  /** Validates the declarative hooks.json payload without filesystem access. */
10
103
  export function validateHookManifest(raw) {
11
104
  const issues = [];
@@ -16,53 +109,48 @@ export function validateHookManifest(raw) {
16
109
  issue('hook_manifest_invalid', 'manifest must be an object', typeName(raw), '{ schemaVersion, hooks }', 'Provide a valid hooks.json manifest.');
17
110
  return { issues };
18
111
  }
19
- const unknown = Object.keys(raw).filter((key) => key !== 'schemaVersion' && key !== 'hooks');
112
+ if (raw['schemaVersion'] === 1) {
113
+ const validation = validateV1(raw, issue);
114
+ return { ...(validation.manifest === undefined ? {} : { manifest: validation.manifest }), issues };
115
+ }
116
+ if (raw['schemaVersion'] !== 2) {
117
+ issue('hook_schema_version', 'schemaVersion must be exactly 1 or 2', String(raw['schemaVersion']), '1 | 2', 'Update the manifest schema version to 1 or 2.', 'schemaVersion');
118
+ return { issues };
119
+ }
120
+ const unknown = Object.keys(raw).filter((key) => key !== 'schemaVersion' && key !== 'hooks' && key !== 'lifecycle');
20
121
  if (unknown.length > 0) {
21
- issue('hook_manifest_invalid', 'unknown top-level keys', unknown.join(', '), 'only: schemaVersion, hooks', 'Remove the unknown keys.');
122
+ issue('hook_manifest_invalid', 'unknown top-level keys', unknown.join(', '), 'only: schemaVersion, hooks, lifecycle', 'Remove the unknown keys.');
22
123
  return { issues };
23
124
  }
24
- if (raw['schemaVersion'] !== 1) {
25
- issue('hook_schema_version', 'schemaVersion must be exactly 1', String(raw['schemaVersion']), '1', 'Update the manifest schema version to 1.', 'schemaVersion');
125
+ const rawHooks = raw['hooks'];
126
+ const rawLifecycle = raw['lifecycle'];
127
+ if (rawHooks !== undefined && !Array.isArray(rawHooks)) {
128
+ issue('hook_manifest_invalid', 'hooks must be an array when present', typeName(rawHooks), 'an array of hook declarations', 'Fix hooks.', 'hooks');
26
129
  return { issues };
27
130
  }
28
- if (!Array.isArray(raw['hooks']) || raw['hooks'].length === 0) {
29
- issue('hook_manifest_invalid', 'hooks must be a non-empty array', typeName(raw['hooks']), 'a non-empty array of hook declarations', 'Declare at least one hook.', 'hooks');
131
+ if (rawLifecycle !== undefined && !Array.isArray(rawLifecycle)) {
132
+ issue('hook_manifest_invalid', 'lifecycle must be an array when present', typeName(rawLifecycle), 'an array of lifecycle hook declarations', 'Fix lifecycle.', 'lifecycle');
30
133
  return { issues };
31
134
  }
32
- const hooks = [];
33
- for (let index = 0; index < raw['hooks'].length; index++) {
34
- const value = raw['hooks'][index];
35
- const path = `hooks[${index}]`;
36
- if (!isRecord(value)) {
37
- issue('hook_manifest_invalid', 'hook must be an object', typeName(value), '{ target, phase, op, description, effects }', 'Fix the hook declaration.', path);
38
- return { issues };
39
- }
40
- const keys = Object.keys(value).filter((key) => !['target', 'phase', 'op', 'description', 'effects'].includes(key));
41
- if (keys.length > 0) {
42
- issue('hook_manifest_invalid', 'unknown hook keys', keys.join(', '), 'only: target, phase, op, description, effects', 'Remove the unknown keys.', path);
43
- return { issues };
44
- }
45
- for (const field of ['target', 'op', 'description']) {
46
- if (typeof value[field] !== 'string' || value[field].length === 0) {
47
- issue('hook_manifest_invalid', `${field} must be a non-empty string`, typeName(value[field]), 'a non-empty string', `Set ${field}.`, `${path}.${field}`);
48
- return { issues };
49
- }
50
- }
51
- if (value['phase'] !== 'before' && value['phase'] !== 'after' && value['phase'] !== 'replace') {
52
- issue('hook_manifest_invalid', 'phase must be before|after|replace', String(value['phase']), 'before | after | replace', 'Set phase to a supported hook phase.', `${path}.phase`);
135
+ const hooks = rawHooks ?? [];
136
+ const lifecycle = rawLifecycle ?? [];
137
+ if (hooks.length === 0 && lifecycle.length === 0) {
138
+ issue('hook_manifest_invalid', 'hooks or lifecycle must be a non-empty array', 'both arrays were empty or absent', 'at least one command or lifecycle hook declaration', 'Declare at least one hook.', 'hooks');
139
+ return { issues };
140
+ }
141
+ const declaredHooks = [];
142
+ for (let index = 0; index < hooks.length; index++) {
143
+ const hook = validateCommandDeclaration(hooks[index], index, issue);
144
+ if (hook === undefined)
53
145
  return { issues };
54
- }
55
- if (!Array.isArray(value['effects']) || value['effects'].length === 0 || !value['effects'].every((effect) => typeof effect === 'string' && effect.length > 0)) {
56
- issue('hook_manifest_invalid', 'effects must be a non-empty string array', typeName(value['effects']), 'a non-empty array of non-empty strings', 'Declare every persistent effect.', `${path}.effects`);
146
+ declaredHooks.push(hook);
147
+ }
148
+ const declaredLifecycle = [];
149
+ for (let index = 0; index < lifecycle.length; index++) {
150
+ const hook = validateLifecycleDeclaration(lifecycle[index], index, issue);
151
+ if (hook === undefined)
57
152
  return { issues };
58
- }
59
- hooks.push({
60
- target: value['target'],
61
- phase: value['phase'],
62
- op: value['op'],
63
- description: value['description'],
64
- effects: value['effects'],
65
- });
66
- }
67
- return { manifest: { schemaVersion: 1, hooks }, issues };
153
+ declaredLifecycle.push(hook);
154
+ }
155
+ return { manifest: { schemaVersion: 2, hooks: declaredHooks, lifecycle: declaredLifecycle }, issues };
68
156
  }
@@ -0,0 +1,12 @@
1
+ import type { LifecycleHookRequest } from '../../../hook-authoring.js';
2
+ import type { EffectiveLifecycleHook } from '../discovery.js';
3
+ export declare const LIFECYCLE_HOOK_TIMEOUT_MS = 5000;
4
+ export declare const LIFECYCLE_EVENT_BUDGET_MS = 10000;
5
+ export interface LifecycleHookInvocation {
6
+ node: LifecycleHookRequest['node'];
7
+ runtime: LifecycleHookRequest['runtime'];
8
+ }
9
+ /** Runs one lifecycle hook. The node launch remains authoritative on every failure. */
10
+ export declare function invokeLifecycleHook(hook: EffectiveLifecycleHook, invocation: LifecycleHookInvocation, timeoutMs: number): Promise<void>;
11
+ /** Executes lifecycle hooks in discovery order, within one bounded start-event budget. */
12
+ export declare function invokeLifecycleHooks(hooks: readonly EffectiveLifecycleHook[], invocation: LifecycleHookInvocation): Promise<void>;
@@ -0,0 +1,152 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { PROCESS_GROUP_KILL_GRACE_MS, posixProcessGroupSpawnOptions, terminateProcessGroup } from '../../../hook-process.js';
4
+ import { isRecord } from '../../../shared/predicates.js';
5
+ import { diag } from '../../io.js';
6
+ const MAX_STDOUT = 10 * 1024 * 1024;
7
+ export const LIFECYCLE_HOOK_TIMEOUT_MS = 5_000;
8
+ export const LIFECYCLE_EVENT_BUDGET_MS = 10_000;
9
+ /** Runs one lifecycle hook. The node launch remains authoritative on every failure. */
10
+ export async function invokeLifecycleHook(hook, invocation, timeoutMs) {
11
+ try {
12
+ const request = {
13
+ protocolVersion: 1,
14
+ op: hook.op,
15
+ event: hook.event,
16
+ phase: hook.phase,
17
+ operationId: randomBytes(16).toString('hex'),
18
+ node: invocation.node,
19
+ runtime: invocation.runtime,
20
+ context: { cwd: invocation.node.cwd },
21
+ };
22
+ const result = await runLifecycleProcess(hook, JSON.stringify(request), timeoutMs);
23
+ if (result.signal !== null)
24
+ throw new Error(`executable was killed by signal ${result.signal}`);
25
+ const envelope = parseEnvelope(result.stdout);
26
+ if (envelope.ok) {
27
+ if (result.status !== 0)
28
+ diag(`crtr: ${label(hook)} returned ok:true but exited ${result.status} (honoring the envelope)`);
29
+ return;
30
+ }
31
+ diag(`crtr: ${label(hook)} failed: ${envelope.error.code}: ${envelope.error.message}`);
32
+ }
33
+ catch (error) {
34
+ diag(`crtr: ${label(hook)} failed: ${describeError(error)}`);
35
+ }
36
+ }
37
+ /** Executes lifecycle hooks in discovery order, within one bounded start-event budget. */
38
+ export async function invokeLifecycleHooks(hooks, invocation) {
39
+ const deadline = Date.now() + LIFECYCLE_EVENT_BUDGET_MS;
40
+ for (const hook of hooks) {
41
+ const remaining = deadline - Date.now();
42
+ // A timed-out hook must be reaped before launch can proceed. Reserve the
43
+ // process-group termination grace inside the event budget rather than
44
+ // letting the final cleanup extend the node-start critical path.
45
+ const timeoutMs = Math.min(LIFECYCLE_HOOK_TIMEOUT_MS, remaining - PROCESS_GROUP_KILL_GRACE_MS);
46
+ if (timeoutMs <= 0) {
47
+ diag(`crtr: lifecycle hook event ${hook.event} exceeded its ${LIFECYCLE_EVENT_BUDGET_MS}ms aggregate budget; skipped ${label(hook)}`);
48
+ return;
49
+ }
50
+ await invokeLifecycleHook(hook, invocation, timeoutMs);
51
+ }
52
+ }
53
+ async function runLifecycleProcess(hook, input, timeoutMs) {
54
+ let child;
55
+ try {
56
+ child = spawn(hook.executable, ['--crtr-hook-protocol', '1'], {
57
+ cwd: hook.plugin.root,
58
+ env: process.env,
59
+ stdio: ['pipe', 'pipe', 'inherit'],
60
+ ...posixProcessGroupSpawnOptions(),
61
+ });
62
+ }
63
+ catch (error) {
64
+ throw new Error(`executable could not be run (${describeError(error)})`);
65
+ }
66
+ return new Promise((resolve, reject) => {
67
+ let stdoutBytes = 0;
68
+ const stdout = [];
69
+ let failure;
70
+ let closed = false;
71
+ let cleaned = false;
72
+ let status = null;
73
+ let signal = null;
74
+ let timeout;
75
+ const finish = () => {
76
+ if (!closed || (failure !== undefined && !cleaned))
77
+ return;
78
+ if (timeout !== undefined)
79
+ clearTimeout(timeout);
80
+ if (failure?.kind === 'timeout')
81
+ return reject(new Error(`executable timed out after ${timeoutMs}ms`));
82
+ if (failure?.kind === 'output_limit')
83
+ return reject(new Error('stdout exceeded the 10 MiB cap'));
84
+ if (failure?.kind === 'spawn')
85
+ return reject(new Error(`executable could not be run (${describeError(failure.error)})`));
86
+ if (failure?.kind === 'cleanup')
87
+ return reject(new Error(`executable cleanup failed (${describeError(failure.error)})`));
88
+ resolve({ stdout: Buffer.concat(stdout).toString('utf8'), status, signal });
89
+ };
90
+ const terminate = (reason) => {
91
+ if (failure !== undefined)
92
+ return;
93
+ failure = reason;
94
+ if (timeout !== undefined)
95
+ clearTimeout(timeout);
96
+ void terminateProcessGroup(child).then(() => { cleaned = true; finish(); }, (error) => { failure = { kind: 'cleanup', error }; cleaned = true; finish(); });
97
+ };
98
+ child.stdout.on('data', (chunk) => {
99
+ stdoutBytes += chunk.byteLength;
100
+ if (stdoutBytes > MAX_STDOUT)
101
+ return terminate({ kind: 'output_limit' });
102
+ stdout.push(chunk);
103
+ });
104
+ child.once('error', (error) => {
105
+ if (child.pid === undefined) {
106
+ failure = { kind: 'spawn', error };
107
+ closed = true;
108
+ cleaned = true;
109
+ finish();
110
+ return;
111
+ }
112
+ terminate({ kind: 'spawn', error });
113
+ });
114
+ child.once('close', (nextStatus, nextSignal) => {
115
+ status = nextStatus;
116
+ signal = nextSignal;
117
+ closed = true;
118
+ finish();
119
+ });
120
+ timeout = setTimeout(() => terminate({ kind: 'timeout' }), timeoutMs);
121
+ child.stdin.end(input);
122
+ });
123
+ }
124
+ function parseEnvelope(stdout) {
125
+ let value;
126
+ try {
127
+ value = JSON.parse(stdout.trim());
128
+ }
129
+ catch {
130
+ throw new Error('stdout was not a single JSON envelope');
131
+ }
132
+ if (!isRecord(value) || value['protocolVersion'] !== 1)
133
+ throw new Error('stdout was not a protocolVersion: 1 envelope');
134
+ if (value['ok'] === true && exactKeys(value, ['protocolVersion', 'ok']))
135
+ return { protocolVersion: 1, ok: true };
136
+ if (value['ok'] !== false || !exactKeys(value, ['protocolVersion', 'ok', 'error']))
137
+ throw new Error('response envelope had missing or extra fields');
138
+ const error = value['error'];
139
+ if (!isRecord(error) || !exactKeys(error, ['code', 'message'], ['next']) || typeof error['code'] !== 'string' || error['code'] === '' || typeof error['message'] !== 'string' || error['message'] === '' || (error['next'] !== undefined && typeof error['next'] !== 'string')) {
140
+ throw new Error('failure envelope had invalid error fields');
141
+ }
142
+ return { protocolVersion: 1, ok: false, error: { code: error['code'], message: error['message'], ...(error['next'] === undefined ? {} : { next: error['next'] }) } };
143
+ }
144
+ function exactKeys(value, required, optional = []) {
145
+ return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
146
+ }
147
+ function label(hook) {
148
+ return `plugin "${hook.plugin.name}" lifecycle hook "${hook.op}" for ${hook.event}`;
149
+ }
150
+ function describeError(error) {
151
+ return error instanceof Error ? error.message : String(error);
152
+ }
@@ -114,10 +114,11 @@ export function ensureScopeInitialized(scope, root) {
114
114
  // a missing `modelLadders` from whatever build is CURRENTLY running, and an explicit
115
115
  // user override (`sys config set modelLadders...`) still persists and still wins.
116
116
  // Keybindings are sparse user overrides, so an untouched scope omits that member too.
117
- const { modelLadders: _omittedModelLadders, keybindings: _omittedKeybindings, tmux_passthrough: _omittedTmuxPassthrough, brokerThresholds, ...rest } = defaultScopeConfig();
118
- // Broker thresholds are host-wide daemon policy, so only user scope
119
- // persists them. Project config must never acquire a machine setting.
120
- atomicWriteJson(cfgPath, scope === 'user' ? { ...rest, brokerThresholds } : rest);
117
+ const { modelLadders: _omittedModelLadders, keybindings: _omittedKeybindings, tmux_passthrough: _omittedTmuxPassthrough, brokerThresholds, lifecycle, ...rest } = defaultScopeConfig();
118
+ // Broker thresholds and lifecycle timing are host-wide daemon policy, so
119
+ // only user scope persists them. Project config must never acquire a
120
+ // machine setting.
121
+ atomicWriteJson(cfgPath, scope === 'user' ? { ...rest, brokerThresholds, lifecycle } : rest);
121
122
  });
122
123
  }
123
124
  function normalizeKeybindings(raw) {
@@ -701,6 +702,12 @@ function mergeBrokerThresholds(raw, defaults) {
701
702
  ? { warning, automaticReviveCap }
702
703
  : { ...defaults };
703
704
  }
705
+ function mergeLifecycle(raw, defaults) {
706
+ const partial = raw !== null && typeof raw === 'object'
707
+ ? raw
708
+ : {};
709
+ return { unattendedParkMs: normalizePositiveInteger(partial.unattendedParkMs, defaults.unattendedParkMs) };
710
+ }
704
711
  function normalizeWhipMessages(raw) {
705
712
  if (!Array.isArray(raw))
706
713
  return [...DEFAULT_WHIP_MESSAGES];
@@ -728,6 +735,7 @@ function mergeConfig(partial) {
728
735
  ? partial.auto_open_child_viewers
729
736
  : defaults.auto_open_child_viewers;
730
737
  const brokerThresholds = mergeBrokerThresholds(partial.brokerThresholds, defaults.brokerThresholds);
738
+ const lifecycle = mergeLifecycle(partial.lifecycle, defaults.lifecycle);
731
739
  const completion_bell = typeof partial.completion_bell === 'boolean' ? partial.completion_bell : defaults.completion_bell;
732
740
  const working_gerunds = partial.working_gerunds === undefined
733
741
  ? [...defaults.working_gerunds]
@@ -761,7 +769,7 @@ function mergeConfig(partial) {
761
769
  const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
762
770
  const bin = normalizeBin(partial.bin);
763
771
  const humanActions = normalizeHumanActions(partial.humanActions);
764
- return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, 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 };
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 };
765
773
  }
766
774
  /** Raw (un-defaulted) partial config for one scope, or null if the scope has
767
775
  * no root or no config.json. Used by `readMergedLaunchConfig` to layer
@@ -92,7 +92,7 @@ export async function realizeFeedbackCompanion(args) {
92
92
  // The bind deadline below only a live broker can meet, so a capacity
93
93
  // refusal must surface here and discard the companion — freezing would
94
94
  // burn the whole deadline waiting for a launch that is not coming.
95
- reviveNode(node.node_id, { resume: false, capacity: 'refuse' });
95
+ await reviveNode(node.node_id, { resume: false, capacity: 'refuse' });
96
96
  if (!await awaitCompanionBind(node.node_id, COMPANION_BIND_DEADLINE_MS)) {
97
97
  throw new Error(`feedback companion did not bind within ${COMPANION_BIND_DEADLINE_MS}ms`);
98
98
  }
@@ -3,13 +3,13 @@ import type { AttachBindingId, AttachMenuBindingId } from './attach-control.js';
3
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';
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
- 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' | 'search' | 'revive-now' | 'focus' | 'focus-background' | 'yank-id';
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' | 'pin' | 'search' | 'revive-now' | 'focus' | 'focus-background' | 'yank-id';
7
7
  type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' | `slot.${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` | 'create' | 'search-existing' | 'use-root' | 'set-default' | 'add-directory' | 'unpin' | 'manage';
8
8
  type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance' | 'cell-previous' | 'cell-next';
9
9
  type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
10
10
  type SetupModelsAction = 'open' | 'commit' | 'cancel' | 'scope-next';
11
11
  export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | AttachMenuBindingId | `crtr.tmux.menu-global.${TmuxMenuEntryAction}` | `crtr.tmux.node.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | `crtr.attach.scroll.${'up' | 'down' | 'page-up' | 'page-down' | 'live'}` | 'crtr.attach.mouse.toggle' | 'crtr.attach.copy.message' | 'crtr.attach.help.toggle' | `crtr.attach.search.${'next' | 'previous' | 'edit' | 'close'}` | AttachBindingId | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.inspector.${'force-quit' | 'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}` | `crtr.setup.models.${SetupModelsAction}`;
12
- export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.attach.search.next"> | BindingDefinition<"crtr.attach.search.previous"> | BindingDefinition<"crtr.attach.search.edit"> | BindingDefinition<"crtr.attach.search.close"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.yank-id"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.menu.manage">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
12
+ export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.attach.search.next"> | BindingDefinition<"crtr.attach.search.previous"> | BindingDefinition<"crtr.attach.search.edit"> | BindingDefinition<"crtr.attach.search.close"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.pin"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.yank-id"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.menu.manage">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
13
13
  export declare const BINDING_IDS: ReadonlySet<string>;
14
14
  /** True for an action whose direct gesture is handled inside an attach viewer.
15
15
  * Tmux menu selectors and root shortcuts are chrome, not pane input. */
@@ -147,7 +147,8 @@ function browseBindings() {
147
147
  b('crtr.browse.scope', 'Browse and pickers', 'Change scope', 'Toggle the browse cwd scope.', ['c'], 'terminal', [nav]),
148
148
  b('crtr.browse.profile', 'Browse and pickers', 'Toggle profile', 'Toggle the browse profile scope.', ['shift+p'], 'terminal', [nav]),
149
149
  b('crtr.browse.residents', 'Browse and pickers', 'Toggle residents', 'Toggle resident display.', ['r'], 'terminal', [nav]),
150
- b('crtr.browse.preview', 'Browse and pickers', 'Toggle preview', 'Toggle the preview panel.', ['p'], 'terminal', [nav]),
150
+ b('crtr.browse.preview', 'Browse and pickers', 'Toggle preview', 'Toggle the preview panel.', ['v'], 'terminal', [nav]),
151
+ b('crtr.browse.pin', 'Browse and pickers', 'Pin node', 'Star or unstar the selected node.', ['p'], 'terminal', [nav]),
151
152
  b('crtr.browse.search', 'Browse and pickers', 'Search', 'Open browse search.', ['/'], 'terminal', [nav]),
152
153
  b('crtr.browse.revive-now', 'Browse and pickers', 'Revive now', 'Revive the selected node now.', ['shift+k'], 'terminal', [nav]),
153
154
  b('crtr.browse.focus', 'Browse and pickers', 'Focus node', 'Focus the selected node.', ['enter', 'o'], 'terminal', [nav]),
@@ -189,7 +189,7 @@ export function modelRequestFromConfig(model, modelIntent, exact) {
189
189
  if (exact)
190
190
  return { kind: 'exact', spec: model };
191
191
  if (modelIntent) {
192
- const slash = modelIntent.indexOf('/');
192
+ const slash = modelIntent.lastIndexOf('/');
193
193
  const strength = slash > 0 ? modelIntent.slice(slash + 1) : '';
194
194
  if (STRENGTHS.includes(strength))
195
195
  return { kind: 'logical', family: modelIntent.slice(0, slash), strength: strength, spec: model };
@@ -172,7 +172,7 @@ export async function realizeReview(reviewId) {
172
172
  // Realization is bounded by the bind deadline below, which only a live broker
173
173
  // can meet: refuse at the cap rather than freeze into a certain timeout.
174
174
  if (companion.pi_session_id === null)
175
- reviveNode(companion.node_id, { resume: false, capacity: 'refuse' });
175
+ await reviveNode(companion.node_id, { resume: false, capacity: 'refuse' });
176
176
  // R4: session_start is the durable bind boundary.
177
177
  if (!await awaitCompanionBind(review.companion_node_id, COMPANION_BIND_DEADLINE_MS)) {
178
178
  throw companionUnbound(review.review_id, COMPANION_BIND_DEADLINE_MS);
@@ -3,6 +3,7 @@ import { envModelIntent } from '../../../shared/env.js';
3
3
  import { emitEvent } from '../../events/emit.js';
4
4
  import { clearFault, readFault } from '../fault.js';
5
5
  import { isUnknownModel } from '../model-selection.js';
6
+ import { announceMessageOrigin } from '../stamp/channel.js';
6
7
  export function createAuthReloadGate(deps) {
7
8
  const { nodeId, cfg, replies, rebind, session, services, broadcastModelChanged, formatModelSpec, resolveLaunchModel, driveKickoff, turnAdmission, } = deps;
8
9
  let reloadInFlight = null;
@@ -118,7 +119,9 @@ export function createAuthReloadGate(deps) {
118
119
  return Promise.resolve();
119
120
  }
120
121
  clearFault(nodeId, { link: 'pi→provider' });
121
- return faultSession.prompt(formatCard('recovery', {}, AUTH_FAULT_RECOVERY_BODY), { preflightResult: release });
122
+ const card = formatCard('recovery', {}, AUTH_FAULT_RECOVERY_BODY);
123
+ announceMessageOrigin(card, { type: 'runtime', kind: 'recovery' });
124
+ return faultSession.prompt(card, { preflightResult: release });
122
125
  });
123
126
  }
124
127
  catch (err) {
@@ -1,3 +1,4 @@
1
+ import type { MessageOrigin } from '../stamp/protocol.js';
1
2
  import type { ClientToBroker } from '../broker-protocol.js';
2
3
  import type { BrokerClient, BrokerClientRegistry } from './client-registry.js';
3
4
  import type { EventProjection } from './event-projection.js';
@@ -9,9 +10,18 @@ import type { RebindDriver } from './rebind.js';
9
10
  import type { ToolGroupTracker } from './tool-groups.js';
10
11
  import type { PromptDispatchTracker } from './turn-ignition.js';
11
12
  import type { TurnAdmissionGate } from './turn-admission.js';
12
- /** The frames that drive the live engine — the viewer's input vocabulary. */
13
- export type DriveFrame = Extract<ClientToBroker, {
14
- type: 'prompt' | 'steer' | 'follow_up' | 'abort' | 'bash';
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. */
16
+ type DeliveredInput = {
17
+ delivery?: {
18
+ origin?: MessageOrigin;
19
+ };
20
+ };
21
+ export type DriveFrame = (Extract<ClientToBroker, {
22
+ type: 'prompt' | 'follow_up';
23
+ }> & DeliveredInput) | Extract<ClientToBroker, {
24
+ type: 'steer' | 'abort' | 'bash';
15
25
  }>;
16
26
  export type EngineDriver = (client: BrokerClient, frame: DriveFrame) => Promise<unknown> | undefined;
17
27
  export declare function createEngineDriver(deps: {
@@ -26,3 +36,4 @@ export declare function createEngineDriver(deps: {
26
36
  toolGroups: ToolGroupTracker;
27
37
  turnAdmission: TurnAdmissionGate;
28
38
  }): EngineDriver;
39
+ export {};
@@ -2,6 +2,8 @@ import { emitEvent } from '../../events/emit.js';
2
2
  import { operationIdContext } from '../../events/operation-id.js';
3
3
  import { promptWithAdvertisedCommandInvocation } from '../advertised-command-invocation.js';
4
4
  import { heldDeferredInboxPromptJoin } from './held-deferred-inbox.js';
5
+ import { announceMessageOrigin } from '../stamp/channel.js';
6
+ import { clearTurnVisibility, currentTurnVisibility, setTurnVisibility, } from '../turn-visibility.js';
5
7
  import { chooseGuidanceDeliveryMode, promptWithNextTurnGuidance, resolveEngineRoute, resolveSteerCall, } from './engine-routing.js';
6
8
  export function createEngineDriver(deps) {
7
9
  const { registry, replies, session, dispatches, memoryRefs, authReloadGate, rebind, projection, toolGroups, turnAdmission, } = deps;
@@ -47,6 +49,20 @@ export function createEngineDriver(deps) {
47
49
  const route = resolveEngineRoute(frame, session().isStreaming);
48
50
  const promptOptions = route.call === 'prompt' ? route.options : undefined;
49
51
  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
+ const announceDelivery = () => {
57
+ if (frame.delivery === undefined)
58
+ 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);
65
+ };
50
66
  const prompt = async () => {
51
67
  // Both human surfaces converge at this admitted boundary. The watcher
52
68
  // claims held deferred mail before the prompt body, but its cursor stays
@@ -60,7 +76,19 @@ export function createEngineDriver(deps) {
60
76
  // replay. A person's live prompt is not replayable, so it must run.
61
77
  }
62
78
  }
63
- await session().prompt(frame.text, { ...promptOptions, preflightResult: release });
79
+ 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
+ }
64
92
  };
65
93
  if (guidance === null) {
66
94
  if (route.call === 'prompt') {