@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.
- package/dist/api/client.d.ts +5 -1
- package/dist/api/client.js +6 -0
- package/dist/api/dto/broker.d.ts +11 -2
- package/dist/api/dto/broker.js +2 -2
- package/dist/api/dto/canvas.d.ts +53 -2
- package/dist/api/dto/nodes.d.ts +10 -2
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +50 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +46 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +6 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +7 -2
- package/dist/clients/attach/session/whip-achievements.d.ts +4 -0
- package/dist/clients/attach/session/whip-achievements.js +145 -0
- package/dist/clients/attach/slash/dispatch.js +5 -3
- package/dist/clients/attach/viewer.js +607 -606
- package/dist/commands/canvas-history/grep.js +3 -22
- package/dist/commands/canvas-history/read.js +5 -3
- package/dist/commands/canvas-history/search.js +3 -22
- package/dist/commands/canvas-history/shared.d.ts +8 -0
- package/dist/commands/canvas-history/shared.js +64 -1
- package/dist/commands/canvas-history/stats.d.ts +1 -0
- package/dist/commands/canvas-history/stats.js +68 -0
- package/dist/commands/canvas-history.js +4 -3
- package/dist/commands/node/create.js +2 -2
- package/dist/commands/pkg/plugin-inspect.js +2 -2
- package/dist/commands/pkg/plugin-manage.js +3 -3
- package/dist/commands/sys/config.js +19 -2
- package/dist/commands/sys/doctor.js +4 -3
- package/dist/commands/sys/panels/models-panel.js +3 -1
- package/dist/core/__tests__/human-deliver.test.js +1 -1
- package/dist/core/__tests__/integration/lifecycle-hooks.test.d.ts +1 -0
- package/dist/core/__tests__/integration/lifecycle-hooks.test.js +182 -0
- package/dist/core/__tests__/integration/revive.test.js +19 -17
- package/dist/core/__tests__/model-pin-durability.test.js +25 -1
- package/dist/core/__tests__/push-final-guard.test.js +1 -1
- package/dist/core/__tests__/relaunch-root.test.js +17 -0
- package/dist/core/__tests__/revive-capacity.test.js +20 -20
- package/dist/core/__tests__/revive-parked-fresh.test.js +14 -14
- package/dist/core/__tests__/seam/broker-cap-freeze.test.js +2 -2
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +19 -4
- package/dist/core/__tests__/seam/dormancy-release.test.js +7 -7
- package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +1 -1
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +2 -2
- package/dist/core/broker-client/__tests__/transport-relay.test.js +3 -1
- package/dist/core/broker-client/transport-relay.js +5 -1
- package/dist/core/canvas/__tests__/history-transcript.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/history-transcript.test.js +102 -0
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +13 -10
- package/dist/core/canvas/browse/model.js +16 -1
- package/dist/core/canvas/browse/render.d.ts +23 -3
- package/dist/core/canvas/browse/render.js +429 -64
- package/dist/core/canvas/canvas.js +12 -8
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.d.ts +36 -1
- package/dist/core/canvas/history.js +328 -7
- package/dist/core/canvas/labels.d.ts +2 -2
- package/dist/core/canvas/migrations.js +32 -0
- package/dist/core/canvas/node-recap.d.ts +23 -0
- package/dist/core/canvas/node-recap.js +67 -0
- package/dist/core/canvas/render-source.d.ts +33 -1
- package/dist/core/canvas/render-source.js +80 -22
- package/dist/core/canvas/types.d.ts +9 -0
- package/dist/core/command-hooks/discovery.d.ts +27 -1
- package/dist/core/command-hooks/discovery.js +52 -0
- package/dist/core/command-hooks/index.d.ts +4 -3
- package/dist/core/command-hooks/index.js +2 -1
- package/dist/core/command-hooks/lifecycle-catalog.d.ts +3 -0
- package/dist/core/command-hooks/lifecycle-catalog.js +4 -0
- package/dist/core/command-hooks/report.d.ts +11 -2
- package/dist/core/command-hooks/report.js +10 -1
- package/dist/core/command-hooks/schema.d.ts +16 -1
- package/dist/core/command-hooks/schema.js +128 -40
- package/dist/core/command-hooks/transport/exec-lifecycle.d.ts +12 -0
- package/dist/core/command-hooks/transport/exec-lifecycle.js +152 -0
- package/dist/core/config.js +13 -5
- package/dist/core/human/feedback-companion.js +1 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -1
- package/dist/core/model-routes.js +1 -1
- package/dist/core/review/realize.js +1 -1
- package/dist/core/runtime/broker/auth-reload.js +4 -1
- package/dist/core/runtime/broker/engine-drive.d.ts +14 -3
- package/dist/core/runtime/broker/engine-drive.js +29 -1
- package/dist/core/runtime/broker/fault-retry.js +7 -2
- package/dist/core/runtime/broker/frame-client.js +3 -0
- package/dist/core/runtime/broker/frame-dispatch.js +7 -0
- package/dist/core/runtime/broker/inbox.d.ts +12 -2
- package/dist/core/runtime/broker/inbox.js +68 -38
- package/dist/core/runtime/broker/rebind.js +7 -0
- package/dist/core/runtime/broker-protocol.d.ts +16 -1
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +27 -3
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/interactive-deliver.d.ts +8 -2
- package/dist/core/runtime/interactive-deliver.js +12 -3
- package/dist/core/runtime/launch-target.js +1 -1
- package/dist/core/runtime/launch.d.ts +12 -6
- package/dist/core/runtime/launch.js +19 -17
- package/dist/core/runtime/model-swap.d.ts +5 -4
- package/dist/core/runtime/model-swap.js +7 -5
- package/dist/core/runtime/node-read.d.ts +2 -0
- package/dist/core/runtime/node-read.js +18 -9
- package/dist/core/runtime/promote.js +2 -1
- package/dist/core/runtime/reset.js +4 -2
- package/dist/core/runtime/revive-all.d.ts +1 -1
- package/dist/core/runtime/revive-all.js +2 -2
- package/dist/core/runtime/revive.d.ts +2 -2
- package/dist/core/runtime/revive.js +71 -13
- package/dist/core/runtime/session-visibility.d.ts +10 -14
- package/dist/core/runtime/session-visibility.js +43 -30
- package/dist/core/runtime/spawn.d.ts +3 -3
- package/dist/core/runtime/spawn.js +20 -3
- package/dist/core/runtime/stamp/channel.d.ts +22 -0
- package/dist/core/runtime/stamp/channel.js +83 -0
- package/dist/core/runtime/stamp/protocol.d.ts +90 -0
- package/dist/core/runtime/stamp/protocol.js +113 -0
- package/dist/core/runtime/tmux-driver.d.ts +11 -1
- package/dist/core/runtime/tmux-driver.js +27 -11
- package/dist/core/runtime/turn-visibility.d.ts +10 -0
- package/dist/core/runtime/turn-visibility.js +31 -0
- package/dist/core/self-update.js +2 -5
- package/dist/core/tui/draw.d.ts +1 -0
- package/dist/core/tui/draw.js +5 -1
- package/dist/core/tui/markdown.d.ts +17 -0
- package/dist/core/tui/markdown.js +407 -0
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +2 -2
- package/dist/daemon/api/bridge.d.ts +3 -2
- package/dist/daemon/api/bridge.js +34 -9
- package/dist/daemon/api/handlers/attach.js +4 -4
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/canvas.js +62 -16
- package/dist/daemon/api/handlers/messages.js +14 -3
- package/dist/daemon/api/handlers/nodes.js +17 -7
- package/dist/daemon/api/map.js +2 -0
- package/dist/daemon/cron/sinks.js +3 -3
- package/dist/daemon/fleet.js +1 -1
- package/dist/daemon/messaging/node-message.js +16 -2
- package/dist/daemon/profile-delete.js +1 -1
- package/dist/daemon/reconcilers/broker-supervision.js +26 -9
- package/dist/daemon/reconcilers/live-obligation.d.ts +8 -1
- package/dist/daemon/reconcilers/live-obligation.js +17 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +6 -6
- package/dist/hook-authoring.d.ts +31 -1
- package/dist/hook-authoring.js +42 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +27 -2
- package/dist/pi-extensions/canvas-recap.js +4 -0
- package/dist/pi-extensions/canvas-stamp.d.ts +14 -0
- package/dist/pi-extensions/canvas-stamp.js +77 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -0
- package/dist/shared/__tests__/env-boundary.test.js +4 -2
- package/dist/shared/crtr-version.d.ts +2 -0
- package/dist/shared/crtr-version.js +18 -0
- package/dist/shared/env.d.ts +10 -0
- package/dist/shared/env.js +19 -4
- package/dist/shared/generated-context.d.ts +1 -2
- package/dist/shared/generated-context.js +3 -4
- package/dist/types.d.ts +10 -0
- package/dist/types.js +4 -0
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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(
|
|
29
|
-
issue('hook_manifest_invalid', '
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
+
}
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
|
119
|
-
// persists them. Project config must never acquire a
|
|
120
|
-
|
|
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.', ['
|
|
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.
|
|
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
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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') {
|