@north-light/crouter 0.3.225 → 0.3.226

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.
@@ -1,6 +1,6 @@
1
+ import { pageAwaitsResponse } from '../../core/human/page-schema.js';
1
2
  import { writePageProgress } from '../../core/human/tickets.js';
2
3
  import { mountPanel } from './tui/panel.js';
3
- import { awaitsResponse } from './tui/slots.js';
4
4
  /** Embeds the page renderer in a controller-owned rectangle. */
5
5
  export class PageAdapter {
6
6
  opts;
@@ -25,7 +25,8 @@ export class PageAdapter {
25
25
  opts.onProgress?.(responses);
26
26
  },
27
27
  onComplete: opts.onComplete,
28
- onExit: () => { if (awaitsResponse(this.manifest))
28
+ // Leaving a page that asks something must not settle it on an empty answer.
29
+ onExit: () => { if (pageAwaitsResponse(this.manifest))
29
30
  opts.onBack();
30
31
  else
31
32
  opts.onComplete({}); },
@@ -1,4 +1,4 @@
1
- import type { CommentAnchor, PageManifest, PageSlot, SlotResponse, TableConfig } from '../../../core/human/page-schema.js';
1
+ import type { CommentAnchor, PageSlot, SlotResponse, TableConfig } from '../../../core/human/page-schema.js';
2
2
  import type { TuiState } from './types.js';
3
3
  export type SelectGroup = 'option' | 'card' | 'row' | 'column';
4
4
  export type ActionRow = {
@@ -31,4 +31,3 @@ export declare function pageComplete(state: Pick<TuiState, 'manifest' | 'respons
31
31
  export declare function commentIdFor(anchor: CommentAnchor): string;
32
32
  /** The response-comment anchors this component may author: options take per-option, tables take rows only. */
33
33
  export declare function commentAnchors(slot: PageSlot): CommentAnchor[];
34
- export declare function awaitsResponse(manifest: PageManifest): boolean;
@@ -132,6 +132,3 @@ export function commentAnchors(slot) {
132
132
  return (slot.config.rows ?? []).map((row) => ({ kind: 'row', rowId: row.id }));
133
133
  return [];
134
134
  }
135
- export function awaitsResponse(manifest) {
136
- return manifest.delivery.reply;
137
- }
@@ -116,7 +116,7 @@ test('readPageTicket derives pending, resolved and canceled state and carries th
116
116
  cancelTicket(canceledDir, { reason: 'no longer needed' });
117
117
  assert.equal(readPageTicket(canceledDir).state, 'canceled');
118
118
  });
119
- test('page scan derives response expectation from manifest delivery', () => {
119
+ test('page scan derives response expectation from the page shape and its reply route', () => {
120
120
  const dir = submit('display', displayPage, '2026-01-04T00:00:00.000Z');
121
121
  const ticket = readPageTicket(dir);
122
122
  assert.equal(ticket.manifest.dialect, 'jsx');
@@ -135,6 +135,21 @@ test('page scan derives response expectation from manifest delivery', () => {
135
135
  assert.deepEqual(written['responses'], {});
136
136
  assert.equal(written['summary'], '');
137
137
  });
138
+ test('an inbox page that asks something waits on the person with no reply route', () => {
139
+ const stamp = (emittedAt) => ({ nodeId: 'node-durable', emittedAt });
140
+ // The programmatic request shape: answerable from the inbox, never routed to an agent bridge.
141
+ const durable = submitPage({ dir: ticketDir('durable-request'), sourceFile: askPage, source: stamp('2026-01-06T00:00:00.000Z'), delivery: { placement: 'panel', inbox: true, reply: false } }).dir;
142
+ submitPage({ dir: ticketDir('durable-notice'), sourceFile: displayPage, source: stamp('2026-01-06T00:00:01.000Z'), delivery: { placement: 'panel', inbox: true, reply: false } });
143
+ const listed = scanInbox();
144
+ const request = listed.find((item) => item.id === 'durable-request');
145
+ assert.equal(request?.kind === 'page' ? request.awaitsResponse : undefined, true, 'a page with response-bearing slots waits on the person even with no bridge to reply to');
146
+ const notice = listed.find((item) => item.id === 'durable-notice');
147
+ assert.equal(notice?.kind === 'page' ? notice.awaitsResponse : undefined, false, 'a display-only notice waits on nothing');
148
+ const claim = claimTicket(durable);
149
+ const answered = completePage(durable, { rollout: { selectedOptionIds: ['canary'], comments: [] } }, claim.token);
150
+ assert.equal(answered.won, true, 'a request with no reply route still takes a real answer');
151
+ assert.deepEqual(JSON.parse(readFileSync(responsePath(durable), 'utf8'))['responses'], { rollout: { selectedOptionIds: ['canary'], comments: [] } });
152
+ });
138
153
  test('scanPageHistory returns every page of one node, oldest first, regardless of terminal file', () => {
139
154
  submit('older', askPage, '2026-01-01T00:00:00.000Z');
140
155
  submit('other-node', askPage, '2026-01-02T00:00:00.000Z', 'node-2');
@@ -219,6 +219,7 @@ export interface PageSource {
219
219
  export interface PageDelivery {
220
220
  placement: 'inline' | 'panel';
221
221
  inbox: boolean;
222
+ /** Routing only: whether an answer travels back to a resident agent's bridge. Never whether the page can be answered — {@link pageAwaitsResponse} decides that. */
222
223
  reply: boolean;
223
224
  }
224
225
  export interface PageManifest {
@@ -269,6 +270,8 @@ export interface PageOverviewActions {
269
270
  }
270
271
  /** Derive both overview facts from a manifest alone. Pure: no ticket state, no second read. */
271
272
  export declare function pageOverviewActions(manifest: PageManifest): PageOverviewActions;
273
+ /** Whether this page is waiting on a person: it asks something, or an agent bridge is parked on its reply. */
274
+ export declare function pageAwaitsResponse(manifest: PageManifest): boolean;
272
275
  /** The canonical response-bearing predicate used by every page consumer. */
273
276
  export declare function isResponseBearingSlot(slot: PageSlot): boolean;
274
277
  /** Validate a persisted manifest. Publish-time validation made its slots authoritative, so reads do not resolve a component catalog. */
@@ -149,6 +149,10 @@ export function pageOverviewActions(manifest) {
149
149
  fastAction: { kind: 'answer', responses: { [slot.id]: { selectedOptionIds: [recommendation.optionId], comments: [] } } },
150
150
  };
151
151
  }
152
+ /** Whether this page is waiting on a person: it asks something, or an agent bridge is parked on its reply. */
153
+ export function pageAwaitsResponse(manifest) {
154
+ return manifest.delivery.reply || manifest.slots.some(isResponseBearingSlot);
155
+ }
152
156
  /** The canonical response-bearing predicate used by every page consumer. */
153
157
  export function isResponseBearingSlot(slot) {
154
158
  if (slot.display === true)
@@ -3,7 +3,7 @@ import { basename, join } from 'node:path';
3
3
  import { claimPath, isResolved, pageManifestPath, reviewPath } from './convention.js';
4
4
  import { readJsonOrNull } from '../fs-utils.js';
5
5
  import { validateReviewDescriptor } from './review-schema.js';
6
- import { pageOverviewActions } from './page-schema.js';
6
+ import { pageAwaitsResponse, pageOverviewActions } from './page-schema.js';
7
7
  import { parsePage } from './page.js';
8
8
  import { pageTicketState, readTicketResult } from './tickets.js';
9
9
  import { ticketsRoot } from './root.js';
@@ -56,7 +56,7 @@ function pageSummary(dir, id) {
56
56
  inbox: manifest.delivery.inbox,
57
57
  steps: manifest.steps,
58
58
  slotKinds: manifest.slots.map((slot) => slot.kind),
59
- awaitsResponse: manifest.delivery.reply,
59
+ awaitsResponse: pageAwaitsResponse(manifest),
60
60
  source: manifest.source ?? {},
61
61
  emittedAt,
62
62
  state,
@@ -64,6 +64,7 @@ export interface PageTicketSummary {
64
64
  inbox: boolean;
65
65
  steps: number;
66
66
  slotKinds: string[];
67
+ /** This ticket is waiting on the person: it asks something, or an agent bridge is parked on its reply. */
67
68
  awaitsResponse: boolean;
68
69
  /** Every question's recommended option, for a row that says what is suggested without opening the page. */
69
70
  recommendedOptions: import('./page-schema.js').RecommendedOption[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.225",
3
+ "version": "0.3.226",
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.225",
3
+ "version": "0.3.226",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.225",
9
+ "version": "0.3.226",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {