@north-light/crouter 0.3.229 → 0.3.230

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.
@@ -250,7 +250,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
250
250
  pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
251
251
  await wait(TICK_MS + 100);
252
252
  appendInbox(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'handoff at yield' });
253
- await wait(SETTLE_MS);
253
+ await waitForFirstInjection(pi);
254
254
  assert.equal(pi.injected.length, 1, 'the active conversation received the handoff before yield');
255
255
  // Yield discards that conversation before it can act on the queued handoff.
256
256
  // Its settle must leave the entry durable for the fresh revive's kickoff.
@@ -267,7 +267,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
267
267
  // No agent_start fired → watcher treats the node as idle.
268
268
  await wait(TICK_MS + 100);
269
269
  appendInbox('node-idle', { from: 'child-3', tier: 'normal', kind: 'final', label: 'done while idle' });
270
- await wait(SETTLE_MS);
270
+ await waitForFirstInjection(pi);
271
271
  assert.equal(pi.injected.length, 1);
272
272
  assert.equal(pi.injected[0].deliverAs, undefined, 'idle → sendUserMessage triggers a turn, no deliverAs');
273
273
  });
@@ -295,7 +295,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
295
295
  assert.equal(pi.injected.length, 0, 'a paused profile delivers nothing to its live broker');
296
296
  assert.equal(readCursor('node-paused-profile'), undefined, 'the held entry stays unconsumed');
297
297
  resumeProfile(profile.profileId);
298
- await wait(SETTLE_MS);
298
+ await waitForFirstInjection(pi);
299
299
  assert.equal(pi.injected.length, 1, 'resume delivers the entry that waited');
300
300
  assert.match(pi.injected[0].content, /sent while paused/);
301
301
  }
@@ -348,7 +348,7 @@ describe('canvas inbox watcher — action-gated startup (session_start arm)', ()
348
348
  // now must pick up the already-buffered pre-bind entry.
349
349
  actionsReady = true;
350
350
  pi.fire('session_start', { type: 'session_start' }, undefined);
351
- await wait(SETTLE_MS);
351
+ await waitForFirstInjection(pi);
352
352
  assert.equal(sendAttempts, 1, 'exactly one action attempt once session_start arms the watcher');
353
353
  assert.equal(pi.injected.length, 1, 'exactly one handoff once actions are bound');
354
354
  assert.equal(readCursor('node-action-gate'), undefined, 'still uncommitted until agent_settled');
@@ -404,7 +404,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
404
404
  pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
405
405
  await wait(TICK_MS + 100);
406
406
  appendInbox('node-cursor-crash', { from: 'child-1', tier: 'normal', kind: 'update', label: 'still working' });
407
- await wait(SETTLE_MS);
407
+ await waitForFirstInjection(pi1);
408
408
  assert.equal(pi1.injected.length, 1, 'delivered mid-turn as a followUp');
409
409
  assert.equal(pi1.injected[0].deliverAs, 'followUp');
410
410
  // The durability boundary: no settle has fired, so the on-disk cursor must
@@ -420,7 +420,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
420
420
  // modeling crash-then-revive rather than a clean stop.
421
421
  const pi2 = makeFakePi();
422
422
  disposers.push(registerWatcher(pi2));
423
- await wait(SETTLE_MS);
423
+ await waitForFirstInjection(pi2);
424
424
  assert.equal(pi2.injected.length, 1, 'a fresh watcher re-reads and re-delivers the same uncommitted entry');
425
425
  assert.match(pi2.injected[0].content, /still working/);
426
426
  });
@@ -431,7 +431,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
431
431
  pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
432
432
  await wait(TICK_MS + 100);
433
433
  const entry = appendInbox('node-cursor-committed', { from: 'child-2', tier: 'normal', kind: 'update', label: 'still working' });
434
- await wait(SETTLE_MS);
434
+ await waitForFirstInjection(pi1);
435
435
  assert.equal(pi1.injected.length, 1);
436
436
  assert.equal(readCursor('node-cursor-committed'), undefined, 'not yet committed before the settle');
437
437
  // The turn settles with pi's queue drained (the followUp was consumed) —
@@ -450,7 +450,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
450
450
  // No agent_start fired → watcher treats the node as idle.
451
451
  await wait(TICK_MS + 100);
452
452
  const entry = appendInbox('node-cursor-idle', { from: 'child-3', tier: 'normal', kind: 'update', label: 'idle update' });
