@north-light/crouter 0.3.250 → 0.3.252

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.
Files changed (50) hide show
  1. package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +1 -5
  2. package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -1
  3. package/dist/clients/attach/__tests__/completion-frecency.test.d.ts +1 -0
  4. package/dist/clients/attach/__tests__/completion-frecency.test.js +35 -0
  5. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +3 -1
  6. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +2 -1
  7. package/dist/clients/attach/input/completion-frecency.d.ts +45 -0
  8. package/dist/clients/attach/input/completion-frecency.js +141 -0
  9. package/dist/clients/attach/input/controller.d.ts +17 -0
  10. package/dist/clients/attach/input/controller.js +40 -0
  11. package/dist/clients/attach/input/ref-autocomplete.d.ts +3 -1
  12. package/dist/clients/attach/input/ref-autocomplete.js +49 -8
  13. package/dist/clients/attach/input/titled-editor.d.ts +3 -0
  14. package/dist/clients/attach/input/titled-editor.js +5 -0
  15. package/dist/clients/attach/session/editor-inventory.d.ts +3 -0
  16. package/dist/clients/attach/session/editor-inventory.js +1 -1
  17. package/dist/clients/attach/session/input-wiring.d.ts +3 -0
  18. package/dist/clients/attach/session/input-wiring.js +2 -0
  19. package/dist/clients/attach/viewer.js +578 -578
  20. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +98 -1
  21. package/dist/core/__tests__/canvas-inbox-watcher-naming.test.js +4 -1
  22. package/dist/core/__tests__/canvas-inbox-watcher.test.js +4 -1
  23. package/dist/core/__tests__/fixtures/fake-engine.d.ts +3 -1
  24. package/dist/core/__tests__/fixtures/fake-engine.js +16 -3
  25. package/dist/core/__tests__/integration/deferred-no-wake.test.js +4 -1
  26. package/dist/core/__tests__/migration.test.js +57 -11
  27. package/dist/core/__tests__/seam/broker-provider-retry.test.js +17 -2
  28. package/dist/core/__tests__/seam/dormancy-release.test.js +1 -0
  29. package/dist/core/__tests__/seam/held-deferred-human-prompt.test.d.ts +1 -0
  30. package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +63 -0
  31. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +9 -2
  32. package/dist/core/canvas/migrations.js +63 -24
  33. package/dist/core/runtime/broker/engine-drive.js +20 -3
  34. package/dist/core/runtime/broker/fault-retry.js +31 -13
  35. package/dist/core/runtime/broker/held-deferred-inbox.d.ts +13 -0
  36. package/dist/core/runtime/broker/held-deferred-inbox.js +13 -0
  37. package/dist/core/runtime/broker.js +19 -0
  38. package/dist/core/runtime/close.js +2 -2
  39. package/dist/core/runtime/fault.js +8 -5
  40. package/dist/core/runtime/recycle.js +63 -41
  41. package/dist/core/runtime/reset.d.ts +1 -1
  42. package/dist/core/runtime/reset.js +25 -2
  43. package/dist/daemon/__tests__/helpers/source-daemon.js +1 -0
  44. package/dist/daemon/api/handlers/messages.js +1 -1
  45. package/dist/daemon/api/handlers/reports.js +1 -1
  46. package/dist/daemon/cron/sinks.js +1 -1
  47. package/dist/daemon/messaging/node-message.js +1 -1
  48. package/dist/pi-extensions/canvas-inbox-watcher.js +109 -5
  49. package/package.json +1 -1
  50. package/runtime.lock.json +2 -2
@@ -10,7 +10,10 @@
10
10
  // Context sends never trigger a turn: the visible human delivery is the sole
11
11
  // trigger, so the agent cannot run on context alone before hearing the person.
12
12
  // A context-only batch keeps its wake by triggering on its LAST card while idle,
13
- // and rides the running turn while streaming.
13
+ // and rides the running turn while streaming. A deferred-only idle batch stays
14
+ // held until either durable mail wakes a turn or the broker prepends it to an
15
+ // already-admitted live human prompt; that bridge still commits only at
16
+ // agent_settled.
14
17
  import { envNodeId, envProfileId, envWatcherDebounceMs, envWatcherTickMs } from '../shared/env.js';
15
18
  import { coalesceBrokerInbox, entryCards, readBrokerCanceledEntryIds, readBrokerCursor, readBrokerInboxSince, reportRefNodeId, } from '../core/runtime/broker/inbox.js';
16
19
  import { advanceBrokerInboxCursor, brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
@@ -20,6 +23,7 @@ import { errorClassFromError } from '../core/events/errors.js';
20
23
  import { operationIdContext } from '../core/events/operation-id.js';
21
24
  import { situationalContextEnvelope, SITUATIONAL_CONTEXT_CUSTOM_TYPE } from '../core/runtime/situational-context.js';
22
25
  import { isProfilePaused } from '../core/profiles/manifest.js';
26
+ import { brokerIdleTurnStart, HELD_DEFERRED_INBOX_PROMPT_JOIN, } from '../core/runtime/broker/held-deferred-inbox.js';
23
27
  const MAX_PENDING_HANDOFFS = 64;
24
28
  const MAX_SOURCE_OPERATION_IDS = 16;
25
29
  let currentWatcher;
@@ -188,6 +192,7 @@ class CanvasInboxWatcher {
188
192
  lastArrival = 0;
189
193
  pendingHandoffs = [];
190
194
  pendingCommit;
195
+ promptJoinInFlight = false;
191
196
  seeded = false;
192
197
  disposed = false;
193
198
  disposalReason;
@@ -250,7 +255,7 @@ class CanvasInboxWatcher {
250
255
  // every queued continuation has drained, so hasPendingMessages() must
251
256
  // not veto the commit here. Await crtrd before changing the in-memory
252
257
  // durable cursor; a failed commit remains replayable after process exit.
253
- const result = await this.settle(nodeId, ctx, false);
258
+ const result = await this.settle(nodeId, ctx, false, true);
254
259
  if (result === 'refresh_held')
255
260
  this.dispose('refresh', false);
256
261
  }
@@ -383,6 +388,7 @@ class CanvasInboxWatcher {
383
388
  this.reportNodes.clear();
384
389
  this.reportNodesAsked.clear();
385
390
  this.pendingCommit = undefined;
391
+ this.promptJoinInFlight = false;
386
392
  this.nodeId = undefined;
387
393
  this.lastCtx = undefined;
388
394
  this.lastArrival = 0;
@@ -531,6 +537,8 @@ class CanvasInboxWatcher {
531
537
  this.retryPendingCommitOnTick(nodeId);
532
538
  return;
533
539
  }
540
+ if (this.promptJoinInFlight)
541
+ return;
534
542
  // A paused profile is inert: read nothing, send nothing, leave the cursor
535
543
  // where it is, so every queued entry — critical included, since aborting the
536
544
  // live turn is exactly the punch-through pause forbids — is still there to
@@ -599,6 +607,9 @@ class CanvasInboxWatcher {
599
607
  const hiddenIds = new Set(hiddenEntries.map((entry) => entry.entry_id));
600
608
  const visibleIds = new Set(visibleEntries.map((entry) => entry.entry_id));
601
609
  const routedEntries = entries.filter((entry) => hiddenIds.has(entry.entry_id) || visibleIds.has(entry.entry_id));
610
+ // Deferred-only idle mail remains in this buffer until a non-deferred inbox
611
+ // entry starts delivery, or a live human prompt claims it through
612
+ // prependHeldDeferredInbox(). Neither path wakes the node on its own.
602
613
  const deferredHold = this.isIdle()
603
614
  && routedEntries.length > 0
604
615
  && routedEntries.every((entry) => entry.tier === 'deferred');
@@ -727,7 +738,10 @@ class CanvasInboxWatcher {
727
738
  this.pi.sendMessage({ customType: SITUATIONAL_CONTEXT_CUSTOM_TYPE, content: card, display: true }, route === 'idle' ? {} : { deliverAs: route });
728
739
  }
729
740
  if (route === 'idle') {
730
- this.pi.sendUserMessage(content);
741
+ const start = brokerIdleTurnStart();
742
+ if (start === undefined)
743
+ throw new Error('broker idle-turn admission is unavailable');
744
+ start(content);
731
745
  }
732
746
  else {
733
747
  this.pi.sendUserMessage(content, { deliverAs: route });
@@ -826,6 +840,83 @@ class CanvasInboxWatcher {
826
840
  this.requeueFromBatch(batch, execution.requeueEntryIds);
827
841
  return 'attempted';
828
842
  }
843
+ /** Claim the deferred-only idle batch for the human prompt already admitted
844
+ * by the broker. Each rendered delivery is persisted before that prompt, but
845
+ * no cursor may advance until the prompt's agent_settled event proves the
846
+ * turn consumed it. */
847
+ async prependHeldDeferredInbox(session) {
848
+ if (!this.canMutate() || this.nodeId === undefined || profilePaused())
849
+ return;
850
+ const plan = this.ordinaryPlan();
851
+ if (!plan.deferredHold)
852
+ return;
853
+ const deliveries = plan.visibleEntries.length === 0
854
+ ? []
855
+ : this.digest(plan.visibleEntries, 'poll');
856
+ const handoffCount = Number(plan.hiddenEntries.length > 0) + deliveries.length;
857
+ if (this.pendingHandoffs.length + handoffCount > MAX_PENDING_HANDOFFS)
858
+ return;
859
+ const batch = [...this.buffer];
860
+ this.promptJoinInFlight = true;
861
+ try {
862
+ if (plan.hiddenEntries.length > 0) {
863
+ await this.sendPromptJoinDelivery(session, 'hidden', plan.hiddenEntries, this.contextCards(this.nodeId, plan.hiddenEntries));
864
+ }
865
+ for (const delivery of deliveries) {
866
+ await this.sendPromptJoinDelivery(session, 'visible', delivery.entries, [...delivery.cards, delivery.text]);
867
+ }
868
+ if (!this.canMutate())
869
+ return;
870
+ this.buffer.splice(0, batch.length);
871
+ }
872
+ finally {
873
+ this.promptJoinInFlight = false;
874
+ }
875
+ }
876
+ async sendPromptJoinDelivery(session, channel, entries, messages) {
877
+ let facts;
878
+ try {
879
+ facts = deliveryFacts(entries, channel);
880
+ }
881
+ catch (error) {
882
+ this.raiseReadFatal('poll', error);
883
+ }
884
+ let operationId;
885
+ try {
886
+ await operationIdContext.fresh(async (freshOperationId) => {
887
+ operationId = freshOperationId;
888
+ emitBestEffort({
889
+ level: 'info',
890
+ event: 'inbox.delivery.started',
891
+ operation_id: freshOperationId,
892
+ fields: deliveryFields(facts),
893
+ });
894
+ for (const content of messages) {
895
+ await session.sendCustomMessage({
896
+ customType: SITUATIONAL_CONTEXT_CUSTOM_TYPE,
897
+ content,
898
+ display: true,
899
+ });
900
+ }
901
+ });
902
+ }
903
+ catch (error) {
904
+ if (operationId === undefined)
905
+ this.raiseReadFatal('poll', error);
906
+ emitDeliveryFailure(operationId, facts, 'send', error);
907
+ throw error;
908
+ }
909
+ if (operationId === undefined) {
910
+ this.raiseReadFatal('poll', new Error(`${channel} prompt-join delivery returned without an operation id`));
911
+ }
912
+ const handoff = { operationId, ...facts, settleAtAgentSettled: true };
913
+ if (!this.canMutate()) {
914
+ if (this.disposed && this.disposalReason !== undefined)
915
+ emitDeliveryAbandoned(handoff, this.disposalReason);
916
+ return;
917
+ }
918
+ this.pendingHandoffs.push(handoff);
919
+ }
829
920
  coveredEntryIds(channel) {
830
921
  const covered = new Set();
831
922
  for (const handoff of this.pendingHandoffs) {
@@ -883,7 +974,7 @@ class CanvasInboxWatcher {
883
974
  emitDeliveryCompleted(handoff);
884
975
  return 'settled';
885
976
  }
886
- async settle(nodeId, ctx, holdPendingMessages) {
977
+ async settle(nodeId, ctx, holdPendingMessages, atAgentSettled = false) {
887
978
  if (holdPendingMessages) {
888
979
  try {
889
980
  if (ctx?.hasPendingMessages?.() === true)
@@ -903,9 +994,17 @@ class CanvasInboxWatcher {
903
994
  for (const handoff of alreadyDurable)
904
995
  emitDeliveryCompleted(handoff);
905
996
  }
997
+ const agentSettledBlock = atAgentSettled
998
+ ? undefined
999
+ : this.pendingHandoffs
1000
+ .filter((handoff) => handoff.settleAtAgentSettled === true)
1001
+ .map((handoff) => physical.throughIndexes.get(handoff))
1002
+ .reduce((first, index) => first === undefined ? index : Math.min(first, index), undefined);
906
1003
  const eligible = this.pendingHandoffs.filter((handoff) => {
907
1004
  const throughIndex = physical.throughIndexes.get(handoff);
908
- return physical.blockedIndex === undefined || throughIndex < physical.blockedIndex;
1005
+ return (handoff.settleAtAgentSettled !== true || atAgentSettled)
1006
+ && (physical.blockedIndex === undefined || throughIndex < physical.blockedIndex)
1007
+ && (agentSettledBlock === undefined || throughIndex < agentSettledBlock);
909
1008
  });
910
1009
  if (eligible.length === 0)
911
1010
  return 'settled';
@@ -1006,6 +1105,11 @@ function installWatcher(pi, cadence) {
1006
1105
  currentWatcher?.dispose('reload', false);
1007
1106
  const watcher = new CanvasInboxWatcher(pi, cadence, ++nextGeneration);
1008
1107
  currentWatcher = watcher;
1108
+ // Pi loads this extension through Jiti while the broker driver is native ESM,
1109
+ // so their module-level singletons differ. Register the bound watcher on the
1110
+ // process-global key that the broker reads immediately before a human prompt.
1111
+ const promptJoin = (session) => watcher.prependHeldDeferredInbox(session);
1112
+ globalThis[HELD_DEFERRED_INBOX_PROMPT_JOIN] = promptJoin;
1009
1113
  try {
1010
1114
  watcher.start();
1011
1115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.250",
3
+ "version": "0.3.252",
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.250",
3
+ "version": "0.3.252",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.250",
9
+ "version": "0.3.252",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {