@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.
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +1 -5
- package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -1
- package/dist/clients/attach/__tests__/completion-frecency.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/completion-frecency.test.js +35 -0
- package/dist/clients/attach/__tests__/ref-autocomplete.test.js +3 -1
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +2 -1
- package/dist/clients/attach/input/completion-frecency.d.ts +45 -0
- package/dist/clients/attach/input/completion-frecency.js +141 -0
- package/dist/clients/attach/input/controller.d.ts +17 -0
- package/dist/clients/attach/input/controller.js +40 -0
- package/dist/clients/attach/input/ref-autocomplete.d.ts +3 -1
- package/dist/clients/attach/input/ref-autocomplete.js +49 -8
- package/dist/clients/attach/input/titled-editor.d.ts +3 -0
- package/dist/clients/attach/input/titled-editor.js +5 -0
- package/dist/clients/attach/session/editor-inventory.d.ts +3 -0
- package/dist/clients/attach/session/editor-inventory.js +1 -1
- package/dist/clients/attach/session/input-wiring.d.ts +3 -0
- package/dist/clients/attach/session/input-wiring.js +2 -0
- package/dist/clients/attach/viewer.js +578 -578
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +98 -1
- package/dist/core/__tests__/canvas-inbox-watcher-naming.test.js +4 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +4 -1
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +3 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +16 -3
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +4 -1
- package/dist/core/__tests__/migration.test.js +57 -11
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +17 -2
- package/dist/core/__tests__/seam/dormancy-release.test.js +1 -0
- package/dist/core/__tests__/seam/held-deferred-human-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +63 -0
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +9 -2
- package/dist/core/canvas/migrations.js +63 -24
- package/dist/core/runtime/broker/engine-drive.js +20 -3
- package/dist/core/runtime/broker/fault-retry.js +31 -13
- package/dist/core/runtime/broker/held-deferred-inbox.d.ts +13 -0
- package/dist/core/runtime/broker/held-deferred-inbox.js +13 -0
- package/dist/core/runtime/broker.js +19 -0
- package/dist/core/runtime/close.js +2 -2
- package/dist/core/runtime/fault.js +8 -5
- package/dist/core/runtime/recycle.js +63 -41
- package/dist/core/runtime/reset.d.ts +1 -1
- package/dist/core/runtime/reset.js +25 -2
- package/dist/daemon/__tests__/helpers/source-daemon.js +1 -0
- package/dist/daemon/api/handlers/messages.js +1 -1
- package/dist/daemon/api/handlers/reports.js +1 -1
- package/dist/daemon/cron/sinks.js +1 -1
- package/dist/daemon/messaging/node-message.js +1 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +109 -5
- package/package.json +1 -1
- 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
|
-
|
|
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
|
|
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
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
9
|
+
"version": "0.3.252",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|