@north-light/crouter 0.3.185 → 0.3.186

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.
@@ -22,9 +22,13 @@ export function listMemoryDocs(kindFilter, scopeFilter, quiet = false, docs = li
22
22
  if (kindFilter !== undefined && sub.kind !== kindFilter)
23
23
  continue;
24
24
  // A directory INDEX surfaces as a dir entry: its dir path with a trailing
25
- // slash, flagged is_dir, so the inventory distinguishes it from a doc.
25
+ // slash, flagged is_dir, so the inventory distinguishes it from a doc. A
26
+ // store-root INDEX has no dir path, so it keeps its canonical `INDEX`
27
+ // name — every name in this column must be one `crtr memory read` and a
28
+ // `[[link]]` accept, and a bare `/` is neither.
26
29
  const isDir = isIndexName(sub.name);
27
- const name = isDir ? indexDirOf(sub.name) + '/' : sub.name;
30
+ const dirPath = isDir ? indexDirOf(sub.name) : '';
31
+ const name = dirPath === '' ? sub.name : dirPath + '/';
28
32
  addItem({ name, kind: sub.kind, scope: sub.scope, path: sub.path, shortForm: sub.shortForm, isDir, slash: sub.slash });
29
33
  }
30
34
  items.sort((a, b) => {
@@ -53,7 +57,7 @@ export const listLeaf = defineLeaf({
53
57
  { kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from next_cursor. Omit on first call.' },
54
58
  ],
55
59
  output: [
56
- { name: 'items', type: 'object[]', required: true, constraint: 'One row per document: {name, short_form, kind, scope, is_dir}. path is included only with --paths. A directory INDEX.md surfaces as a dir entry (is_dir true, name = the directory path with a trailing slash); is_dir is false for an ordinary doc. short_form is the abbreviated hook — shown here and nowhere else. Sorted by scope then kind then name ascending.' },
60
+ { name: 'items', type: 'object[]', required: true, constraint: 'One row per document: {name, short_form, kind, scope, is_dir}. path is included only with --paths. A directory INDEX.md surfaces as a dir entry (is_dir true, name = the directory path with a trailing slash, or `INDEX` for a store-root INDEX that has no directory path); is_dir is false for an ordinary doc. Every name is readable as-is with `crtr memory read`. short_form is the abbreviated hook — shown here and nowhere else. Sorted by scope then kind then name ascending.' },
57
61
  { name: 'next_cursor', type: 'string | null', required: true, constraint: 'Opaque token for the next page; null means no more items.' },
58
62
  { name: 'total', type: 'integer | null', required: true, constraint: 'Exact total across all pages when cheap to compute (always cheap here); never null in practice for this leaf.' },
59
63
  { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read a document in full or narrow the inventory.' },
@@ -98,12 +98,10 @@ test('a welcome is a COMPLETE catch-up: a re-welcome drops a dead broker s level
98
98
  const s0 = fold(initialSessionState(), [
99
99
  welcome(),
100
100
  { type: 'extension_ui_request', id: 'u1', method: 'setStatus', statusKey: 'gone', statusText: 'stale' },
101
- { type: 'queue_update', steering: ['one', 'two'], followUp: [], steeringIds: ['id-1'] },
101
+ { type: 'queue_update', steering: ['one', 'two'], followUp: [] },
102
102
  ]);
103
103
  assert.equal(s0.display.statuses['gone'], 'stale');
104
- // Broker-enriched ids zip positionally; a position without one (older broker,
105
- // short array) still renders as a bare-text entry.
106
- assert.deepEqual(s0.queued, [{ id: 'id-1', text: 'one' }, { text: 'two' }]);
104
+ assert.deepEqual(s0.queued, [{ text: 'one' }, { text: 'two' }]);
107
105
  assert.equal(s0.engine?.pendingMessageCount, 2);
108
106
  // A fresh broker (post-revive) welcomes with its OWN levels and an empty queue,
109
107
  // and emits no queue_update to overwrite the panel — so the welcome must.
@@ -1,13 +1,11 @@
1
1
  import type { BrokerToClient } from '../broker-protocol.js';
2
2
  import type { BrokerSession } from './read-ops.js';
3
3
  import type { BrokerClientRegistry } from './client-registry.js';
4
- import type { MessageIdLedger } from './message-ledger.js';
5
4
  import type { SubscriptionGeneration } from './rebind.js';
6
5
  import type { FaultRetry } from './fault-retry.js';
7
6
  type EventProjectionDeps = {
8
7
  registry: BrokerClientRegistry;
9
8
  faultRetry: FaultRetry;
10
- ledger: MessageIdLedger;
11
9
  installedGeneration: () => SubscriptionGeneration | undefined;
12
10
  currentSession: () => BrokerSession;
13
11
  notifyTurnAccepted: () => void;
@@ -33,12 +31,7 @@ export declare class EventProjection {
33
31
  reset(): void;
34
32
  dispose(): void;
35
33
  knownStreamWait(deadlineMs: number): () => void;
36
- /** Message-identity enrichment (see MessageIdLedger): fold pi's queue state
37
- * into the ledger and relay id arrays parallel to the texts; attach the
38
- * retired/dispatched id to the user message that carries it. Both are
39
- * additive fields on the verbatim pi event — consumers read or ignore them. */
40
- private withMessageIdentity;
41
- relayEvent(rawEvent: BrokerToClient, generation: SubscriptionGeneration): void;
34
+ relayEvent(event: BrokerToClient, generation: SubscriptionGeneration): void;
42
35
  private clearPendingUpdate;
43
36
  private flushPendingUpdate;
44
37
  private invalidatePendingBroadcasts;
@@ -1,4 +1,3 @@
1
- import { contentText } from '@earendil-works/pi-ai';
2
1
  import { readConfig } from '../../config.js';
3
2
  import { emitEvent } from '../../events/emit.js';
4
3
  import { releaseKnownStreamWait, resolveStreamWatchdogMs, StreamWatchdog } from '../stream-watchdog.js';
@@ -65,30 +64,7 @@ export class EventProjection {
65
64
  releaseKnownStreamWait(this.streamWatchdog, generation.id, waitingSession.isStreaming, generation.executingTools.size > 0);
66
65
  };
67
66
  }
68
- /** Message-identity enrichment (see MessageIdLedger): fold pi's queue state
69
- * into the ledger and relay id arrays parallel to the texts; attach the
70
- * retired/dispatched id to the user message that carries it. Both are
71
- * additive fields on the verbatim pi event — consumers read or ignore them. */
72
- withMessageIdentity(event) {
73
- if (event.type === 'queue_update') {
74
- const ids = this.deps.ledger.reconcileQueueUpdate(event.steering, event.followUp);
75
- const enriched = { ...event, steeringIds: ids.steeringIds, followUpIds: ids.followUpIds };
76
- return enriched;
77
- }
78
- if (event.type === 'message_start' && event.message.role === 'user') {
79
- // contentText(content, '') is pi's OWN retirement text extraction
80
- // (agent-session.js _handleAgentEvent) — the same function, so the
81
- // ledger's text matching cannot diverge from pi's queue splice.
82
- const crtrMessageId = this.deps.ledger.takeUserMessageId(contentText(event.message.content, ''));
83
- if (crtrMessageId === undefined)
84
- return event;
85
- const enriched = { ...event, crtrMessageId };
86
- return enriched;
87
- }
88
- return event;
89
- }
90
- relayEvent(rawEvent, generation) {
91
- const event = this.withMessageIdentity(rawEvent);
67
+ relayEvent(event, generation) {
92
68
  this.deps.faultRetry.onEvent(event);
93
69
  const type = event.type;
94
70
  if (type === 'message_start' && event.message.role === 'user') {
@@ -6,7 +6,6 @@ import type { BrokerClient, BrokerClientRegistry } from './client-registry.js';
6
6
  import { type BrokerSession } from './read-ops.js';
7
7
  import type { ToolGroupTracker } from './tool-groups.js';
8
8
  import type { EventProjection } from './event-projection.js';
9
- import type { MessageIdLedger } from './message-ledger.js';
10
9
  import type { RebindDriver } from './rebind.js';
11
10
  export declare const REVIEW_BOUNDARY_NAVIGATION_CODE = "review_boundary_navigation";
12
11
  export declare class ReviewBoundaryNavigationError extends Error {
@@ -181,7 +180,6 @@ export type FrameDispatchDeps = {
181
180
  registry: BrokerClientRegistry;
182
181
  toolGroups: ToolGroupTracker;
183
182
  projection: EventProjection;
184
- ledger: MessageIdLedger;
185
183
  rebind: RebindDriver;
186
184
  reviewCompanion: boolean;
187
185
  pendingDialogs: Map<string, PendingDialog>;
@@ -279,7 +279,7 @@ export function stripHiddenEntriesFromJsonlExport(filePath) {
279
279
  }
280
280
  /** Owns frame-local input state while dispatching through explicit broker collaborators. */
281
281
  export function createFrameDispatchContext(deps) {
282
- const { nodeId, cfg, registry, toolGroups, projection, ledger, rebind, reviewCompanion, pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit, persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec, formatExactModelSpec, resolveLaunchModel, } = deps;
282
+ const { nodeId, cfg, registry, toolGroups, projection, rebind, reviewCompanion, pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit, persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec, formatExactModelSpec, resolveLaunchModel, } = deps;
283
283
  const currentSession = () => rebind.session();
284
284
  const currentServices = () => rebind.services();
285
285
  let kickoffPending;
@@ -301,14 +301,9 @@ export function createFrameDispatchContext(deps) {
301
301
  const prompt = kickoffPending;
302
302
  kickoffPending = undefined;
303
303
  kickoffInFlight = true;
304
- // The kickoff prompt is a broker-originated dispatch — mint its identity
305
- // here so its relayed user message_start carries an id like any other.
306
- const kickoffMessageId = randomUUID();
307
- ledger.noteDispatch(kickoffMessageId, prompt);
308
304
  trackPromptDispatch(operationIdContext.fresh(() => currentSession()
309
305
  .prompt(prompt)
310
306
  .catch((err) => {
311
- ledger.dropIntent(kickoffMessageId);
312
307
  emitEvent({
313
308
  level: 'error',
314
309
  event: 'broker.prompt.first_failed',
@@ -397,7 +392,6 @@ export function createFrameDispatchContext(deps) {
397
392
  // is producing now, then start this as a new turn. A normal mid-stream
398
393
  // prompt deliberately steers into pi's queue; the whip must not.
399
394
  if (frame.type === 'prompt' && frame.interrupt === true) {
400
- const whipMessageId = frame.message_id;
401
395
  return (async () => {
402
396
  if (currentSession().isBashRunning) {
403
397
  currentSession().abortBash();
@@ -420,7 +414,6 @@ export function createFrameDispatchContext(deps) {
420
414
  type: 'prompt',
421
415
  text: frame.text,
422
416
  ...(frame.images !== undefined ? { images: frame.images } : {}),
423
- ...(whipMessageId !== undefined ? { message_id: whipMessageId } : {}),
424
417
  });
425
418
  })().catch((err) => {
426
419
  relayError(err);
@@ -451,19 +444,6 @@ export function createFrameDispatchContext(deps) {
451
444
  const dispatch = () => {
452
445
  const route = resolveEngineRoute(frame, currentSession().isStreaming);
453
446
  const guidance = resolveFrameGuidance(frame.text);
454
- // Message identity (see MessageIdLedger): note the dispatch intent
455
- // before the engine call so pi's synchronous queue_update/message_start
456
- // can adopt it. A leading engine command never produces a matching
457
- // user message — no intent. On rejection the intent is dropped.
458
- let noteFailure = relayError;
459
- if (!isLeadingEngineCommand(frame.text, currentSession())) {
460
- const messageId = frame.message_id ?? randomUUID();
461
- ledger.noteDispatch(messageId, frame.text);
462
- noteFailure = (err) => {
463
- ledger.dropIntent(messageId);
464
- relayError(err);
465
- };
466
- }
467
447
  if (guidance === null) {
468
448
  if (route.call === 'prompt') {
469
449
  // A routed prompt STARTS a turn — including an idle follow_up
@@ -472,12 +452,12 @@ export function createFrameDispatchContext(deps) {
472
452
  // steer.
473
453
  return settle(trackPromptDispatch(operationIdContext.fresh(() => currentSession()
474
454
  .prompt(frame.text, route.options)
475
- .catch(noteFailure))));
455
+ .catch(relayError))));
476
456
  }
477
457
  // C1: pi's followUp() takes images as a POSITIONAL 2nd arg (`followUp(text, images?)`).
478
458
  return settle(currentSession()
479
459
  .followUp(frame.text, frame.images)
480
- .catch(noteFailure));
460
+ .catch(relayError));
481
461
  }
482
462
  const deliverAs = chooseGuidanceDeliveryMode(frame.type, route.call, currentSession().isStreaming);
483
463
  if (route.call === 'prompt') {
@@ -489,18 +469,18 @@ export function createFrameDispatchContext(deps) {
489
469
  // ATOMICALLY clears its own pending entry if prompt() settles
490
470
  // without ever reaching that drain (an early throw/return leaves
491
471
  // nothing stranded to leak into a later, unrelated turn).
492
- return settle(trackPromptDispatch(promptWithNextTurnGuidance(currentSession(), guidance, () => operationIdContext.fresh(() => currentSession().prompt(frame.text, route.options))).catch(noteFailure)));
472
+ return settle(trackPromptDispatch(promptWithNextTurnGuidance(currentSession(), guidance, () => operationIdContext.fresh(() => currentSession().prompt(frame.text, route.options))).catch(relayError)));
493
473
  }
494
474
  return settle(trackPromptDispatch(operationIdContext.fresh(() => currentSession()
495
475
  .prompt(frame.text, route.options)
496
- .catch(noteFailure)
476
+ .catch(relayError)
497
477
  .then(() => sendGuidance(guidance, deliverAs)))));
498
478
  }
499
479
  // deliverAs is always 'followUp' here per chooseGuidanceDeliveryMode's
500
480
  // contract (a followUp routeCall never yields 'nextTurn').
501
481
  return settle(currentSession()
502
482
  .followUp(frame.text, frame.images)
503
- .catch(noteFailure)
483
+ .catch(relayError)
504
484
  .then(() => sendGuidance(guidance, deliverAs)));
505
485
  };
506
486
  // /login acknowledges before its asynchronous local refresh completes.
@@ -530,8 +510,6 @@ export function createFrameDispatchContext(deps) {
530
510
  // and executes it immediately even while streaming. It also expands a
531
511
  // matching skill/template before applying the same steer behavior.
532
512
  if (resolveSteerCall(frame.text, currentSession()) === 'prompt') {
533
- // The steer→prompt leading-command path never yields a matching user
534
- // message — no ledger intent (see MessageIdLedger).
535
513
  void currentSession()
536
514
  .prompt(frame.text, {
537
515
  images: frame.images,
@@ -540,25 +518,17 @@ export function createFrameDispatchContext(deps) {
540
518
  .catch(relayError);
541
519
  break;
542
520
  }
543
- // Message identity: note the intent before steer() so pi's synchronous
544
- // enqueue queue_update can adopt it; drop it if the call rejects.
545
- const steerMessageId = frame.message_id ?? randomUUID();
546
- ledger.noteDispatch(steerMessageId, frame.text);
547
- const steerFailure = (err) => {
548
- ledger.dropIntent(steerMessageId);
549
- relayError(err);
550
- };
551
521
  const guidance = resolveFrameGuidance(frame.text);
552
522
  // C1: pi's steer() takes images as a POSITIONAL 2nd arg (`steer(text, images?)`).
553
523
  if (guidance === null) {
554
524
  void currentSession()
555
525
  .steer(frame.text, frame.images)
556
- .catch(steerFailure);
526
+ .catch(relayError);
557
527
  break;
558
528
  }
559
529
  void currentSession()
560
530
  .steer(frame.text, frame.images)
561
- .catch(steerFailure)
531
+ .catch(relayError)
562
532
  .then(() => sendGuidance(guidance, 'steer'));
563
533
  break;
564
534
  }
@@ -836,9 +806,6 @@ export function createFrameDispatchContext(deps) {
836
806
  type: 'prompt',
837
807
  text: frame.text,
838
808
  ...(frame.images !== undefined ? { images: frame.images } : {}),
839
- // Carry the sender-minted message identity onto the synthesized prompt
840
- // frame so a gateway/Core deliver retires its optimistic entry by id.
841
- ...(frame.message_id !== undefined ? { message_id: frame.message_id } : {}),
842
809
  });
843
810
  if (via === 'steer' || outcome === undefined) {
844
811
  turnAcceptedWaiters.delete(ackAccepted);
@@ -78,27 +78,16 @@ export interface PromptFrame {
78
78
  images?: ImageContent[];
79
79
  /** Abort the active turn before starting this prompt. Used by the attach viewer's whip action so its command replaces, rather than joins, in-flight work. */
80
80
  interrupt?: true;
81
- /** Sender-minted message identity (see MessageIdLedger). The broker mirrors
82
- * it through pi's id-less queues and echoes it back on the relayed user
83
- * `message_start` (`crtrMessageId`) and enriched `queue_update`
84
- * (`steeringIds`/`followUpIds`), so an optimistic sender retires its
85
- * pending entry by exact id instead of text/order guessing. Absent (a
86
- * legacy client, kickoff, inbox): the broker mints one itself. */
87
- message_id?: string;
88
81
  }
89
82
  export interface SteerFrame {
90
83
  type: 'steer';
91
84
  text: string;
92
85
  images?: ImageContent[];
93
- /** Sender-minted message identity — see {@link PromptFrame.message_id}. */
94
- message_id?: string;
95
86
  }
96
87
  export interface FollowUpFrame {
97
88
  type: 'follow_up';
98
89
  text: string;
99
90
  images?: ImageContent[];
100
- /** Sender-minted message identity — see {@link PromptFrame.message_id}. */
101
- message_id?: string;
102
91
  }
103
92
  export interface AbortFrame {
104
93
  type: 'abort';
@@ -122,9 +111,6 @@ export interface DeliverFrame {
122
111
  id: string;
123
112
  text: string;
124
113
  images?: ImageContent[];
125
- /** Sender-minted message identity — see {@link PromptFrame.message_id}.
126
- * Distinct from `id`, the one-shot ack-correlation token. */
127
- message_id?: string;
128
114
  }
129
115
  /** Run a `!` bash command — writable clients only. Maps to `session.executeBash()`,
130
116
  * which runs the command, records a `bashExecution` message in context, and
@@ -365,38 +351,7 @@ export interface WelcomeFrame {
365
351
  * can construct an AuthStorage/ModelRegistry pointing at the SAME auth.json.
366
352
  * crtr surface attach is tmux-local only: broker + viewer share a filesystem. */
367
353
  agentDir?: string;
368
- /** The last few id-bearing user-message dispatches from the broker's message
369
- * ledger, oldest first — so a reattaching follower (Core's snapshot wake
370
- * replay) can key the snapshot's trailing user wakes by id instead of
371
- * discarding them heuristically. Live-process memory only: a broker restart
372
- * loses it (accepted — optimistic pending sends don't survive that either).
373
- * Absent on a broker pinned to an older runtime generation. */
374
- recentUserMessages?: Array<{
375
- id: string;
376
- text: string;
377
- }>;
378
- }
379
- /** A relayed pi `queue_update`, enriched by the broker with id arrays PARALLEL
380
- * to pi's text arrays (`steeringIds[i]` identifies `steering[i]`). The ids are
381
- * the broker's message ledger mirror of pi's queues — sender-minted when the
382
- * frame carried `message_id`, broker-minted otherwise. Absent on a broker
383
- * pinned to an older runtime generation; consumers fall back to bare texts. */
384
- export type RelayedQueueUpdate = Extract<AgentSessionEvent, {
385
- type: 'queue_update';
386
- }> & {
387
- steeringIds?: string[];
388
- followUpIds?: string[];
389
- };
390
- /** A relayed user `message_start`, enriched by the broker with the message's
391
- * crouter identity — the id staged by the retirement `queue_update` pi emits
392
- * synchronously before it, or the matching dispatch intent for an idle
393
- * prompt. Absent when the message carries none (an engine-command expansion,
394
- * a pre-ledger replay, an older broker). */
395
- export type RelayedUserMessageStart = Extract<AgentSessionEvent, {
396
- type: 'message_start';
397
- }> & {
398
- crtrMessageId?: string;
399
- };
354
+ }
400
355
  /** Broadcast to EVERY client after a successful `set_model`/`cycle_model`. pi
401
356
  * emits no AgentSessionEvent for a model switch, so without this the new model
402
357
  * reaches no viewer at all (the requester gets only a bare ack) and footers
@@ -44,7 +44,6 @@ import { BrokerClientRegistry } from './broker/client-registry.js';
44
44
  import { ToolGroupTracker } from './broker/tool-groups.js';
45
45
  import { FaultRetry } from './broker/fault-retry.js';
46
46
  import { EventProjection } from './broker/event-projection.js';
47
- import { MessageIdLedger } from './broker/message-ledger.js';
48
47
  import { createFrameDispatchContext, handleFrame } from './broker/frame-dispatch.js';
49
48
  import { RebindDriver } from './broker/rebind.js';
50
49
  import { brokerExtensionState, commitBrokerModel } from './broker/daemon-ops.js';
@@ -285,9 +284,6 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
285
284
  let projection;
286
285
  let notifyTurnAccepted = () => { };
287
286
  let resetFrameState = () => { };
288
- // Broker-owned message identity (see MessageIdLedger): mirrors pi's id-less
289
- // queues so relayed queue_update/user message_start frames carry ids.
290
- const messageLedger = new MessageIdLedger();
291
287
  rebind = new RebindDriver({
292
288
  nodeId, cfg, session, services, runtime, registry, toolGroups,
293
289
  uiContext: () => uiContext,
@@ -297,7 +293,6 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
297
293
  disposeAndExit: (reason, status) => disposeAndExit(reason, status),
298
294
  onSessionRebind: () => {
299
295
  resetFrameState();
300
- messageLedger.reset();
301
296
  },
302
297
  reWelcomeAll: () => reWelcomeAll(),
303
298
  });
@@ -339,7 +334,6 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
339
334
  projection = new EventProjection({
340
335
  registry,
341
336
  faultRetry,
342
- ledger: messageLedger,
343
337
  installedGeneration: () => rebind.installedGeneration(),
344
338
  currentSession: () => rebind.session(),
345
339
  notifyTurnAccepted: () => notifyTurnAccepted(),
@@ -396,7 +390,6 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
396
390
  role: client.role,
397
391
  pending_dialog: first !== undefined ? first.request : null,
398
392
  agentDir: getAgentDir(),
399
- recentUserMessages: messageLedger.recentUserMessages(),
400
393
  }, true);
401
394
  // No display_* replay: the retained extension state rides IN the snapshot
402
395
  // (`snapshot.display`), so catch-up is one atomic frame. A viewer attaching
@@ -509,7 +502,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
509
502
  rebind.setRebindSession();
510
503
  await rebind.enqueueRebind(session, services);
511
504
  const frameDispatch = createFrameDispatchContext({
512
- nodeId, cfg, registry, toolGroups, projection, ledger: messageLedger, rebind, reviewCompanion,
505
+ nodeId, cfg, registry, toolGroups, projection, rebind, reviewCompanion,
513
506
  pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit,
514
507
  persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec,
515
508
  formatExactModelSpec, resolveLaunchModel,
@@ -6,11 +6,8 @@ export type LiveDeliverRoute = 'prompt' | 'steer';
6
6
  export type LiveInterruptOutcome = 'turn' | 'bash' | 'idle';
7
7
  /** Deliver `text` to a LIVE node's engine on its serialized frame loop. The
8
8
  * broker routes it itself (idle → prompt, streaming → steer) and acks once
9
- * routed; resolves with the route taken. `messageId` is the sender-minted
10
- * message identity (`SendMessageRequest.message_id`), carried on the deliver
11
- * frame so the broker's ledger echoes it back on the relayed user
12
- * `message_start` — distinct from the one-shot ack-correlation id. */
13
- export declare function deliverLive(nodeId: string, text: string, messageId?: string): Promise<LiveDeliverRoute>;
9
+ * routed; resolves with the route taken. */
10
+ export declare function deliverLive(nodeId: string, text: string): Promise<LiveDeliverRoute>;
14
11
  /** Abort whatever a LIVE node's engine is doing (turn or `!` bash). Resolves
15
12
  * once the broker has PROCESSED the abort — which, on the single frame loop,
16
13
  * also implies every earlier-accepted deliver was already routed (so a
@@ -14,17 +14,14 @@
14
14
  import { oneShotControllerRequest } from './broker-request.js';
15
15
  /** Deliver `text` to a LIVE node's engine on its serialized frame loop. The
16
16
  * broker routes it itself (idle → prompt, streaming → steer) and acks once
17
- * routed; resolves with the route taken. `messageId` is the sender-minted
18
- * message identity (`SendMessageRequest.message_id`), carried on the deliver
19
- * frame so the broker's ledger echoes it back on the relayed user
20
- * `message_start` — distinct from the one-shot ack-correlation id. */
21
- export function deliverLive(nodeId, text, messageId) {
17
+ * routed; resolves with the route taken. */
18
+ export function deliverLive(nodeId, text) {
22
19
  return oneShotControllerRequest({
23
20
  nodeId,
24
21
  what: 'the interactive delivery',
25
22
  ackFor: 'deliver',
26
23
  correlated: true,
27
- frame: (id) => ({ type: 'deliver', id, text, ...(messageId !== undefined ? { message_id: messageId } : {}) }),
24
+ frame: (id) => ({ type: 'deliver', id, text }),
28
25
  result: (detail) => (detail === 'steer' ? 'steer' : 'prompt'),
29
26
  });
30
27
  }
@@ -24,13 +24,10 @@ export interface SessionState {
24
24
  /** Live context occupancy in prompt tokens: seeded from the snapshot's
25
25
  * contextUsage, then tracked off each assistant `message_end`. */
26
26
  contextTokens: number | undefined;
27
- /** Queued steering + follow-up messages, from `queue_update`. `id` is the
28
- * broker's message-ledger identity for the entry (`steeringIds`/
29
- * `followUpIds`), absent on a broker pinned to an older runtime generation.
30
- * A fresh broker with an empty queue emits no `queue_update`, so every
31
- * welcome resets this rather than leaving a dead broker's rows on screen. */
27
+ /** Queued steering + follow-up messages, from `queue_update`. A fresh broker
28
+ * with an empty queue emits no `queue_update`, so every welcome resets this
29
+ * rather than leaving a dead broker's rows on screen. */
32
30
  queued: Array<{
33
- id?: string;
34
31
  text: string;
35
32
  }>;
36
33
  display: DisplayState;
@@ -132,11 +132,7 @@ export function reduce(state, frame) {
132
132
  case 'thinking_level_changed':
133
133
  return patchEngine(state, { thinkingLevel: frame.level });
134
134
  case 'queue_update': {
135
- // The broker enriches relayed queue_updates with id arrays parallel to
136
- // pi's text arrays; zip them, tolerating their absence (older broker).
137
- const { steeringIds, followUpIds } = frame;
138
- const zip = (texts, ids) => texts.map((text, i) => (ids?.[i] !== undefined ? { id: ids[i], text } : { text }));
139
- const queued = [...zip(frame.steering, steeringIds), ...zip(frame.followUp, followUpIds)];
135
+ const queued = [...frame.steering, ...frame.followUp].map((text) => ({ text }));
140
136
  const next = patchEngine(state, { pendingMessageCount: queued.length });
141
137
  return { ...next, queued };
142
138
  }
@@ -59,12 +59,6 @@ function parseSendBody(body) {
59
59
  if (delivery !== undefined && delivery !== 'interactive') {
60
60
  throw usage(`invalid delivery: ${String(delivery)} (expected interactive)`);
61
61
  }
62
- const messageId = b['message_id'];
63
- if (messageId !== undefined && typeof messageId !== 'string') {
64
- throw usage('message_id must be a string');
65
- }
66
- if (typeof messageId === 'string' && messageId.trim() !== '')
67
- out.message_id = messageId;
68
62
  if (delivery === 'interactive') {
69
63
  // Interactive delivery is a live-conversation send: a plain immediate body,
70
64
  // nothing that only makes sense on the durable path.
@@ -164,7 +158,7 @@ async function handleMessage(ctx) {
164
158
  // straight there (append + revive, watcher delivers post-boot). ---
165
159
  if (req.delivery === 'interactive' && isBrokerLive(meta)) {
166
160
  try {
167
- await deliverLive(id, req.body, req.message_id);
161
+ await deliverLive(id, req.body);
168
162
  const body = {
169
163
  node_id: id,
170
164
  delivered: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.185",
3
+ "version": "0.3.186",
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.185",
3
+ "version": "0.3.186",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.185",
9
+ "version": "0.3.186",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -1,53 +0,0 @@
1
- /** One id-bearing message the ledger tracks. */
2
- export interface LedgerEntry {
3
- id: string;
4
- text: string;
5
- }
6
- export declare class MessageIdLedger {
7
- private steering;
8
- private followUp;
9
- private pendingIntents;
10
- private stagedRetirementId;
11
- private recent;
12
- /** Record an accepted client frame's identity before its engine call runs.
13
- * `id` is the sender's `message_id` when it minted one, else broker-minted. */
14
- noteDispatch(id: string, text: string): void;
15
- /** Remove a noted intent whose engine call rejected — the message will never
16
- * appear in a queue or as a user message. No-op if already adopted. */
17
- dropIntent(id: string): void;
18
- /**
19
- * Fold one pi `queue_update` into the ledger and return the id arrays
20
- * parallel to its text arrays (total: every position gets an id).
21
- *
22
- * Per-queue positional diff against the mirrored entries: a text matching an
23
- * old entry at-or-after the cursor keeps its id (everything skipped is
24
- * removed); an unmatched text ADOPTS the first pending intent with equal
25
- * text, else mints a fresh id. Exactly ONE removal across both queues is
26
- * pi's per-message retirement (its queue_update precedes the user
27
- * `message_start` it belongs to) and is staged for `takeUserMessageId`;
28
- * 0 or >1 removals (an enqueue, clearQueue/dequeue, abort) stage nothing.
29
- */
30
- reconcileQueueUpdate(steering: readonly string[], followUp: readonly string[]): {
31
- steeringIds: string[];
32
- followUpIds: string[];
33
- };
34
- /**
35
- * The id for a relayed user `message_start`, or undefined when this message
36
- * carries none (an engine-command expansion, a text pi transformed, or a
37
- * pre-ledger replay). A staged retirement wins — pi emitted its queue_update
38
- * synchronously right before this message; otherwise the first pending
39
- * intent with equal text (the idle-prompt case, which never touches a
40
- * queue). Id-bearing results are remembered for `recentUserMessages`.
41
- */
42
- takeUserMessageId(messageText: string): string | undefined;
43
- /** The last {@link MAX_RECENT} id-bearing user-message dispatches, oldest
44
- * first — the `welcome.recentUserMessages` payload. */
45
- recentUserMessages(): Array<{
46
- id: string;
47
- text: string;
48
- }>;
49
- /** Session rebind/replacement: the mirrored queues, staged retirement, and
50
- * recent list all described the OLD session — drop everything. */
51
- reset(): void;
52
- private remember;
53
- }