@north-light/crouter 0.3.218 → 0.3.220
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/clients/attach/viewer.js +368 -368
- package/dist/commands/sys/panels/broker-limits-panel.js +3 -3
- package/dist/core/__tests__/helpers/broker-clients.d.ts +1 -0
- package/dist/core/__tests__/helpers/broker-clients.js +1 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +37 -1
- package/dist/core/runtime/broker/client-registry.d.ts +6 -3
- package/dist/core/runtime/broker/client-registry.js +6 -4
- package/dist/core/runtime/broker/frame-dispatch.js +1 -0
- package/dist/core/runtime/broker-protocol.d.ts +4 -0
- package/dist/core/runtime/launch-config-worker.d.ts +1 -0
- package/dist/core/runtime/launch-config-worker.js +12 -0
- package/dist/core/runtime/launch.d.ts +16 -2
- package/dist/core/runtime/launch.js +55 -21
- package/dist/core/runtime/promote.d.ts +20 -2
- package/dist/core/runtime/promote.js +63 -59
- package/dist/core/runtime/recycle.js +2 -2
- package/dist/core/runtime/reset.js +2 -2
- package/dist/core/runtime/spawn.js +3 -3
- package/dist/core/runtime/warm-pool.js +9 -7
- package/dist/daemon/api/__tests__/bridge-heartbeat.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +30 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +30 -0
- package/dist/daemon/api/bridge.d.ts +6 -0
- package/dist/daemon/api/bridge.js +39 -2
- package/dist/daemon/api/handlers/nodes.js +16 -17
- package/dist/types.js +5 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -62,7 +62,7 @@ import { claimWarmSpare, deleteNode, getNode, listNodes, listWarmSpares, registe
|
|
|
62
62
|
import { recordedPidLiveness } from '../canvas/pid.js';
|
|
63
63
|
import { nowIso } from '../fs-utils.js';
|
|
64
64
|
import { spawnChildPrepared } from './spawn.js';
|
|
65
|
-
import {
|
|
65
|
+
import { buildLaunchSpecAsync } from './launch.js';
|
|
66
66
|
import { setModelLive } from './model-swap.js';
|
|
67
67
|
import { setSituationalContextLive } from './situational-live.js';
|
|
68
68
|
import { deliverBearingsLive } from './deliver-live.js';
|
|
@@ -100,8 +100,8 @@ function warmRecipeKey(cwd, profileId, launch) {
|
|
|
100
100
|
}
|
|
101
101
|
/** Resolve a request's frozen half. Both cwd and profile arrive already
|
|
102
102
|
* resolved from the create handler, so this is pure launch-spec construction. */
|
|
103
|
-
function resolveRecipe(request) {
|
|
104
|
-
const { launch } =
|
|
103
|
+
async function resolveRecipe(request) {
|
|
104
|
+
const { launch } = await buildLaunchSpecAsync(request.kind, request.mode, {
|
|
105
105
|
lifecycle: 'resident',
|
|
106
106
|
hasManager: false,
|
|
107
107
|
...(request.model !== null ? { model: request.model } : {}),
|
|
@@ -114,7 +114,7 @@ function resolveRecipe(request) {
|
|
|
114
114
|
* pool cannot serve it. The caller falls back to a cold spawn; a claim never
|
|
115
115
|
* fails the create. */
|
|
116
116
|
export async function claimWarmNode(request) {
|
|
117
|
-
const recipe = resolveRecipe(request);
|
|
117
|
+
const recipe = await resolveRecipe(request);
|
|
118
118
|
const created = nowIso();
|
|
119
119
|
// The row's stamp moves inside the claim transaction; meta.json (the identity
|
|
120
120
|
// source of truth `rebuildIndex` re-derives the row from) follows with the
|
|
@@ -261,7 +261,9 @@ export function refillWarmPool(request) {
|
|
|
261
261
|
// Mint at the kind's DEFAULT model and with NO situational context: both
|
|
262
262
|
// are claim-time properties, so baking either in would only narrow who the
|
|
263
263
|
// spare can serve.
|
|
264
|
-
|
|
264
|
+
void resolveRecipe({ ...request, model: null, situationalContext: null })
|
|
265
|
+
.then(mint)
|
|
266
|
+
.catch((err) => console.error(`[warm-pool] recipe resolution failed: ${err.message}`));
|
|
265
267
|
}
|
|
266
268
|
catch (err) {
|
|
267
269
|
console.error(`[warm-pool] recipe resolution failed: ${err.message}`);
|
|
@@ -349,7 +351,7 @@ export function prewarmRecentRecipes() {
|
|
|
349
351
|
// `general`/`base` is only the shape the spare BOOTS as — the key it is
|
|
350
352
|
// filed under excludes everything a claim can reshape, so any kind can
|
|
351
353
|
// claim it.
|
|
352
|
-
|
|
354
|
+
void resolveRecipe({
|
|
353
355
|
kind: 'general',
|
|
354
356
|
mode: 'base',
|
|
355
357
|
cwd: candidate.cwd,
|
|
@@ -357,7 +359,7 @@ export function prewarmRecentRecipes() {
|
|
|
357
359
|
model: null,
|
|
358
360
|
situationalContext: null,
|
|
359
361
|
name: null,
|
|
360
|
-
}));
|
|
362
|
+
}).then(mint).catch((err) => console.error(`[warm-pool] prewarm failed for ${candidate.cwd}: ${err.message}`));
|
|
361
363
|
}
|
|
362
364
|
catch (err) {
|
|
363
365
|
console.error(`[warm-pool] prewarm failed for ${candidate.cwd}: ${err.message}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
import { startRemoteAttachHeartbeat } from '../bridge.js';
|
|
5
|
+
class FakeSocket extends EventEmitter {
|
|
6
|
+
pings = 0;
|
|
7
|
+
terminated = 0;
|
|
8
|
+
ping() { this.pings += 1; }
|
|
9
|
+
terminate() { this.terminated += 1; }
|
|
10
|
+
}
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
test('remote attach heartbeat pings an idle viewer and keeps a ponging observer alive', async () => {
|
|
15
|
+
const ws = new FakeSocket();
|
|
16
|
+
const stop = startRemoteAttachHeartbeat(ws, 20);
|
|
17
|
+
await sleep(25);
|
|
18
|
+
assert.equal(ws.pings, 1, 'an idle attach receives a WebSocket ping before proxy idle timeout');
|
|
19
|
+
ws.emit('pong');
|
|
20
|
+
await sleep(10);
|
|
21
|
+
assert.equal(ws.terminated, 0, 'a pong renews the heartbeat without terminating the observer');
|
|
22
|
+
stop();
|
|
23
|
+
});
|
|
24
|
+
test('remote attach heartbeat drops only an unresponsive observer socket', async () => {
|
|
25
|
+
const ws = new FakeSocket();
|
|
26
|
+
const stop = startRemoteAttachHeartbeat(ws, 5);
|
|
27
|
+
await sleep(16);
|
|
28
|
+
assert.equal(ws.terminated, 1, 'two missed heartbeat intervals terminate the detached viewer');
|
|
29
|
+
stop();
|
|
30
|
+
});
|
|
@@ -169,6 +169,36 @@ test('A4a: WS attach welcome.snapshot is byte-identical to a direct ViewSocketCl
|
|
|
169
169
|
assert.equal(JSON.stringify(wsWelcome.snapshot), JSON.stringify(direct.welcome.snapshot), 'WS welcome.snapshot is byte-identical to the direct ViewSocketClient snapshot');
|
|
170
170
|
direct.close();
|
|
171
171
|
});
|
|
172
|
+
test('A4a regression: reconnecting an observer keeps the live broker instance', async () => {
|
|
173
|
+
const root = h.spawnRoot('a4 reconnect root');
|
|
174
|
+
const id = (await client.createNode({ kind: 'developer', parent: root, prompt: 'a4 reconnect worker' })).node_id;
|
|
175
|
+
await h.awaitBoot(id);
|
|
176
|
+
const pid = h.node(id)?.pi_pid;
|
|
177
|
+
const boots = h.bootCount(id);
|
|
178
|
+
const attachOnce = () => new Promise((resolveAttach, rejectAttach) => {
|
|
179
|
+
const ws = new WebSocket(`ws+unix://${sockPath}:/v1/nodes/${id}/attach`);
|
|
180
|
+
const timer = setTimeout(() => {
|
|
181
|
+
ws.terminate();
|
|
182
|
+
rejectAttach(new Error('timed out waiting for observer welcome'));
|
|
183
|
+
}, 15_000);
|
|
184
|
+
ws.on('open', () => ws.send(JSON.stringify({ type: 'hello', role: 'observer', client_id: `a4-reconnect-${Math.random()}` })));
|
|
185
|
+
ws.on('message', (data) => {
|
|
186
|
+
if (JSON.parse(data.toString('utf8'))['type'] !== 'welcome')
|
|
187
|
+
return;
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
ws.close();
|
|
190
|
+
resolveAttach();
|
|
191
|
+
});
|
|
192
|
+
ws.on('error', (error) => {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
rejectAttach(error);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
await attachOnce();
|
|
198
|
+
await attachOnce();
|
|
199
|
+
assert.equal(h.bootCount(id), boots, 'observer reconnect did not relaunch the broker');
|
|
200
|
+
assert.equal(h.node(id)?.pi_pid, pid, 'the in-flight broker instance was retained');
|
|
201
|
+
});
|
|
172
202
|
test('A4b: ?revive=0 on a dormant node refuses 409 without launching a broker', async () => {
|
|
173
203
|
const root = h.spawnRoot('a4b root');
|
|
174
204
|
// A dormant row: no live broker, no pi_pid, view.sock absent.
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http';
|
|
2
2
|
import type { Duplex } from 'node:stream';
|
|
3
|
+
import { type WebSocket } from 'ws';
|
|
4
|
+
/** Keep an otherwise quiet observer connection alive through common 60s proxy
|
|
5
|
+
* idle limits. A viewer only observes a broker, so a missed heartbeat may close
|
|
6
|
+
* this socket but must never affect the broker or its in-flight work. */
|
|
7
|
+
export declare const REMOTE_ATTACH_HEARTBEAT_MS = 25000;
|
|
8
|
+
export declare function startRemoteAttachHeartbeat(ws: WebSocket, intervalMs?: number): () => void;
|
|
3
9
|
/** Handle the HTTP upgrade for `GET /v1/nodes/{id}/attach`. A-9 matches the
|
|
4
10
|
* route, extracts `{id}`, and calls this with the raw upgrade triplet; this
|
|
5
11
|
* owns the WS handshake (its own noServer `wss` above) AND the spec §5.3
|
|
@@ -33,6 +33,40 @@ import { probeViewSocket, waitForBrokerViewSocket } from '../../core/runtime/vie
|
|
|
33
33
|
* client cap) so a remote peer cannot buffer 256MiB before the broker's 24MiB
|
|
34
34
|
* decoder rejects the message. */
|
|
35
35
|
const wss = new WebSocketServer({ noServer: true, maxPayload: BROKER_READ_CAPS.maxLineBytes });
|
|
36
|
+
/** Keep an otherwise quiet observer connection alive through common 60s proxy
|
|
37
|
+
* idle limits. A viewer only observes a broker, so a missed heartbeat may close
|
|
38
|
+
* this socket but must never affect the broker or its in-flight work. */
|
|
39
|
+
export const REMOTE_ATTACH_HEARTBEAT_MS = 25_000;
|
|
40
|
+
export function startRemoteAttachHeartbeat(ws, intervalMs = REMOTE_ATTACH_HEARTBEAT_MS) {
|
|
41
|
+
let alive = true;
|
|
42
|
+
let stopped = false;
|
|
43
|
+
const acknowledge = () => { alive = true; };
|
|
44
|
+
const stop = () => {
|
|
45
|
+
if (stopped)
|
|
46
|
+
return;
|
|
47
|
+
stopped = true;
|
|
48
|
+
clearInterval(timer);
|
|
49
|
+
ws.off('pong', acknowledge);
|
|
50
|
+
};
|
|
51
|
+
ws.on('pong', acknowledge);
|
|
52
|
+
const timer = setInterval(() => {
|
|
53
|
+
if (!alive) {
|
|
54
|
+
stop();
|
|
55
|
+
ws.terminate();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
alive = false;
|
|
59
|
+
try {
|
|
60
|
+
ws.ping();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
stop();
|
|
64
|
+
ws.terminate();
|
|
65
|
+
}
|
|
66
|
+
}, intervalMs);
|
|
67
|
+
timer.unref();
|
|
68
|
+
return stop;
|
|
69
|
+
}
|
|
36
70
|
/** A WS close reason is capped at 123 UTF-8 bytes by the protocol. */
|
|
37
71
|
function clampReason(reason) {
|
|
38
72
|
const buf = Buffer.from(reason, 'utf8');
|
|
@@ -106,6 +140,7 @@ function bridgeConnection(ws, nodeId) {
|
|
|
106
140
|
const decoder = new FrameDecoder(CLIENT_READ_CAPS);
|
|
107
141
|
let closed = false;
|
|
108
142
|
let viewerFaultRecorded = false;
|
|
143
|
+
let stopHeartbeat;
|
|
109
144
|
socket.on('connect', () => {
|
|
110
145
|
clearFault(nodeId, { link: 'relay↔broker' });
|
|
111
146
|
});
|
|
@@ -113,6 +148,7 @@ function bridgeConnection(ws, nodeId) {
|
|
|
113
148
|
if (closed)
|
|
114
149
|
return;
|
|
115
150
|
closed = true;
|
|
151
|
+
stopHeartbeat?.();
|
|
116
152
|
if (!socket.destroyed) {
|
|
117
153
|
try {
|
|
118
154
|
socket.destroy();
|
|
@@ -128,6 +164,7 @@ function bridgeConnection(ws, nodeId) {
|
|
|
128
164
|
/* ignore */
|
|
129
165
|
}
|
|
130
166
|
};
|
|
167
|
+
stopHeartbeat = startRemoteAttachHeartbeat(ws);
|
|
131
168
|
// socket → WS: bounded decode, one WS text message per complete frame.
|
|
132
169
|
socket.on('data', (chunk) => {
|
|
133
170
|
let frames;
|
|
@@ -183,7 +220,7 @@ function bridgeConnection(ws, nodeId) {
|
|
|
183
220
|
link: 'viewer↔crtrd',
|
|
184
221
|
op: 'remote attach websocket',
|
|
185
222
|
kind: classification.kind,
|
|
186
|
-
retry: { disposition: '
|
|
223
|
+
retry: { disposition: 'auto' },
|
|
187
224
|
message: reasonText || `remote attach closed (${code})`,
|
|
188
225
|
});
|
|
189
226
|
}
|
|
@@ -197,7 +234,7 @@ function bridgeConnection(ws, nodeId) {
|
|
|
197
234
|
link: 'viewer↔crtrd',
|
|
198
235
|
op: 'remote attach websocket',
|
|
199
236
|
kind: classification.kind,
|
|
200
|
-
retry: { disposition: '
|
|
237
|
+
retry: { disposition: 'auto' },
|
|
201
238
|
message: 'remote attach websocket error',
|
|
202
239
|
});
|
|
203
240
|
}
|
|
@@ -19,15 +19,14 @@ import { nodeArtifacts } from '../../../core/canvas/history.js';
|
|
|
19
19
|
import { readNodeMessagesPage, readNodeSession, readNodeSnapshot, transcriptMarkdown } from '../../../core/runtime/node-read.js';
|
|
20
20
|
import { assembleNodeSubject } from '../../../core/substrate/subject.js';
|
|
21
21
|
import { reviveAll } from '../../../core/runtime/revive-all.js';
|
|
22
|
-
import {
|
|
22
|
+
import { promoteAsync, requestYieldAsync, reshapeNodeAsync } from '../../../core/runtime/promote.js';
|
|
23
23
|
import { recycleNode } from '../../../core/runtime/recycle.js';
|
|
24
24
|
import { relaunchRoot } from '../../../core/runtime/reset.js';
|
|
25
25
|
import { assertReopenGate } from '../../../core/runtime/reopen.js';
|
|
26
26
|
import { writeYieldMessage, readGoal } from '../../../core/runtime/kickoff.js';
|
|
27
27
|
import { hasRoadmap, seedRoadmap } from '../../../core/runtime/roadmap.js';
|
|
28
28
|
import { renameWindow } from '../../../core/runtime/placement.js';
|
|
29
|
-
import {
|
|
30
|
-
import { buildLaunchSpec, isProviderPinnedModelToken, normalizeModel } from '../../../core/runtime/launch.js';
|
|
29
|
+
import { buildLaunchSpecAsync, isProviderPinnedModelToken, normalizeModel, readMergedLaunchConfigAsync } from '../../../core/runtime/launch.js';
|
|
31
30
|
import { modelRequestFromConfig } from '../../../core/model-routes.js';
|
|
32
31
|
import { BrokerUnreachableError } from '../../../core/runtime/broker-request.js';
|
|
33
32
|
import { isBrokerLive, persistDormantModel, setModelLive } from '../../../core/runtime/model-swap.js';
|
|
@@ -196,13 +195,13 @@ function requestFromLaunch(launch) {
|
|
|
196
195
|
return modelRequestFromConfig(launch.model, intent, launch.modelExact === true);
|
|
197
196
|
}
|
|
198
197
|
async function assertNodeModelChangeRegistered(meta, target, registry) {
|
|
199
|
-
const launch =
|
|
198
|
+
const launch = (await buildLaunchSpecAsync(target.kind, target.mode, {
|
|
200
199
|
lifecycle: target.lifecycle,
|
|
201
200
|
hasManager: meta.parent !== null,
|
|
202
201
|
model: target.model,
|
|
203
202
|
cwd: meta.cwd,
|
|
204
203
|
profileId: meta.profile_id ?? null,
|
|
205
|
-
}).launch;
|
|
204
|
+
})).launch;
|
|
206
205
|
await assertLaunchModelRegistered(requestFromLaunch(launch), { cwd: meta.cwd, profileId: meta.profile_id ?? null }, registry);
|
|
207
206
|
}
|
|
208
207
|
export async function handleCreate(ctx, deps = {}) {
|
|
@@ -234,16 +233,16 @@ export async function handleCreate(ctx, deps = {}) {
|
|
|
234
233
|
const profileId = await resolveProfileId(req.profile, parent ?? null, contextCwd);
|
|
235
234
|
const profile = assertProfileActive(profileId);
|
|
236
235
|
const kind = resolveCreateKind(req.kind, profile);
|
|
237
|
-
assertKindServer(kind);
|
|
238
236
|
const resolvedReq = { ...req, kind };
|
|
239
237
|
const cwd = resolveNodeCwd(resolvedReq, parent, profileId, contextCwd);
|
|
240
|
-
|
|
238
|
+
await assertKindServer(kind, cwd, profileId);
|
|
239
|
+
const launch = (await buildLaunchSpecAsync(kind, (resolvedReq.mode ?? 'base'), {
|
|
241
240
|
lifecycle: req.root === true ? (req.root_lifecycle ?? 'resident') : 'terminal',
|
|
242
241
|
hasManager: req.root !== true,
|
|
243
242
|
...(req.model !== undefined ? { model: req.model } : {}),
|
|
244
243
|
cwd,
|
|
245
244
|
profileId,
|
|
246
|
-
}).launch;
|
|
245
|
+
})).launch;
|
|
247
246
|
await assertLaunchModelRegistered(requestFromLaunch(launch), { cwd, profileId }, deps.modelRegistry);
|
|
248
247
|
// --- Warm path: hand out an already-booted spare instead of paying a full
|
|
249
248
|
// engine boot. A miss is not a failure — it falls through to the cold
|
|
@@ -555,12 +554,12 @@ async function handleRecycle(ctx) {
|
|
|
555
554
|
},
|
|
556
555
|
};
|
|
557
556
|
}
|
|
558
|
-
function handleDemote(ctx) {
|
|
557
|
+
async function handleDemote(ctx) {
|
|
559
558
|
const id = ctx.params['id'];
|
|
560
559
|
requireMeta(id);
|
|
561
560
|
// Use the canonical reshape primitive to ensure modelExact, provider
|
|
562
561
|
// pin/intent, and default-name migration are all preserved.
|
|
563
|
-
|
|
562
|
+
await reshapeNodeAsync(id, { lifecycle: 'terminal' });
|
|
564
563
|
return { status: 200, body: detailOf(id) };
|
|
565
564
|
}
|
|
566
565
|
export async function handlePromote(ctx, deps = {}) {
|
|
@@ -575,7 +574,7 @@ export async function handlePromote(ctx, deps = {}) {
|
|
|
575
574
|
model: req.model,
|
|
576
575
|
}, deps.modelRegistry);
|
|
577
576
|
}
|
|
578
|
-
const result =
|
|
577
|
+
const result = await promoteAsync(id, {
|
|
579
578
|
...(req.kind !== undefined ? { kind: req.kind } : {}),
|
|
580
579
|
...(req.resident === true ? { resident: true } : {}),
|
|
581
580
|
...(req.model !== undefined ? { model: req.model } : {}),
|
|
@@ -602,7 +601,7 @@ export async function handleYield(ctx, deps = {}) {
|
|
|
602
601
|
}
|
|
603
602
|
const note = req.note ?? 'Refresh: continue against your roadmap or goal.';
|
|
604
603
|
writeYieldMessage(id, note);
|
|
605
|
-
const result =
|
|
604
|
+
const result = await requestYieldAsync(id, {
|
|
606
605
|
...(req.promote === true ? { promote: true } : {}),
|
|
607
606
|
...(req.kind !== undefined ? { kind: req.kind } : {}),
|
|
608
607
|
...(req.model !== undefined ? { model: req.model } : {}),
|
|
@@ -645,7 +644,7 @@ export async function handleConfig(ctx, deps = {}) {
|
|
|
645
644
|
}
|
|
646
645
|
if (patch.model !== undefined) {
|
|
647
646
|
const pinned = isProviderPinnedModelToken(patch.model);
|
|
648
|
-
const ladders =
|
|
647
|
+
const ladders = (await readMergedLaunchConfigAsync(meta.cwd, meta.profile_id ?? null)).modelLadders;
|
|
649
648
|
const spec = normalizeModel(patch.model, ladders);
|
|
650
649
|
await assertLaunchModelRegistered({ kind: 'concrete', spec }, { cwd: meta.cwd, profileId: meta.profile_id ?? null }, deps.modelRegistry);
|
|
651
650
|
const persistDormant = () => {
|
|
@@ -681,7 +680,7 @@ export async function handleConfig(ctx, deps = {}) {
|
|
|
681
680
|
// --- kind / mode / lifecycle: reshape via canonical primitive (CLI parity). ---
|
|
682
681
|
if (patch.kind !== undefined || patch.mode !== undefined || patch.lifecycle !== undefined) {
|
|
683
682
|
if (patch.kind !== undefined)
|
|
684
|
-
assertKindServer(patch.kind);
|
|
683
|
+
await assertKindServer(patch.kind, meta.cwd, meta.profile_id ?? null);
|
|
685
684
|
const newKind = patch.kind ?? meta.kind;
|
|
686
685
|
const newMode = (patch.mode ?? meta.mode);
|
|
687
686
|
const newLifecycle = patch.lifecycle ?? meta.lifecycle;
|
|
@@ -697,7 +696,7 @@ export async function handleConfig(ctx, deps = {}) {
|
|
|
697
696
|
}
|
|
698
697
|
// Use canonical reshape primitive to preserve modelExact, provider pin/intent,
|
|
699
698
|
// and default-name migration.
|
|
700
|
-
meta =
|
|
699
|
+
meta = await reshapeNodeAsync(id, {
|
|
701
700
|
...(patch.kind !== undefined ? { kind: newKind } : {}),
|
|
702
701
|
...(patch.mode !== undefined ? { mode: newMode } : {}),
|
|
703
702
|
...(patch.lifecycle !== undefined ? { lifecycle: newLifecycle } : {}),
|
|
@@ -715,8 +714,8 @@ export async function handleConfig(ctx, deps = {}) {
|
|
|
715
714
|
}
|
|
716
715
|
/** Validate a `--kind` against the installed personas server-side (crtrd reads
|
|
717
716
|
* the merged launch config). Mirrors the CLI's assertKind. */
|
|
718
|
-
function assertKindServer(kind) {
|
|
719
|
-
const kinds = Object.keys(
|
|
717
|
+
async function assertKindServer(kind, cwd, profileId) {
|
|
718
|
+
const kinds = Object.keys((await readMergedLaunchConfigAsync(cwd, profileId)).kinds);
|
|
720
719
|
if (!kinds.includes(kind)) {
|
|
721
720
|
throw usage(`unknown kind: ${kind}`, { received: kind, next: `Valid kinds: ${kinds.sort().join(', ')}.` });
|
|
722
721
|
}
|
package/dist/types.js
CHANGED
|
@@ -77,7 +77,11 @@ export function defaultScopeConfig() {
|
|
|
77
77
|
auto_update: { crtr: 'notify', content: 'notify', interval_hours: 24 },
|
|
78
78
|
max_panes_per_window: DEFAULT_MAX_PANES_PER_WINDOW,
|
|
79
79
|
auto_open_child_viewers: true,
|
|
80
|
-
|
|
80
|
+
// Every live broker is a whole pi engine. 256 of them is not a bound any
|
|
81
|
+
// real host survives — an 8 GB guest dies around a few dozen — so these are
|
|
82
|
+
// sized as a backstop that actually engages before the machine does.
|
|
83
|
+
// Override per host with CRTR_MAX_LIVE_BROKERS / CRTR_WARN_LIVE_BROKERS.
|
|
84
|
+
brokerThresholds: { warning: 16, automaticReviveCap: 32 },
|
|
81
85
|
completion_bell: true,
|
|
82
86
|
working_gerunds: [...DEFAULT_WORKING_GERUNDS],
|
|
83
87
|
whip_mode: false,
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.220",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.220",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|