453
- await wait(SETTLE_MS);
453
+ await waitForFirstInjection(pi);
454
454
  assert.equal(pi.injected.length, 1);
455
455
  assert.equal(pi.injected[0].deliverAs, undefined, 'idle delivery has no deliverAs');
456
456
  // The idle send triggers a turn, but its void return is a handoff, not proof
@@ -477,7 +477,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
477
477
  assert.equal(readCursor('node-cursor-failsend'), undefined, 'durable cursor not advanced past a re-buffered, never-delivered entry');
478
478
  const pi2 = makeFakePi();
479
479
  disposers.push(registerWatcher(pi2));
480
- await wait(SETTLE_MS);
480
+ await waitForFirstInjection(pi2);
481
481
  assert.equal(pi2.injected.length, 1, 'a fresh watcher re-reads and re-delivers the never-committed entry');
482
482
  assert.match(pi2.injected[0].content, /never delivered/);
483
483
  });
@@ -501,7 +501,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
501
501
  assert.equal(readCursor('node-cursor-critical'), undefined, 'abort-generated settle does not commit the pending critical entry before its retry');
502
502
  const pi2 = makeFakePi();
503
503
  disposers.push(registerWatcher(pi2));
504
- await wait(SETTLE_MS);
504
+ await waitForFirstInjection(pi2);
505
505
  assert.equal(pi2.injected.length, 1, 'a fresh watcher re-delivers the critical entry');
506
506
  assert.match(pi2.injected[0].content, /critical work/);
507
507
  });
@@ -517,7 +517,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
517
517
  pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
518
518
  await wait(TICK_MS + 60);
519
519
  const a = appendInbox('node-cursor-batchB', { from: 'child-a', tier: 'normal', kind: 'update', label: 'batch A' });
520
- await wait(280); // > 200ms debounce → A flushes and is delivered mid-turn (handoff)
520
+ await waitForFirstInjection(pi1);
521
521
  assert.equal(pi1.injected.length, 1, 'batch A delivered mid-turn');
522
522
  assert.equal(pi1.injected[0].deliverAs, 'followUp');
523
523
  const b = appendInbox('node-cursor-batchB', { from: 'child-b', tier: 'normal', kind: 'update', label: 'batch B' });
@@ -529,7 +529,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
529
529
  assert.notEqual(readCursor('node-cursor-batchB'), b.entry_id, 'commit does not cross the still-buffered batch B');
530
530
  const pi2 = makeFakePi();
531
531
  disposers.push(registerWatcher(pi2));
532
- await wait(SETTLE_MS + 40);
532
+ await waitForFirstInjection(pi2);
533
533
  assert.equal(pi2.injected.length, 1, 'a fresh watcher re-reads from A and re-delivers B');
534
534
  assert.match(pi2.injected[0].content, /batch B/);
535
535
  assert.doesNotMatch(pi2.injected[0].content, /batch A/, 'A is already committed, only B is re-delivered');
@@ -548,7 +548,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
548
548
  pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
549
549
  await wait(TICK_MS + 100);
550
550
  const entry = appendInbox('node-cursor-failhold', { from: 'child-1', tier: 'normal', kind: 'update', label: 'queued across a failure' });
551
- await wait(SETTLE_MS);
551
+ await waitForFirstInjection(pi1);
552
552
  assert.equal(pi1.injected.length, 1, 'delivered mid-turn as a followUp');
553
553
  assert.equal(pi1.injected[0].deliverAs, 'followUp');
554
554
  // A failure/aborted agent_end is never a commit point regardless of queue
@@ -583,7 +583,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
583
583
  pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
584
584
  await wait(TICK_MS + 100);
585
585
  appendInbox(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'handoff at shutdown' });
586
- await wait(SETTLE_MS);
586
+ await waitForFirstInjection(pi);
587
587
  assert.equal(pi.injected.length, 1, 'the active conversation received the handoff before refresh');
588
588
  setIntent(meta.node_id, 'refresh');
589
589
  await shutdown(pi, { pending: false });
@@ -596,7 +596,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
596
596
  pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
597
597
  await wait(TICK_MS + 100);
598
598
  const entry = appendInbox('node-cursor-shutdownhold', { from: 'child-1', tier: 'normal', kind: 'update', label: 'still queued at shutdown' });
599
- await wait(SETTLE_MS);
599
+ await waitForFirstInjection(pi1);
600
600
  assert.equal(pi1.injected.length, 1, 'delivered mid-turn as a followUp');
601
601
  // session_shutdown fires with no prior agent_settled, and the context
602
602
  // reports an unconsumed queued send — the hold declines the commit.
@@ -606,7 +606,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
606
606
  // A fresh watcher re-reads and re-delivers, then a drained settle commits.
607
607
  const pi2 = makeFakePi();
608
608
  disposers.push(registerWatcher(pi2));
609
- await wait(SETTLE_MS);
609
+ await waitForFirstInjection(pi2);
610
610
  assert.equal(pi2.injected.length, 1, 'a fresh watcher re-reads and re-delivers the never-committed entry');
611
611
  assert.match(pi2.injected[0].content, /still queued at shutdown/);
612
612
  await settle(pi2);
@@ -8,7 +8,6 @@ import { tmpdir } from 'node:os';
8
8
  import { join } from 'node:path';
9
9
  import { createServer } from 'node:net';
10
10
  import { spawn as spawnProcess } from 'node:child_process';
11
- import { findDaemonPids, stopDaemonProcess } from '../../../daemon/manage.js';
12
11
  import { createNode, getNode, getRow, subscribersOf } from '../../canvas/canvas.js';
13
12
  import { closeDb } from '../../canvas/db.js';
14
13
  import { nodeDir } from '../../canvas/paths.js';
@@ -21,6 +20,7 @@ import { piSessionsRoot } from '../../runtime/pi-vendored.js';
21
20
  import { emptyContextExposureState, exposureTarget, loadContextExposureState, registerExposure, saveContextExposureState, } from '../../substrate/injected-store.js';
22
21
  import { ROOT_PROFILE_ID } from '../../profiles/manifest.js';
23
22
  let home;
23
+ let previousPidfile;
24
24
  function spawner(id) {
25
25
  return {
26
26
  node_id: id,
@@ -38,15 +38,25 @@ before(() => {
38
38
  // canvas home short because root readiness now opens a real view.sock.
39
39
  home = mkdtempSync('/tmp/crtr-spawn-root-');
40
40
  process.env['CRTR_HOME'] = home;
41
+ previousPidfile = process.env['CRTR_PIDFILE'];
42
+ process.env['CRTR_PIDFILE'] = join(home, 'crtrd.pid');
41
43
  });
42
44
  beforeEach(() => {
43
45
  closeDb();
44
46
  rmSync(home, { recursive: true, force: true });
47
+ mkdirSync(home, { recursive: true });
48
+ // These tests exercise direct spawn behavior, not daemon autostart. Keep
49
+ // ensureDaemon() from launching a background daemon whose startup verifier
50
+ // would outlive the test's next CRTR_HOME reset.
51
+ writeFileSync(join(home, 'crtrd.pid'), `${process.pid}\n`);
45
52
  });
46
- after(async () => {
53
+ after(() => {
47
54
  closeDb();
48
- await Promise.all(findDaemonPids().map((pid) => stopDaemonProcess(pid)));
49
55
  rmSync(home, { recursive: true, force: true });
56
+ if (previousPidfile === undefined)
57
+ delete process.env['CRTR_PIDFILE'];
58
+ else
59
+ process.env['CRTR_PIDFILE'] = previousPidfile;
50
60
  });
51
61
  test('managed child: parent on the spine + active subscription + provenance', () => {
52
62
  createNode(spawner('A'));
@@ -17,10 +17,11 @@ writeFileSync(pageSource, 'export default () => (\n <Page title="A page" subtit
17
17
  const page = (id, emittedAt) => submitPage({ dir: ticketDir(id), sourceFile: pageSource, source: { emittedAt, nodeId: 'node-1' }, delivery: { placement: 'inline', inbox: true, reply: true } });
18
18
  const answer = { go: { text: 'yes', edited: true } };
19
19
  async function waitFor(paths) {
20
- for (let attempts = 0; !paths.every(existsSync); attempts++) {
21
- if (attempts > 1_000)
20
+ const deadline = Date.now() + 10_000;
21
+ while (!paths.every(existsSync)) {
22
+ if (Date.now() >= deadline)
22
23
  throw new Error(`workers did not reach barrier: ${paths.join(', ')}`);
23
- await new Promise((resolvePromise) => setTimeout(resolvePromise, 1));
24
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
24
25
  }
25
26
  }
26
27
  page('z-last', '2025-01-01T00:00:00.000Z');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.229",
3
+ "version": "0.3.230",
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.229",
3
+ "version": "0.3.230",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.229",
9
+ "version": "0.3.230",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {