@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
@@ -0,0 +1,182 @@
1
+ import assert from 'node:assert/strict';
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import test from 'node:test';
5
+ import { contextDir, jobDir, nodeDir, reportsDir } from '../../canvas/paths.js';
6
+ import { validateHookManifest } from '../../command-hooks/schema.js';
7
+ import { headlessBrokerHost } from '../../runtime/host.js';
8
+ import { reviveNode } from '../../runtime/revive.js';
9
+ import { createHarness } from '../helpers/harness.js';
10
+ function installLifecyclePlugin(harness, name, op) {
11
+ const ops = typeof op === 'string' ? [op] : op;
12
+ const root = join(harness.agentHome, '.crouter', 'plugins', name);
13
+ mkdirSync(join(root, '.crouter-plugin'), { recursive: true });
14
+ writeFileSync(join(root, '.crouter-plugin', 'plugin.json'), JSON.stringify({
15
+ name,
16
+ version: '0.1.0',
17
+ description: 'lifecycle hook fixture',
18
+ hooks: 'hooks.json',
19
+ hookExecutable: 'bin/hook.js',
20
+ }));
21
+ writeFileSync(join(root, 'hooks.json'), JSON.stringify({
22
+ schemaVersion: 2,
23
+ lifecycle: ops.map((entry) => ({ event: 'node:start', phase: 'on', op: entry, description: `${entry} fixture`, effects: ['writes fixture state'] })),
24
+ }));
25
+ const executable = join(root, 'bin', 'hook.js');
26
+ mkdirSync(join(root, 'bin'), { recursive: true });
27
+ writeFileSync(executable, `#!/usr/bin/env node
28
+ const { existsSync, writeFileSync } = require('node:fs');
29
+ const { join } = require('node:path');
30
+ let input = '';
31
+ process.stdin.on('data', (chunk) => { input += chunk; });
32
+ process.stdin.on('end', () => {
33
+ const request = JSON.parse(input);
34
+ writeFileSync(join(request.node.contextDir, 'lifecycle-order'), request.op + '\\n', { flag: 'a' });
35
+ if (request.op === 'record') writeFileSync(join(request.node.contextDir, 'lifecycle-request.json'), JSON.stringify(request));
36
+ if (request.op === 'hang') {
37
+ writeFileSync(join(request.node.contextDir, 'lifecycle-hang-started'), 'started');
38
+ setInterval(() => {}, 1000);
39
+ return;
40
+ }
41
+ if (request.op === 'bad') {
42
+ process.stdout.write('not a JSON envelope');
43
+ return;
44
+ }
45
+ if (request.op === 'nonzero') {
46
+ writeFileSync(join(request.node.contextDir, 'lifecycle-nonzero-started'), 'started');
47
+ process.stdout.write(JSON.stringify({ protocolVersion: 1, ok: true }));
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ process.stdout.write(JSON.stringify({ protocolVersion: 1, ok: true }));
52
+ });
53
+ `);
54
+ chmodSync(executable, 0o755);
55
+ }
56
+ function installV1CommandHook(harness) {
57
+ const root = join(harness.agentHome, '.crouter', 'plugins', 'z-v1-command');
58
+ mkdirSync(join(root, '.crouter-plugin'), { recursive: true });
59
+ writeFileSync(join(root, '.crouter-plugin', 'plugin.json'), JSON.stringify({
60
+ name: 'z-v1-command', version: '0.1.0', description: 'v1 command hook fixture', hooks: 'hooks.json', hookExecutable: 'bin/hook.js',
61
+ }));
62
+ writeFileSync(join(root, 'hooks.json'), JSON.stringify({
63
+ schemaVersion: 1,
64
+ hooks: [{ target: 'sys version', phase: 'before', op: 'v1_check', description: 'v1 command hook', effects: ['writes fixture state'] }],
65
+ }));
66
+ const executable = join(root, 'bin', 'hook.js');
67
+ mkdirSync(join(root, 'bin'), { recursive: true });
68
+ writeFileSync(executable, `#!/usr/bin/env node
69
+ const { writeFileSync } = require('node:fs');
70
+ const { join } = require('node:path');
71
+ let input = '';
72
+ process.stdin.on('data', (chunk) => { input += chunk; });
73
+ process.stdin.on('end', () => {
74
+ JSON.parse(input);
75
+ writeFileSync(join(__dirname, '..', 'v1-ran'), 'ran');
76
+ process.stdout.write(JSON.stringify({ protocolVersion: 1, ok: true }));
77
+ });
78
+ `);
79
+ chmodSync(executable, 0o755);
80
+ return join(root, 'v1-ran');
81
+ }
82
+ test('node:start hooks receive resolved paths before broker launch; invalid and hanging hooks do not block launch', async () => {
83
+ const harness = await createHarness({ headless: true, sessionPrefix: 'crtr-lifecycle-hooks' });
84
+ const originalLaunch = headlessBrokerHost.launch;
85
+ try {
86
+ installLifecyclePlugin(harness, 'aa-lifecycle-order', ['first', 'second']);
87
+ installLifecyclePlugin(harness, 'a-lifecycle-record', 'record');
88
+ installLifecyclePlugin(harness, 'b-lifecycle-bad', 'bad');
89
+ installLifecyclePlugin(harness, 'c-lifecycle-nonzero', 'nonzero');
90
+ installLifecyclePlugin(harness, 'd-lifecycle-hang', 'hang');
91
+ const v1Marker = installV1CommandHook(harness);
92
+ const show = harness.cli(null, ['--json', 'pkg', 'plugin', 'show', 'a-lifecycle-record', '--scope', 'user']);
93
+ assert.equal(show.code, 0, show.stderr);
94
+ assert.deepEqual(show.json.hooks.lifecycle, [{
95
+ event: 'node:start', phase: 'on', op: 'record', description: 'record fixture', effects: ['writes fixture state'],
96
+ }]);
97
+ const doctor = harness.cli(null, ['--json', 'sys', 'doctor', '--scope', 'user']);
98
+ assert.equal(doctor.code, 0, doctor.stderr);
99
+ const hookCheck = doctor.json.checks.find((check) => check.name === 'plugin:a-lifecycle-record:hooks');
100
+ assert.match(hookCheck?.message ?? '', /node:start \[on record; effects: writes fixture state\]/);
101
+ const nodeId = harness.fabricateBrokerNode({ id: 'lifecycle-hook-node', pi_pid: null, pi_session_id: null, pi_session_file: null });
102
+ const requestPath = join(contextDir(nodeId), 'lifecycle-request.json');
103
+ let requestExistedBeforeLaunch = false;
104
+ headlessBrokerHost.launch = ((id, ...args) => {
105
+ if (id === nodeId)
106
+ requestExistedBeforeLaunch = existsSync(requestPath);
107
+ return originalLaunch.call(headlessBrokerHost, id, ...args);
108
+ });
109
+ const startedAt = Date.now();
110
+ const starting = reviveNode(nodeId, { resume: false, capacity: 'freeze' });
111
+ const contender = await reviveNode(nodeId, { resume: false, capacity: 'freeze' });
112
+ const result = await starting;
113
+ const elapsedMs = Date.now() - startedAt;
114
+ await harness.awaitBoot(nodeId);
115
+ assert.equal(result.outcome, 'launched');
116
+ assert.equal(contender.outcome, 'already-live', 'a concurrent revive cannot cross the awaited hook gap');
117
+ assert.equal(requestExistedBeforeLaunch, true, 'the lifecycle hook finishes before broker launch');
118
+ assert.equal(existsSync(join(contextDir(nodeId), 'lifecycle-nonzero-started')), true, 'a non-zero executable ran without blocking launch');
119
+ assert.equal(existsSync(join(contextDir(nodeId), 'lifecycle-hang-started')), true, 'the hanging executable started and was subject to the lifecycle timeout');
120
+ assert.ok(elapsedMs >= 4_500 && elapsedMs < 8_000, `the 5s hanging hook was bounded before launch (${elapsedMs}ms)`);
121
+ assert.deepEqual(readFileSync(join(contextDir(nodeId), 'lifecycle-order'), 'utf8').trim().split('\n'), ['record', 'first', 'second', 'bad', 'nonzero', 'hang'], 'lifecycle hooks preserve plugin-name then declaration-index discovery order');
122
+ assert.equal(existsSync(v1Marker), false, 'a command-only v1 manifest adds no node-start subprocess');
123
+ const request = JSON.parse(readFileSync(requestPath, 'utf8'));
124
+ assert.equal(request.protocolVersion, 1);
125
+ assert.equal(request.op, 'record');
126
+ assert.equal(request.event, 'node:start');
127
+ assert.equal(request.phase, 'on');
128
+ assert.match(request.operationId, /^[0-9a-f]{32}$/);
129
+ assert.deepEqual(request.context, { cwd: harness.node(nodeId).cwd });
130
+ assert.deepEqual(request.node, {
131
+ id: nodeId,
132
+ name: nodeId,
133
+ kind: 'developer',
134
+ mode: 'base',
135
+ lifecycle: 'terminal',
136
+ cwd: harness.node(nodeId).cwd,
137
+ nodeDir: nodeDir(nodeId),
138
+ contextDir: contextDir(nodeId),
139
+ jobDir: jobDir(nodeId),
140
+ reportsDir: reportsDir(nodeId),
141
+ });
142
+ assert.deepEqual(request.runtime, { isBirth: true, canvasHome: harness.home, profile: null });
143
+ const v1Command = harness.cli(null, ['--json', 'sys', 'version']);
144
+ assert.equal(v1Command.code, 0, v1Command.stderr);
145
+ assert.equal(existsSync(v1Marker), true, 'a v1 command hook still executes around its declared command');
146
+ }
147
+ finally {
148
+ headlessBrokerHost.launch = originalLaunch;
149
+ await harness.dispose();
150
+ }
151
+ });
152
+ test('hooks.json v1 remains command-only while v2 accepts a lifecycle-only declaration', () => {
153
+ const v1 = validateHookManifest({
154
+ schemaVersion: 1,
155
+ hooks: [{ target: 'canvas list', phase: 'before', op: 'check', description: 'check', effects: ['writes a log'] }],
156
+ });
157
+ assert.deepEqual(v1, {
158
+ manifest: {
159
+ schemaVersion: 1,
160
+ hooks: [{ target: 'canvas list', phase: 'before', op: 'check', description: 'check', effects: ['writes a log'] }],
161
+ },
162
+ issues: [],
163
+ });
164
+ const v2 = validateHookManifest({
165
+ schemaVersion: 2,
166
+ lifecycle: [{ event: 'node:start', phase: 'on', op: 'prepare', description: 'prepare', effects: ['creates a directory'] }],
167
+ });
168
+ assert.deepEqual(v2.issues, []);
169
+ assert.deepEqual(v2.manifest, {
170
+ schemaVersion: 2,
171
+ hooks: [],
172
+ lifecycle: [{ event: 'node:start', phase: 'on', op: 'prepare', description: 'prepare', effects: ['creates a directory'] }],
173
+ });
174
+ for (const manifest of [
175
+ { schemaVersion: 2, hooks: [], lifecycle: [] },
176
+ { schemaVersion: 2, lifecycle: [{ event: 'node:stop', phase: 'on', op: 'prepare', description: 'prepare', effects: ['creates a directory'] }] },
177
+ { schemaVersion: 2, lifecycle: [{ event: 'node:start', phase: 'before', op: 'prepare', description: 'prepare', effects: ['creates a directory'] }] },
178
+ { schemaVersion: 2, lifecycle: [{ event: 'node:start', phase: 'on', op: 'prepare', description: 'prepare', effects: ['creates a directory'], unexpected: true }] },
179
+ ]) {
180
+ assert.equal(validateHookManifest(manifest).manifest, undefined, 'invalid v2 lifecycle declarations are rejected');
181
+ }
182
+ });
@@ -180,7 +180,8 @@ test('a resumed revive replays the persisted LaunchSpec (model, tools, extension
180
180
  pi_session_id: 'uuid-9',
181
181
  pi_session_file: sessionFile,
182
182
  launch: {
183
- model: 'anthropic/sonnet',
183
+ model: 'anthropic/claude-opus-5:high',
184
+ modelExact: true,
184
185
  tools: ['bash', 'read'],
185
186
  extensions: ['/ext/a.js', '/ext/b.js'],
186
187
  systemPrompt: 'You are a developer orchestrator.',
@@ -196,7 +197,8 @@ test('a resumed revive replays the persisted LaunchSpec (model, tools, extension
196
197
  assert.equal(argv[ea - 1], '-e');
197
198
  // Model + tools + system prompt + resume target.
198
199
  const mi = argv.indexOf('--model');
199
- assert.equal(argv[mi + 1], 'anthropic/sonnet');
200
+ assert.equal(argv[mi + 1], 'anthropic/claude-opus-5:high');
201
+ assert.ok(argv.includes('--crtr-model-exact'), 'revive preserves the exact-model contract');
200
202
  const ti = argv.indexOf('--tools');
201
203
  assert.equal(argv[ti + 1], 'bash,read');
202
204
  assert.ok(argv.includes('--append-system-prompt'), 'system prompt replayed');
@@ -229,7 +231,7 @@ test('reviveNode no-ops when the node has a live fleet entry — double-revive g
229
231
  });
230
232
  h.fleet.register(M, { pid: 424242, exited: new Promise(() => { }) });
231
233
  const before = h.node(M).cycles ?? 0;
232
- const res = reviveNode(M, { resume: true, capacity: 'freeze' });
234
+ const res = await reviveNode(M, { resume: true, capacity: 'freeze' });
233
235
  assert.equal(res.resumed, false, 'guard path does not re-resume');
234
236
  assert.equal(h.node(M).cycles ?? 0, before, 'cycle counter NOT bumped (guard returned early)');
235
237
  }
@@ -257,7 +259,7 @@ test('reviveNode PROCEEDS when the node has NO fleet entry — cycle counter bum
257
259
  pi_session_file: sessionFile,
258
260
  });
259
261
  assert.equal(h.node(M).cycles ?? 0, 0, 'fabricated at cycle 0 (no revive yet)');
260
- reviveNode(M, { resume: true, capacity: 'freeze' });
262
+ await reviveNode(M, { resume: true, capacity: 'freeze' });
261
263
  const after = h.node(M);
262
264
  assert.equal(after.cycles, 1, 'a real (non-guard) revive bumped the cycle counter to 1');
263
265
  assert.notEqual(after.pi_pid, stalePid, 'revive records the fresh broker pid immediately, not the stale dead pid');
@@ -301,11 +303,11 @@ test('a recovery relaunch keeps the armed deadline; a wake revive consumes it',
301
303
  try {
302
304
  const recovered = h.fabricateBrokerNode({ status: 'active', intent: null, pi_pid: deadPid(), pi_session_id: 'uuid-1' });
303
305
  armDeadline(recovered, 'cron-recovery');
304
- reviveNode(recovered, { resume: true, recovery: true, capacity: 'freeze' });
306
+ await reviveNode(recovered, { resume: true, recovery: true, capacity: 'freeze' });
305
307
  assert.equal(hasPendingCancelOnWakeCron(recovered), true, 'the daemon replacing a dead broker is not a wake — the deadline survives so the wait can still settle');
306
308
  const woken = h.fabricateBrokerNode({ status: 'active', intent: null, pi_pid: deadPid(), pi_session_id: 'uuid-2' });
307
309
  armDeadline(woken, 'cron-wake');
308
- reviveNode(woken, { resume: true, capacity: 'freeze' });
310
+ await reviveNode(woken, { resume: true, capacity: 'freeze' });
309
311
  assert.equal(hasPendingCancelOnWakeCron(woken), false, 'a wake revive won the race and cancels the deadline');
310
312
  }
311
313
  finally {
@@ -356,7 +358,7 @@ test('reviveNode fresh-fallback clears the stale session identity (Major-3: no p
356
358
  pi_session_id: 'uuid-stale',
357
359
  pi_session_file: missingFile,
358
360
  });
359
- const res = reviveNode(M, { resume: true, capacity: 'freeze' });
361
+ const res = await reviveNode(M, { resume: true, capacity: 'freeze' });
360
362
  assert.equal(res.resumed, false, 'a missing .jsonl forces a fresh (non-resuming) launch');
361
363
  const after = h.node(M);
362
364
  assert.equal(after.pi_session_id, null, 'stale pi_session_id cleared before the fresh launch');
@@ -368,7 +370,7 @@ test('reviveNode fresh-fallback clears the stale session identity (Major-3: no p
368
370
  await h.dispose();
369
371
  }
370
372
  });
371
- test('unbound forks replay one durable source file for node-id, path, and UUID operands', () => {
373
+ test('unbound forks replay one durable source file for node-id, path, and UUID operands', async () => {
372
374
  const originalLaunch = headlessBrokerHost.launch;
373
375
  const tf = bindTestFleet();
374
376
  const forkFile = join(home, 'ordinary-fork-source.jsonl');
@@ -402,7 +404,7 @@ test('unbound forks replay one durable source file for node-id, path, and UUID o
402
404
  deleteNode('ordinary-fork-source');
403
405
  for (let index = 0; index < operands.length; index++) {
404
406
  const destination = `ordinary-fork-${index}`;
405
- reviveNode(destination, { resume: index !== 1, capacity: 'freeze' });
407
+ await reviveNode(destination, { resume: index !== 1, capacity: 'freeze' });
406
408
  const argv = launches[index];
407
409
  const fork = argv.indexOf('--fork');
408
410
  assert.equal(argv[fork + 1], forkFile, `${operands[index]} retries from the durable resolved file`);
@@ -435,7 +437,7 @@ test('reviveNode throws a typed cwd_missing error and does NOT crash the node wh
435
437
  pi_pid: deadPid(),
436
438
  });
437
439
  updateNode(M, { cwd: missingCwd });
438
- assert.throws(() => reviveNode(M, { resume: true, capacity: 'freeze' }), (e) => e instanceof InputError && e.payload.error === 'cwd_missing' && e.message.includes(missingCwd), 'reviveNode must throw a typed InputError naming the missing cwd path');
440
+ await assert.rejects(reviveNode(M, { resume: true, capacity: 'freeze' }), (e) => e instanceof InputError && e.payload.error === 'cwd_missing' && e.message.includes(missingCwd), 'reviveNode must throw a typed InputError naming the missing cwd path');
439
441
  const after = h.node(M);
440
442
  assert.equal(after.status, 'active', 'a cwd-missing preflight refusal must NOT crash the node (fixable in place, not a dead engine)');
441
443
  }
@@ -453,7 +455,7 @@ test('reviveNode relaunches fresh when the session header cwd was removed (NL-14
453
455
  const M = h.fabricateBrokerNode({
454
456
  status: 'active', intent: null, pi_pid: deadPid(), pi_session_id: 'stale-session', pi_session_file: sessionFile,
455
457
  });
456
- const result = reviveNode(M, { resume: true, capacity: 'freeze', recovery: true });
458
+ const result = await reviveNode(M, { resume: true, capacity: 'freeze', recovery: true });
457
459
  assert.equal(result.resumed, false, 'the missing session cwd vetoes --session and launches fresh');
458
460
  assert.equal(h.node(M)?.pi_session_id, null, 'the stale session id is cleared before the fresh launch');
459
461
  assert.equal(h.node(M)?.pi_session_file, null, 'the stale session file is cleared before the fresh launch');
@@ -477,7 +479,7 @@ test('reviveNode crashes and clears pid when launch/preflight throws', async ()
477
479
  pi_session_file: sessionFile,
478
480
  });
479
481
  headlessBrokerHost.launch = (_nodeId, _inv, _opts) => { throw new Error('boom'); };
480
- assert.throws(() => reviveNode(M, { resume: true, capacity: 'freeze' }), /refusing to revive/i);
482
+ await assert.rejects(reviveNode(M, { resume: true, capacity: 'freeze' }), /refusing to revive/i);
481
483
  const after = h.node(M);
482
484
  assert.equal(after.status, 'dead', 'failed revive marks the node dead');
483
485
  assert.equal(after.pi_pid, null, 'failed revive clears the stale pid');
@@ -510,7 +512,7 @@ test('reviveNode fans a doctrine wake to subscribers when launch/preflight throw
510
512
  // was fabricated directly, not spawned through the CLI.
511
513
  subscribe(root, M, true);
512
514
  headlessBrokerHost.launch = (_nodeId, _inv, _opts) => { throw new Error('boom'); };
513
- assert.throws(() => reviveNode(M, { resume: true, capacity: 'freeze' }), /refusing to revive/i);
515
+ await assert.rejects(reviveNode(M, { resume: true, capacity: 'freeze' }), /refusing to revive/i);
514
516
  assert.equal(h.node(M).status, 'dead', 'failed revive marks the node dead');
515
517
  const entries = readInboxSince(root, undefined);
516
518
  const notice = entries.find((e) => e.data?.['reason'] === 'child-crashed' && e.data?.['child'] === M);
@@ -530,7 +532,7 @@ test('reviveNode fans a doctrine wake to subscribers when launch/preflight throw
530
532
  // nothing. It woke, flailed over an empty context dir and reported itself
531
533
  // confused. Such a relaunch is a BIRTH: replay the spawn mandate if there is
532
534
  // one, else boot promptless and take no turn at all.
533
- test('an unstarted node relaunches as a birth, not a "revived fresh" kickoff', () => {
535
+ test('an unstarted node relaunches as a birth, not a "revived fresh" kickoff', async () => {
534
536
  const originalLaunch = headlessBrokerHost.launch;
535
537
  const tf = bindTestFleet();
536
538
  const launches = [];
@@ -545,19 +547,19 @@ test('an unstarted node relaunches as a birth, not a "revived fresh" kickoff', (
545
547
  const missing = join(home, 'sessions', 'never-written.jsonl');
546
548
  // (1) Bare interactive node — nothing on disk anywhere: launch promptless.
547
549
  createNode(node('bare-unstarted', { cwd: home, pi_session_id: 'uuid-a', pi_session_file: missing }));
548
- reviveNode('bare-unstarted', { resume: true, capacity: 'freeze' });
550
+ await reviveNode('bare-unstarted', { resume: true, capacity: 'freeze' });
549
551
  const bare = launches[0];
550
552
  assert.equal(bare.some((a) => isReviveKickoff(a)), false, 'an unstarted node is never told it lost context it never had');
551
553
  assert.deepEqual(bare, buildPiArgv(getNode('bare-unstarted'), {}).argv, 'a promptless boot — the argv carries no trailing prompt positional at all');
552
554
  // (2) Spawned with a mandate whose first turn never persisted: replay it.
553
555
  createNode(node('mandate-unstarted', { cwd: home, pi_session_id: 'uuid-b', pi_session_file: missing }));
554
556
  writeGoal('mandate-unstarted', 'do the thing');
555
- reviveNode('mandate-unstarted', { resume: true, capacity: 'freeze' });
557
+ await reviveNode('mandate-unstarted', { resume: true, capacity: 'freeze' });
556
558
  assert.equal(launches[1].at(-1), 'do the thing', 'a birth replays the spawn mandate');
557
559
  // (3) Real durable progress on disk → still a genuine fresh revive.
558
560
  createNode(node('progressed', { cwd: home, pi_session_id: 'uuid-c', pi_session_file: missing }));
559
561
  seedRoadmap('progressed', { goal: 'ship it' });
560
- reviveNode('progressed', { resume: true, capacity: 'freeze' });
562
+ await reviveNode('progressed', { resume: true, capacity: 'freeze' });
561
563
  assert.equal(isReviveKickoff(launches[2].at(-1)), true, 'a node with a roadmap still rebuilds from the kickoff');
562
564
  }
563
565
  finally {
@@ -20,7 +20,7 @@ import { join } from 'node:path';
20
20
  import { buildLaunchSpec, isProviderPinnedModelToken, normalizeModel } from '../runtime/launch.js';
21
21
  import { defaultModelLaddersConfig } from '../../types.js';
22
22
  import { persistDormantModel } from '../runtime/model-swap.js';
23
- import { promote } from '../runtime/promote.js';
23
+ import { promote, reshapeNode } from '../runtime/promote.js';
24
24
  import { createNode, getNode } from '../canvas/canvas.js';
25
25
  import { closeDb } from '../canvas/db.js';
26
26
  const ladders = defaultModelLaddersConfig();
@@ -86,6 +86,15 @@ test('only concrete provider/id tokens pin; logical family/tier refs remain pref
86
86
  assert.equal(isProviderPinnedModelToken('ultra'), false, 'a bare tier has no `/` and stays unpinned');
87
87
  assert.equal(isProviderPinnedModelToken('opus'), false, 'a bare family alias has no `/` and stays unpinned');
88
88
  });
89
+ test('a new --model override resolves once and freezes portable and concrete requests', () => {
90
+ for (const requested of ['anthropic/opus', 'strong', 'anthropic/claude-fable-5:high']) {
91
+ const launch = buildSpec({ model: requested, modelOverride: true });
92
+ assert.equal(launch.model, normalizeModel(requested, ladders), `${requested} resolves through the configured ladder`);
93
+ assert.equal(launch.modelExact, true, `${requested} is exact after resolution`);
94
+ assert.equal(launch.modelIntent, undefined, `${requested} has no portable route intent`);
95
+ assert.equal(launch.env.CRTR_MODEL_INTENT, undefined, `${requested} cannot re-enable fallback on revive`);
96
+ }
97
+ });
89
98
  // Direction 1: an EXPLICIT concrete pin must SURVIVE a config/lifecycle rebuild
90
99
  // even when the carried override has portablized back to a bare tier.
91
100
  test('an explicit concrete pin survives a config/lifecycle rebuild whose portablized override reads as a bare tier', () => {
@@ -103,6 +112,20 @@ test('an explicit concrete pin survives a config/lifecycle rebuild whose portabl
103
112
  // Direction 2: a deliberately TIER-run (unpinned) node must NOT be wrongly
104
113
  // re-pinned when a subsequent rebuild/persist normalizes its override to the
105
114
  // concrete cell it happens to share bytes with.
115
+ test('reshape resolves and persists a new --model override as the exact revive recipe', () => {
116
+ createNode(node('override-node', {
117
+ model_override: 'strong',
118
+ launch: { extensions: [], env: { CRTR_MODEL_INTENT: 'anthropic/strong' }, model: normalizeModel('strong', ladders), modelIntent: { family: 'anthropic', strength: 'strong' } },
119
+ }));
120
+ reshapeNode('override-node', { model: 'anthropic/opus' });
121
+ const after = getNode('override-node');
122
+ const expected = normalizeModel('anthropic/opus', ladders);
123
+ assert.equal(after?.model_override, expected, 'the raw alias never becomes the durable override');
124
+ assert.equal(after?.launch?.model, expected, 'revive replays the resolved provider/model');
125
+ assert.equal(after?.launch?.modelExact, true, 'retries and revives reject substitutes');
126
+ assert.equal(after?.launch?.modelIntent, undefined);
127
+ assert.equal(after?.launch?.env.CRTR_MODEL_INTENT, undefined);
128
+ });
106
129
  test('promotion preserves the exact-model contract required to revive a private human-work fork', () => {
107
130
  const exactModel = 'openai-codex/gpt-5.6-sol:max';
108
131
  createNode(node('exact-node', {
@@ -134,6 +157,7 @@ test('a deliberately tier-run node is not wrongly re-pinned when a rebuild or pe
134
157
  const after = getNode('tier-node');
135
158
  assert.equal(after?.launch?.model, ULTRA_CELL, 'the concrete spec is persisted as the launch model');
136
159
  assert.equal(after?.model_override, ULTRA_CELL, 'the concrete spec is persisted as the override');
160
+ assert.equal(after?.launch?.modelExact, true, 'an explicit model change remains exact on revive');
137
161
  assert.equal(after?.launch?.modelIntent, undefined, 'an explicit concrete choice clears the prior logical route intent');
138
162
  assert.equal(after?.launch?.env.CRTR_MODEL_INTENT, undefined, 'the revived broker does not inherit the prior logical route intent');
139
163
  assert.equal(after?.launch?.modelProviderPinned, false, 'an explicit bare-tier decision clears a prior pin — the raw-token decision wins over the concrete spec shape');
@@ -179,6 +179,6 @@ test('reviveNode refuses to revive a node that became finalized after the caller
179
179
  process.env['CRTR_NODE_ID'] = id;
180
180
  await pushFinal(id, 'concurrent final');
181
181
  assert.equal(getNode(id)?.status, 'done');
182
- assert.throws(() => reviveNode(id, { resume: true, capacity: 'freeze' }), /finaliz/i, 'reviveNode\'s own belt-and-suspenders check refuses a now-latched target');
182
+ await assert.rejects(reviveNode(id, { resume: true, capacity: 'freeze' }), /finaliz/i, 'reviveNode\'s own belt-and-suspenders check refuses a now-latched target');
183
183
  assert.equal(getNode(id)?.status, 'done', 'the node stays done, never silently revived into an unsatisfiable mandate');
184
184
  });
@@ -18,6 +18,7 @@ import { createNode, getNode, subscribe } from '../canvas/canvas.js';
18
18
  import { closeDb } from '../canvas/db.js';
19
19
  import { apiSocketPath } from '../canvas/paths.js';
20
20
  import { brokerCapReached, resetFleetForTests } from '../runtime/fleet.js';
21
+ import { buildPiArgv } from '../runtime/launch.js';
21
22
  import { relaunchRoot } from '../runtime/reset.js';
22
23
  import { bindTestFleet } from './helpers/fleet.js';
23
24
  let home;
@@ -106,6 +107,22 @@ test('relaunchRoot parks the old root done and mints a fresh active root, reapin
106
107
  assert.equal(getNode('child')?.status, 'canceled', 'descendant child canceled');
107
108
  assert.equal(getNode('grand')?.status, 'canceled', 'descendant grandchild canceled');
108
109
  });
110
+ test('relaunchRoot preserves an explicit model override as an exact replay recipe', async () => {
111
+ const exactModel = 'anthropic/claude-opus-5:high';
112
+ createNode(node('root', {
113
+ parent: null,
114
+ lifecycle: 'resident',
115
+ model_override: exactModel,
116
+ launch: { extensions: [], env: {}, model: exactModel, modelExact: true },
117
+ }));
118
+ const { deps } = makeDeps(4242);
119
+ const result = await relaunchRoot('root', deps);
120
+ assert.ok(result);
121
+ const replacement = getNode(result.newNodeId);
122
+ assert.equal(replacement?.launch?.model, exactModel);
123
+ assert.equal(replacement?.launch?.modelExact, true, 'the replacement cannot route away from the override');
124
+ assert.ok(buildPiArgv(replacement, {}).argv.includes('--crtr-model-exact'), 'broker replay preserves the exact-model contract');
125
+ });
109
126
  test('relaunchRoot on a boot failure leaves the old root + descendants fully intact', async () => {
110
127
  // The mint-and-boot-FIRST ordering: a launch failure (pid=null) must occur
111
128
  // BEFORE the old root is parked or its descendants reaped.
@@ -52,9 +52,9 @@ function node(id, over = {}) {
52
52
  return id;
53
53
  }
54
54
  /** Fill the ledger to the cap with live brokers. */
55
- function saturate() {
56
- reviveNode(node(`filler-a-${seq}`), { resume: false, capacity: 'refuse' });
57
- reviveNode(node(`filler-b-${seq}`), { resume: false, capacity: 'refuse' });
55
+ async function saturate() {
56
+ await reviveNode(node(`filler-a-${seq}`), { resume: false, capacity: 'refuse' });
57
+ await reviveNode(node(`filler-b-${seq}`), { resume: false, capacity: 'refuse' });
58
58
  assert.equal(tf.fleet.size(), 2, 'precondition: the ledger is at its cap of 2');
59
59
  }
60
60
  before(() => {
@@ -78,10 +78,10 @@ after(() => {
78
78
  delete process.env['CRTR_HOME'];
79
79
  rmSync(home, { recursive: true, force: true });
80
80
  });
81
- test('a refusing caller gets an error naming the cap and the live count, never a silent no-op', () => {
82
- saturate();
81
+ test('a refusing caller gets an error naming the cap and the live count, never a silent no-op', async () => {
82
+ await saturate();
83
83
  const turned = node('refused', { pi_session_id: 'sess-refused' });
84
- assert.throws(() => reviveNode(turned, { resume: true, capacity: 'refuse' }), (err) => {
84
+ await assert.rejects(() => reviveNode(turned, { resume: true, capacity: 'refuse' }), (err) => {
85
85
  const details = err.details ?? {};
86
86
  assert.equal(details['error'], 'broker_cap_reached', 'the refusal is a typed InputError, so it survives the /v1 error body');
87
87
  assert.match(String(details['message']), /2 live brokers at the cap of 2/, 'the message names both the live count and the cap');
@@ -110,21 +110,21 @@ test('a pre-spawn launch failure releases the reservation', () => {
110
110
  stubLaunch();
111
111
  }
112
112
  });
113
- test('a freezing caller gets a frozen row, marked once and left otherwise untouched', () => {
114
- saturate();
113
+ test('a freezing caller gets a frozen row, marked once and left otherwise untouched', async () => {
114
+ await saturate();
115
115
  const waiting = node('frozen', { pi_session_id: 'sess-frozen' });
116
- const first = reviveNode(waiting, { resume: true, capacity: 'freeze' });
116
+ const first = await reviveNode(waiting, { resume: true, capacity: 'freeze' });
117
117
  assert.equal(first.outcome, 'frozen', 'the caller learns it got no engine');
118
118
  const marked = getNode(waiting);
119
119
  assert.ok(marked.frozen_at != null, 'the row carries the durable wait mark the thaw order reads');
120
120
  assert.equal(marked.pi_pid ?? null, null, 'a frozen row holds no pid — every status surface reads it as frozen, not stale-active');
121
121
  assert.equal(marked.cycles ?? 0, 0, 'a frozen node never spent a cycle');
122
- const second = reviveNode(waiting, { resume: true, capacity: 'freeze' });
122
+ const second = await reviveNode(waiting, { resume: true, capacity: 'freeze' });
123
123
  assert.equal(second.outcome, 'frozen');
124
124
  assert.equal(getNode(waiting).frozen_at, marked.frozen_at, 'freezing is once-per-episode: a second saturated tick keeps the original wait timestamp');
125
125
  });
126
126
  test('a durable daemon message reports capacity_frozen when its wake cannot launch', async () => {
127
- saturate();
127
+ await saturate();
128
128
  const waiting = node('message-frozen');
129
129
  const delivery = await deliverNodeMessage({
130
130
  node_id: waiting,
@@ -137,26 +137,26 @@ test('a durable daemon message reports capacity_frozen when its wake cannot laun
137
137
  assert.equal(delivery.revived, false);
138
138
  assert.equal(delivery.reason, 'capacity_frozen');
139
139
  });
140
- test('an already-live node is admitted at the cap — it consumes no new slot', () => {
140
+ test('an already-live node is admitted at the cap — it consumes no new slot', async () => {
141
141
  const live = node('already-live');
142
- reviveNode(live, { resume: false, capacity: 'refuse' });
143
- reviveNode(node('already-live-filler'), { resume: false, capacity: 'refuse' });
142
+ await reviveNode(live, { resume: false, capacity: 'refuse' });
143
+ await reviveNode(node('already-live-filler'), { resume: false, capacity: 'refuse' });
144
144
  assert.equal(tf.fleet.size(), 2, 'precondition: this node holds one of the two slots, and the ledger is full');
145
145
  // Both dispositions agree: liveness is answered before capacity ever is.
146
- assert.equal(reviveNode(live, { resume: true, capacity: 'refuse' }).outcome, 'already-live');
147
- assert.equal(reviveNode(live, { resume: true, capacity: 'freeze' }).outcome, 'already-live');
146
+ assert.equal((await reviveNode(live, { resume: true, capacity: 'refuse' })).outcome, 'already-live');
147
+ assert.equal((await reviveNode(live, { resume: true, capacity: 'freeze' })).outcome, 'already-live');
148
148
  assert.equal(getNode(live).frozen_at ?? null, null, 'a live node is never marked as waiting for a slot');
149
149
  });
150
150
  test('a freed slot lets the frozen row launch, and the launch retires the mark', async () => {
151
151
  const filler = node('filler-exits');
152
- reviveNode(filler, { resume: false, capacity: 'refuse' });
153
- reviveNode(node('filler-stays'), { resume: false, capacity: 'refuse' });
152
+ await reviveNode(filler, { resume: false, capacity: 'refuse' });
153
+ await reviveNode(node('filler-stays'), { resume: false, capacity: 'refuse' });
154
154
  const waiting = node('thaws', { pi_session_id: 'sess-thaws' });
155
- assert.equal(reviveNode(waiting, { resume: true, capacity: 'freeze' }).outcome, 'frozen');
155
+ assert.equal((await reviveNode(waiting, { resume: true, capacity: 'freeze' })).outcome, 'frozen');
156
156
  // A real exit frees the slot; its policy job runs on the lane like production.
157
157
  tf.fleet.deliverExit(filler, { code: 0, signal: null });
158
158
  await tf.drain();
159
- assert.equal(reviveNode(waiting, { resume: true, capacity: 'freeze' }).outcome, 'launched');
159
+ assert.equal((await reviveNode(waiting, { resume: true, capacity: 'freeze' })).outcome, 'launched');
160
160
  assert.equal(getNode(waiting).frozen_at ?? null, null, 'a launch is the definition of not-frozen');
161
161
  assert.equal(tf.fleet.has(waiting), true, 'the thawed node owns the freed slot');
162
162
  });
@@ -103,10 +103,10 @@ after(() => {
103
103
  else
104
104
  process.env['CRTR_HOME'] = priorHome;
105
105
  });
106
- test('a parked node with mail waiting re-enters fresh even when the caller asked to resume, and the marker clears', () => {
106
+ test('a parked node with mail waiting re-enters fresh even when the caller asked to resume, and the marker clears', async () => {
107
107
  parkResident('parked', { roadmap: '# Roadmap\n\n- one open item\n' });
108
108
  deliverMail('parked');
109
- const result = reviveNode('parked', { resume: true, capacity: 'freeze' });
109
+ const result = await reviveNode('parked', { resume: true, capacity: 'freeze' });
110
110
  assert.equal(result.resumed, false, 'the concluded transcript is not replayed');
111
111
  assert.deepEqual(placements(), [{ nodeId: 'parked', resuming: false }]);
112
112
  assert.equal(kickedOff(launches[0]), true, 'the fresh window is handed its bearings');
@@ -115,15 +115,15 @@ test('a parked node with mail waiting re-enters fresh even when the caller asked
115
115
  assert.equal(reopened.intent ?? null, null, 'the park marker clears on revival');
116
116
  assert.equal(reopened.cycle_pending, true, 'the fresh cycle is provisional until session_start confirms it');
117
117
  });
118
- test('a parked node grounded only by a goal still re-enters fresh when mail rides the revive', () => {
118
+ test('a parked node grounded only by a goal still re-enters fresh when mail rides the revive', async () => {
119
119
  parkResident('goal-only', { goal: 'Watch the deploy lane.' });
120
120
  deliverMail('goal-only');
121
- assert.equal(reviveNode('goal-only', { resume: true, capacity: 'freeze' }).resumed, false);
121
+ assert.equal((await reviveNode('goal-only', { resume: true, capacity: 'freeze' })).resumed, false);
122
122
  assert.equal(kickedOff(launches[0]), true);
123
123
  });
124
- test('an input-less revival of a parked node quietly resumes and injects no kickoff', () => {
124
+ test('an input-less revival of a parked node quietly resumes and injects no kickoff', async () => {
125
125
  parkResident('quiet', { roadmap: '# Roadmap\n\n- one open item\n' });
126
- const result = reviveNode('quiet', { resume: true, capacity: 'freeze' });
126
+ const result = await reviveNode('quiet', { resume: true, capacity: 'freeze' });
127
127
  assert.equal(result.resumed, true, 'nothing arrived, so there is nothing a fresh window would answer');
128
128
  assert.deepEqual(placements(), [{ nodeId: 'quiet', resuming: true }]);
129
129
  assert.equal(kickedOff(launches[0]), false, 'a quiet resume generates nothing');
@@ -132,29 +132,29 @@ test('an input-less revival of a parked node quietly resumes and injects no kick
132
132
  assert.equal(reopened.status, 'active');
133
133
  assert.equal(reopened.intent ?? null, null, 'the park marker clears on a quiet resume too');
134
134
  });
135
- test('deferred mail alone is not input, so the revival still resumes quietly', () => {
135
+ test('deferred mail alone is not input, so the revival still resumes quietly', async () => {
136
136
  parkResident('deferred', { roadmap: '# Roadmap\n\n- one open item\n' });
137
137
  deliverMail('deferred', 'deferred');
138
- assert.equal(reviveNode('deferred', { resume: true, capacity: 'freeze' }).resumed, true);
138
+ assert.equal((await reviveNode('deferred', { resume: true, capacity: 'freeze' })).resumed, true);
139
139
  assert.equal(kickedOff(launches[0]), false);
140
140
  assert.equal(readCursor('deferred'), undefined, 'the deferred entry rides the next natural cycle, unconsumed');
141
141
  });
142
- test('mail already covered by the cursor is not input, so the revival still resumes quietly', () => {
142
+ test('mail already covered by the cursor is not input, so the revival still resumes quietly', async () => {
143
143
  parkResident('stale', { roadmap: '# Roadmap\n\n- one open item\n' });
144
144
  writeCursor('stale', deliverMail('stale'));
145
- assert.equal(reviveNode('stale', { resume: true, capacity: 'freeze' }).resumed, true, 'only the UNSEEN suffix counts as input');
145
+ assert.equal((await reviveNode('stale', { resume: true, capacity: 'freeze' })).resumed, true, 'only the UNSEEN suffix counts as input');
146
146
  assert.equal(kickedOff(launches[0]), false);
147
147
  });
148
- test('a parked node with no goal and no roadmap resumes its transcript rather than waking amnesiac', () => {
148
+ test('a parked node with no goal and no roadmap resumes its transcript rather than waking amnesiac', async () => {
149
149
  parkResident('bare');
150
150
  deliverMail('bare');
151
- const result = reviveNode('bare', { resume: true, capacity: 'freeze' });
151
+ const result = await reviveNode('bare', { resume: true, capacity: 'freeze' });
152
152
  assert.equal(result.resumed, true, 'the degraded park has nothing on disk to ground a fresh window');
153
153
  assert.deepEqual(placements(), [{ nodeId: 'bare', resuming: true }]);
154
154
  });
155
- test('an ordinary done node with a roadmap still resumes — only the park marker vetoes', () => {
155
+ test('an ordinary done node with a roadmap still resumes — only the park marker vetoes', async () => {
156
156
  parkResident('finished', { roadmap: '# Roadmap\n\n- one open item\n' });
157
157
  transition('finished', 'revive');
158
158
  transition('finished', 'finish');
159
- assert.equal(reviveNode('finished', { resume: true, capacity: 'freeze' }).resumed, true);
159
+ assert.equal((await reviveNode('finished', { resume: true, capacity: 'freeze' })).resumed, true);
160
160
  });
@@ -117,7 +117,7 @@ test('a birth at the cap freezes instead of booting, survives saturated ticks, a
117
117
  const filler = await h.spawnHeadlessChild(root, 'terminal thaw filler');
118
118
  assert.equal(h.fleet.size(), 2, 'the parked target is non-live while the cap remains full');
119
119
  appendInbox(second, { from: root, tier: 'normal', kind: 'message', label: 'durable wake' });
120
- assert.equal(reviveNode(second, { resume: true, capacity: 'freeze' }).outcome, 'frozen');
120
+ assert.equal((await reviveNode(second, { resume: true, capacity: 'freeze' })).outcome, 'frozen');
121
121
  closeDb();
122
122
  const frozenTerminal = h.node(second);
123
123
  assert.equal(frozenTerminal.status, 'done', 'freezing preserves the terminal status');
@@ -144,7 +144,7 @@ test('a birth at the cap freezes instead of booting, survives saturated ticks, a
144
144
  created: new Date().toISOString(),
145
145
  pi_session_id: 'resident-drain-session',
146
146
  });
147
- assert.equal(reviveNode('resident-drain', { resume: true, capacity: 'freeze' }).outcome, 'frozen');
147
+ assert.equal((await reviveNode('resident-drain', { resume: true, capacity: 'freeze' })).outcome, 'frozen');
148
148
  await h.tick();
149
149
  closeDb();
150
150
  const drained = h.node('resident-drain');