@north-light/crouter 0.3.181 → 0.3.182

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.
@@ -18,5 +18,9 @@ export declare function readBrokerCursor(nodeId: string): BrokerInboxEntryId | u
18
18
  export declare function readBrokerInboxSince(nodeId: string, cursor?: string): BrokerInboxEntry[];
19
19
  export declare function readBrokerInboxRange(nodeId: string, after: string | undefined, through: string): BrokerInboxEntry[];
20
20
  export declare function readBrokerCanceledEntryIds(nodeId: string): Set<BrokerInboxEntryId>;
21
+ /** The node id a canonical report ref points at, or undefined when the ref is
22
+ * absent, absolute, or not a report path. Callers use it to tell whether the
23
+ * report-node projection they hold can still resolve the ref. */
24
+ export declare function reportRefNodeId(ref: string | undefined): string | undefined;
21
25
  /** Render the existing inbox digest wording with daemon-projected report existence. */
22
26
  export declare function coalesceBrokerInbox(entries: readonly BrokerInboxEntry[], reportNodes: ReadonlyMap<string, BrokerReportNodeDTO>): string;
@@ -101,16 +101,25 @@ function clipBody(body) {
101
101
  }
102
102
  return { text: text.trimEnd(), clipped };
103
103
  }
104
- function inlineReport(entry, reportNodes) {
105
- if (entry.ref === undefined || entry.ref.startsWith('/'))
106
- return null;
107
- const separator = entry.ref.indexOf(':');
104
+ /** The node id a canonical report ref points at, or undefined when the ref is
105
+ * absent, absolute, or not a report path. Callers use it to tell whether the
106
+ * report-node projection they hold can still resolve the ref. */
107
+ export function reportRefNodeId(ref) {
108
+ if (ref === undefined || ref.startsWith('/'))
109
+ return undefined;
110
+ const separator = ref.indexOf(':');
108
111
  if (separator <= 0)
112
+ return undefined;
113
+ const relpath = ref.slice(separator + 1);
114
+ if (!relpath.startsWith('reports/') || relpath.includes('..') || relpath.startsWith('/'))
115
+ return undefined;
116
+ return ref.slice(0, separator);
117
+ }
118
+ function inlineReport(entry, reportNodes) {
119
+ const nodeId = reportRefNodeId(entry.ref);
120
+ if (nodeId === undefined || !reportNodes.has(nodeId))
109
121
  return null;
110
- const nodeId = entry.ref.slice(0, separator);
111
- const relpath = entry.ref.slice(separator + 1);
112
- if (!reportNodes.has(nodeId) || !relpath.startsWith('reports/') || relpath.includes('..') || relpath.startsWith('/'))
113
- return null;
122
+ const relpath = entry.ref.slice(entry.ref.indexOf(':') + 1);
114
123
  const path = `${nodeDir(nodeId)}/${relpath}`;
115
124
  try {
116
125
  const body = readFileSync(path, 'utf8');
@@ -5,7 +5,7 @@
5
5
  // return is only a handoff. Exact cursor persistence at agent_settled is the
6
6
  // durable boundary; an uncommitted process exit is recovered by rereading from
7
7
  // the unchanged cursor.
8
- import { coalesceBrokerInbox, readBrokerCanceledEntryIds, readBrokerCursor, readBrokerInboxSince, } from '../core/runtime/broker/inbox.js';
8
+ import { coalesceBrokerInbox, readBrokerCanceledEntryIds, readBrokerCursor, readBrokerInboxSince, reportRefNodeId, } from '../core/runtime/broker/inbox.js';
9
9
  import { advanceBrokerInboxCursor, brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
10
10
  import { emitEvent } from '../core/events/emit.js';
11
11
  import { errorClassFromError } from '../core/events/errors.js';
@@ -186,6 +186,8 @@ class CanvasInboxWatcher {
186
186
  fatalError;
187
187
  fatalEscaped = false;
188
188
  reportNodes = new Map();
189
+ reportNodesAsked = new Set();
190
+ reportNodesRefreshing = false;
189
191
  constructor(pi, cadence, generation) {
190
192
  this.pi = pi;
191
193
  this.cadence = cadence;
@@ -288,6 +290,7 @@ class CanvasInboxWatcher {
288
290
  async refreshReportNodes(nodeId) {
289
291
  if (nodeId === undefined)
290
292
  return;
293
+ this.reportNodesRefreshing = true;
291
294
  try {
292
295
  const state = await brokerExtensionState(nodeId);
293
296
  if (!this.canMutate())
@@ -297,6 +300,33 @@ class CanvasInboxWatcher {
297
300
  catch {
298
301
  // Report bodies remain canonical pointers until the daemon projection is available.
299
302
  }
303
+ finally {
304
+ this.reportNodesRefreshing = false;
305
+ }
306
+ }
307
+ /** True when delivery must wait a tick for a report-node projection refresh.
308
+ * The projection captured at session_start omits every node created since —
309
+ * each `crtr human ask` bridge and each child this node spawns — so their
310
+ * reports would render as bare refs instead of inlined bodies. One refresh
311
+ * per unknown node id keeps a genuinely missing node from stalling delivery. */
312
+ refreshForUnknownReportRefs() {
313
+ if (this.reportNodesRefreshing)
314
+ return true;
315
+ const unknown = [];
316
+ for (const entry of this.buffer) {
317
+ const refNodeId = reportRefNodeId(entry.ref);
318
+ if (refNodeId === undefined)
319
+ continue;
320
+ if (this.reportNodes.has(refNodeId) || this.reportNodesAsked.has(refNodeId))
321
+ continue;
322
+ unknown.push(refNodeId);
323
+ }
324
+ if (unknown.length === 0)
325
+ return false;
326
+ for (const refNodeId of unknown)
327
+ this.reportNodesAsked.add(refNodeId);
328
+ void this.refreshReportNodes(currentNodeId());
329
+ return this.reportNodesRefreshing;
300
330
  }
301
331
  arm() {
302
332
  if (this.armed)
@@ -343,6 +373,7 @@ class CanvasInboxWatcher {
343
373
  this.cursor = undefined;
344
374
  this.durableCursor = undefined;
345
375
  this.reportNodes.clear();
376
+ this.reportNodesAsked.clear();
346
377
  this.pendingCommit = undefined;
347
378
  this.nodeId = undefined;
348
379
  this.lastCtx = undefined;
@@ -509,6 +540,8 @@ class CanvasInboxWatcher {
509
540
  this.lastArrival = Date.now();
510
541
  }
511
542
  if (this.buffer.length > 0 && Date.now() - this.lastArrival >= this.cadence.debounceMs) {
543
+ if (this.refreshForUnknownReportRefs())
544
+ return;
512
545
  this.flushOrdinary(nodeId);
513
546
  }
514
547
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.181",
3
+ "version": "0.3.182",
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.181",
3
+ "version": "0.3.182",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.181",
9
+ "version": "0.3.182",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {