@north-light/crouter 0.3.270 → 0.3.271

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.
@@ -28,7 +28,7 @@ import { ApiError } from '../../api/errors.js';
28
28
  // debounce window before asserting, exactly as against the real 800/1000 cadence.
29
29
  const TICK_MS = 20;
30
30
  const DEBOUNCE_MS = 25;
31
- const SETTLE_MS = TICK_MS * 2 + DEBOUNCE_MS + 30;
31
+ const SETTLE_MS = TICK_MS * 2 + DEBOUNCE_MS + 200;
32
32
  let origHome;
33
33
  let origNode;
34
34
  const homes = [];
@@ -302,7 +302,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
302
302
  },
303
303
  };
304
304
  const drive = createEngineDriver({
305
- registry: { sendFrame: () => undefined },
305
+ registry: { sendFrame: () => undefined, broadcast: () => undefined },
306
306
  replies: {},
307
307
  session: () => session,
308
308
  dispatches: { track: (promise) => promise },
@@ -282,6 +282,7 @@ export async function createHarness(opts = {}) {
282
282
  ...cleanBaseEnv(),
283
283
  CRTR_HOME: home,
284
284
  HOME: tmpHome,
285
+ CRTR_RUNTIME_HOME: realRuntimeHome,
285
286
  CRTR_PI_BINARY: FAKE_PI_BINARY,
286
287
  CRTR_BROKER_ENGINE: FAKE_ENGINE,
287
288
  },
@@ -109,6 +109,8 @@ test('a managed-cooling terminal authors a rate-limit auto fault whose deadline
109
109
  seedPool('openai-codex', false);
110
110
  const child = await h.spawnHeadlessChild(parent, 'cooling terminal', { model: OPENAI_STRONG.spec });
111
111
  await h.waitFor(() => agentStartCount(child) >= 1, { label: 'initial cooling fixture turn started' });
112
+ const kickoffObserver = await attachUntil(child, 'observer', 'cooling-terminal-kickoff-settled', (a) => a.welcome.snapshot.state.isStreaming === false, 'cooling terminal kickoff settled');
113
+ kickoffObserver.close();
112
114
  const baselineStarts = agentStartCount(child);
113
115
  // A terminal managed-cooling error at agent_end: its explanatory prose avoids
114
116
  // rate-limit words entirely; the typed diagnostic alone must classify the
@@ -47,9 +47,8 @@ export interface FleetRegistry {
47
47
  get(nodeId: string): FleetEntry | undefined;
48
48
  /** The capacity count: registered brokers only, excluding reservations. */
49
49
  size(): number;
50
- /** Every slot the cap accounts for — registered brokers plus in-flight
51
- * reservations (a launch that has claimed a slot but not yet registered, and
52
- * an exited broker whose exit-policy job has not yet run). */
50
+ /** Every slot the cap accounts for — registered brokers plus launch
51
+ * reservations that have not registered yet. */
53
52
  occupancy(): number;
54
53
  /** Claim a slot for `nodeId`. True means the caller may launch; false means
55
54
  * the cap is reached and the caller must freeze or refuse. Idempotent — a
@@ -62,9 +61,9 @@ export interface FleetRegistry {
62
61
  /** Drop an entry without policy (terminal exits + close paths). */
63
62
  forget(nodeId: string): void;
64
63
  /** Wired by `headlessBrokerHost.launch` to the handle's `exited` promise.
65
- * Removes the entry (freeing the capacity slot), then enqueues an
66
- * exit-policy job on the daemon's serial work lane — policy never runs
67
- * inline on the Node event callback (design D-3). */
64
+ * Removes the entry (freeing the capacity slot), then enqueues its
65
+ * observation on the daemon's serial work lane — it never runs inline on
66
+ * the Node event callback (design D-3). */
68
67
  onChildExit(nodeId: string, status: FleetExitStatus): void;
69
68
  /** Test seam — delivers a synthetic exit through the SAME path as
70
69
  * onChildExit, so fabricated-row tests drive policy without a process. */
@@ -12,7 +12,7 @@
12
12
  // learns when the work finishes — this seeds the subscription
13
13
  // graph to mirror the spawn structure. A `spawned_by` audit edge
14
14
  // is also recorded.
15
- import { envHomeOverride } from '../../shared/env.js';
15
+ import { envHomeOverride, envRuntimeHomeOverride } from '../../shared/env.js';
16
16
  import { randomBytes } from 'node:crypto';
17
17
  import { mkdirSync } from 'node:fs';
18
18
  import { createNode, getNode, getRow, subscribe, recordSpawn, subscribersOf, contextDir, } from '../canvas/index.js';
@@ -206,6 +206,11 @@ export function nodeEnv(meta) {
206
206
  const home = envHomeOverride();
207
207
  if (home !== undefined && home !== '')
208
208
  env['CRTR_HOME'] = home;
209
+ // Propagate an explicit runtime home so broker-side `crtr` invocations use
210
+ // the same installed generation despite an isolated HOME.
211
+ const runtimeHome = envRuntimeHomeOverride();
212
+ if (runtimeHome !== undefined && runtimeHome !== '')
213
+ env['CRTR_RUNTIME_HOME'] = runtimeHome;
209
214
  // Propagate the subtree-root node id. The broker uses it to group a subtree;
210
215
  // each launch site also emits the authoritative rootOfSpine(nodeId) over this
211
216
  // passthrough.
@@ -122,10 +122,7 @@ export class DaemonFleet {
122
122
  #enqueue;
123
123
  #now;
124
124
  #map = new Map();
125
- /** Slots claimed but not yet held by a registered broker: a launch in flight,
126
- * or a broker that exited and whose exit-policy job has not yet decided
127
- * whether to relaunch it. Counting both against the cap is what keeps a
128
- * 1:1 respawn from losing its own slot to a concurrent launcher. */
125
+ /** Slots claimed by launches that have not registered a broker yet. */
129
126
  #reservations = new Set();
130
127
  #lastCapacityLogAt = Number.NEGATIVE_INFINITY;
131
128
  constructor(opts) {
@@ -191,13 +188,15 @@ export class DaemonFleet {
191
188
  this.#reservations.delete(nodeId);
192
189
  }
193
190
  onChildExit(nodeId, status) {
194
- // Move the slot from the registry to a reservation in ONE synchronous
195
- // step: the exit-policy job about to be enqueued may relaunch this node,
196
- // and holding the slot through the exit observation prevents a concurrent
197
- // launch from entering before the registry reflects the exit.
191
+ // The real child exit is the liveness boundary. Its queued observation only
192
+ // records diagnostics, so it cannot retain capacity after the process is
193
+ // gone and make an explicit replacement look already live.
194
+ const meta = getNode(nodeId);
195
+ const launchedMs = meta?.launched_at == null ? Number.NaN : Date.parse(meta.launched_at);
196
+ const uptimeMs = Number.isFinite(launchedMs) ? Math.max(0, this.#now() - launchedMs) : null;
197
+ const consecutiveFailures = meta?.respawn_failures ?? 0;
198
198
  this.#map.delete(nodeId);
199
- this.#reservations.add(nodeId);
200
- this.#enqueue(() => this.#runExitPolicy(nodeId, status));
199
+ this.#enqueue(() => this.#emitExitObserved(nodeId, status, uptimeMs, consecutiveFailures));
201
200
  }
202
201
  deliverExit(nodeId, status) {
203
202
  this.onChildExit(nodeId, status);
@@ -223,17 +222,6 @@ export class DaemonFleet {
223
222
  return true;
224
223
  }
225
224
  dispose() { }
226
- async #runExitPolicy(nodeId, status) {
227
- try {
228
- const launchedAt = getNode(nodeId)?.launched_at;
229
- const launchedMs = launchedAt == null ? Number.NaN : Date.parse(launchedAt);
230
- const uptimeMs = Number.isFinite(launchedMs) ? Math.max(0, this.#now() - launchedMs) : null;
231
- this.#emitExitObserved(nodeId, status, uptimeMs, getNode(nodeId)?.respawn_failures ?? 0);
232
- }
233
- finally {
234
- this.releaseReservation(nodeId);
235
- }
236
- }
237
225
  #emitExitObserved(nodeId, status, uptimeMs, consecutiveFailures) {
238
226
  emitEvent({
239
227
  level: 'info',
@@ -15,6 +15,8 @@ export declare function envModelExact(): boolean;
15
15
  * only when a caller needs the override itself — to check whether one is
16
16
  * set, or to propagate it verbatim into a child process's env. */
17
17
  export declare function envHomeOverride(): string | undefined;
18
+ /** The raw installed-runtime directory override, unresolved. */
19
+ export declare function envRuntimeHomeOverride(): string | undefined;
18
20
  /** Dead-provider watchdog timeout (`CRTR_STREAM_WATCHDOG_MS`), 5 minutes by
19
21
  * default. See `runtime/stream-watchdog.ts` for what it guards. */
20
22
  export declare function envStreamWatchdogMs(): number;
@@ -51,6 +51,10 @@ export function envModelExact() {
51
51
  export function envHomeOverride() {
52
52
  return process.env['CRTR_HOME'];
53
53
  }
54
+ /** The raw installed-runtime directory override, unresolved. */
55
+ export function envRuntimeHomeOverride() {
56
+ return process.env['CRTR_RUNTIME_HOME'];
57
+ }
54
58
  // Tuning
55
59
  /** Generic `Number(raw)` parse with a positive-finite guard, shared by every
56
60
  * millisecond tuning var below (each previously reimplemented this locally,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.270",
3
+ "version": "0.3.271",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.270",
3
+ "version": "0.3.271",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.270",
9
+ "version": "0.3.271",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {