@north-light/crouter 0.3.199 → 0.3.200

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 { fileURLToPath } from 'node:url';
28
28
  import { createRequire } from 'node:module';
29
29
  import { createNode, getNode, recordPid, subscribersOf, subscriptionsOf, } from '../../canvas/canvas.js';
30
30
  import { closeDb } from '../../canvas/db.js';
31
- import { reviveNode } from '../../runtime/revive.js';
31
+ import { spawnChild } from '../../runtime/spawn.js';
32
32
  import { superviseTick } from '../../../daemon/crtrd.js';
33
33
  import { DaemonFleet } from '../../../daemon/fleet.js';
34
34
  import { apiSocketPath } from '../../canvas/paths.js';
@@ -150,11 +150,11 @@ export async function createHarness(opts = {}) {
150
150
  const tmpHome = mkdtempSync(join(tmpdir(), 'crtr-harness-HOME-'));
151
151
  const session = `${opts.sessionPrefix ?? 'crtr-harness'}-${process.pid}-${Date.now().toString(36)}`;
152
152
  // The harness reads/writes the isolated canvas in-process. CRTR_PI_BINARY in
153
- // OUR env makes in-process revives (superviseTick → reviveNode → openNodeWindow)
153
+ // OUR env makes in-process launches (spawnChild and superviseTick → reviveNode)
154
154
  // bake the fake-pi into the command string.
155
155
  process.env['CRTR_HOME'] = home;
156
156
  // HOME must be isolated in THIS process too, not just in the subprocess envs
157
- // below: an in-process launch (spawnHeadlessChild's reviveNode, superviseTick
157
+ // below: an in-process launch (spawnHeadlessChild's spawnChild, superviseTick
158
158
  // pass-2 wakes) builds its broker env from process.env, so a real HOME here
159
159
  // makes every broker read the developer's OWN ~/.pi/agent — auth.json and the
160
160
  // managed subscription pools — instead of agentHome, and provider tests then
@@ -202,7 +202,7 @@ export async function createHarness(opts = {}) {
202
202
  resetEventSourceForTests();
203
203
  bindDaemonEventSource();
204
204
  // The harness also stands in for the daemon's FLEET: every in-harness broker
205
- // launch (reviveNode from a test, superviseTick pass-2 wakes) registers its
205
+ // launch (spawnChild, reviveNode, superviseTick pass-2 wakes) registers its
206
206
  // ChildProcess handle here, and real child exits enqueue exit-policy jobs on
207
207
  // this serial work lane — the same single-lane discipline crtrd runs, but
208
208
  // drainable to a deterministic quiescent point. Reset first for the same
@@ -367,17 +367,9 @@ export async function createHarness(opts = {}) {
367
367
  }
368
368
  function nodeDirs() {
369
369
  try {
370
- // Node ids are minted by newNodeId() (runtime/nodes.ts) as
371
- // `<install>-<time36>-<rand4hex>` (a72554e3 stamped the canvas-install id
372
- // onto the historical 2-segment `<time36>-<rand4hex>` shape, so the id is
373
- // opaque and its segment count has already changed once — mirror
374
- // profiles/manifest.ts's ID_SHAPE rather than hard-coding a segment
375
- // count). Scope the scan to "one or more lowercase-alnum segments ending
376
- // in an 8-hex random suffix" so a stray non-node sibling under nodes/ is
377
- // never miscounted as a new node dir — e.g. a `reports/` dir created for
378
- // an empty node id (the rare CI flake on compiled-seam run 27459304455 that
379
- // broke the spawn counters with `expected exactly 1 new node dir, got
380
- // [<id>, reports]`), or literal `reports` (no hyphen, no hex suffix).
370
+ // Node ids end in an 8-hex random suffix and may have any number of
371
+ // lowercase-alphanumeric prefix segments. Only return those node dirs so
372
+ // fixture cleanup never treats a non-node sibling as a canvas row.
381
373
  return readdirSync(join(home, 'nodes')).filter((d) => /^[a-z0-9]+(?:-[a-z0-9]+)*-[0-9a-f]{8}$/.test(d));
382
374
  }
383
375
  catch {
@@ -507,46 +499,16 @@ export async function createHarness(opts = {}) {
507
499
  return id;
508
500
  },
509
501
  async spawnHeadlessChild(parentId, task, o = {}) {
510
- const before = new Set(nodeDirs());
511
- // The synchronous CLI must reach the separate API fixture process, which
512
- // creates the row and necessarily starts a fixture-owned broker before
513
- // returning. Production has one daemon/fleet; the harness split exists
514
- // only because spawnSync blocks this process's event loop. Retire that
515
- // fixture launch immediately, then perform the authoritative boot through
516
- // THIS process's reviveNode so the harness fleet owns the ChildProcess
517
- // handle and its real exit event (§H harness mode a).
518
- const args = ['node', 'new', task, '--parent', parentId, '--cwd', CROUTER];
519
- if (o.kind)
520
- args.push('--kind', o.kind);
521
- if (o.mode)
522
- args.push('--mode', o.mode);
523
- if (o.model)
524
- args.push('--model', o.model);
525
- const res = cli(parentId, args);
526
- if (res.code !== 0) {
527
- throw new Error(`spawnHeadlessChild(${parentId}) failed (code ${res.code})\n--stdout--\n${res.stdout}\n--stderr--\n${res.stderr}`);
528
- }
529
- const added = nodeDirs().filter((d) => !before.has(d));
530
- if (added.length !== 1) {
531
- throw new Error(`spawnHeadlessChild: expected exactly 1 new node dir, got [${added.join(', ')}]`);
532
- }
533
- const childId = added[0];
534
- closeDb();
535
- const fixtureRow = getNode(childId);
536
- const fixturePid = fixtureRow?.pi_pid ?? null;
537
- const fixtureIdentity = fixtureRow?.pi_pid_identity ?? null;
538
- const priorBoots = harness.bootCount(childId);
539
- if (fixturePid !== null && fixtureIdentity !== null && recordedPidLiveness(fixturePid, fixtureIdentity) === 'alive') {
540
- process.kill(fixturePid, 'SIGTERM');
541
- await waitFor(() => recordedPidLiveness(fixturePid, fixtureIdentity) !== 'alive', {
542
- timeoutMs: 10_000,
543
- intervalMs: 25,
544
- label: `API-fixture broker exit for ${childId}`,
545
- });
546
- }
547
- reviveNode(childId, { resume: false });
548
- await harness.awaitBoot(childId, { minCount: priorBoots + 1 });
549
- return childId;
502
+ const result = await spawnChild({
503
+ kind: o.kind ?? 'general',
504
+ cwd: CROUTER,
505
+ prompt: task,
506
+ parent: parentId,
507
+ ...(o.mode !== undefined ? { mode: o.mode } : {}),
508
+ ...(o.model !== undefined ? { model: o.model } : {}),
509
+ });
510
+ await harness.awaitBoot(result.node.node_id);
511
+ return result.node.node_id;
550
512
  },
551
513
  cli,
552
514
  async stop(nodeId, reason = 'stop', errorMessage) {
@@ -10,11 +10,10 @@
10
10
  // lifecycle close` then tears the replacement down (shutdown frame → exit,
11
11
  // socket unlinked, status canceled, daemon leaves it).
12
12
  //
13
- // The harness API server is a separate process (so synchronous CLI calls can be
14
- // served) whose fixture fleet is inert; spawnHeadlessChild retires its fixture
15
- // broker and reboots the node through THIS process's reviveNode, so every
16
- // broker below is harness-fleet-owned and every SIGKILL's REAL ChildProcess
17
- // exit drives policy automatically — no synthetic delivery, no tick, no grace.
13
+ // spawnHeadlessChild calls the production birth path in the harness process, so
14
+ // every broker below is harness-fleet-owned from its first and only boot and
15
+ // every SIGKILL's REAL ChildProcess exit drives policy automatically — no
16
+ // synthetic delivery, no tick, no grace.
18
17
  // Each crash has its own real broker setup, so either top-level test remains a
19
18
  // complete journey under name filtering and pays only its own first respawn
20
19
  // backoff.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.199",
3
+ "version": "0.3.200",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -83,7 +83,8 @@
83
83
  "prepublishOnly": "npm run build",
84
84
  "test": "node --conditions=crtr-src --import tsx/esm --test --test-concurrency=4 --test-timeout=10000 'src/**/__tests__/*.test.ts'",
85
85
  "test:serial": "node --conditions=crtr-src --import tsx/esm --test --test-concurrency=1 --test-timeout=60000 'src/**/__tests__/serial/*.test.ts'",
86
- "test:seam": "npm run build && node --test --test-concurrency=1 --test-timeout=180000 'dist/**/__tests__/seam/*.test.js'"
86
+ "test:seam": "npm run build && npm run test:seam:built",
87
+ "test:seam:built": "node --test --test-concurrency=2 --test-timeout=180000 'dist/**/__tests__/seam/*.test.js'"
87
88
  },
88
89
  "overrides": {
89
90
  "@earendil-works/pi-ai": "0.84.1",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.199",
3
+ "version": "0.3.200",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.199",
9
+ "version": "0.3.200",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {