@crouton-kit/crouter 0.3.68 → 0.3.78
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/README.md +1 -1
- package/dist/builtin-memory/00-runtime-base.md +3 -10
- package/dist/builtin-memory/04-base-worker.md +18 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +639 -741
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +241 -58
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +365 -94
- package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
- package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
- package/dist/builtin-views/canvas/tui.mjs +8 -9
- package/dist/builtin-views/chat/tui.mjs +12 -12
- package/dist/builtin-views/git-pr/tui.mjs +7 -8
- package/dist/builtin-views/inbox/tui.mjs +27 -41
- package/dist/builtin-views/linkedin/tui.mjs +23 -37
- package/dist/builtin-views/prompt-review/tui.mjs +11 -11
- package/dist/builtin-views/settings/tui.mjs +11 -11
- package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -0
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -7
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
- package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/mermaid-render.test.js +20 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
- package/dist/clients/attach/attach-cmd.d.ts +1 -1
- package/dist/clients/attach/attach-cmd.js +733 -730
- package/dist/clients/attach/auth-pickers.d.ts +12 -0
- package/dist/clients/attach/auth-pickers.js +11 -0
- package/dist/clients/attach/canvas-panels.js +2 -3
- package/dist/clients/attach/chat-view.d.ts +7 -8
- package/dist/clients/attach/chat-view.js +119 -79
- package/dist/clients/attach/edit-diff-render.d.ts +6 -0
- package/dist/clients/attach/edit-diff-render.js +263 -0
- package/dist/clients/attach/input-controller.d.ts +28 -2
- package/dist/clients/attach/input-controller.js +38 -5
- package/dist/clients/attach/mermaid-render.js +5 -2
- package/dist/clients/attach/pickers.d.ts +8 -7
- package/dist/clients/attach/pickers.js +13 -17
- package/dist/clients/attach/slash-commands.d.ts +9 -0
- package/dist/clients/attach/slash-commands.js +128 -7
- package/dist/clients/attach/titled-editor.d.ts +12 -1
- package/dist/clients/attach/titled-editor.js +103 -8
- package/dist/commands/canvas-browse.js +2 -2
- package/dist/commands/memory/lint.js +39 -5
- package/dist/commands/memory/write.js +1 -0
- package/dist/commands/node.js +12 -5
- package/dist/commands/profile/new.js +30 -5
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
- package/dist/commands/sys/config.js +18 -21
- package/dist/commands/sys/setup-core.d.ts +12 -1
- package/dist/commands/sys/setup-core.js +25 -3
- package/dist/commands/sys/setup.js +166 -20
- package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
- package/dist/core/__tests__/fault-classifier.test.js +15 -0
- package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +9 -6
- package/dist/core/__tests__/full/cascade-close.test.js +5 -2
- package/dist/core/__tests__/full/dead-pane-regression.test.js +6 -3
- package/dist/core/__tests__/full/detach-focus.test.js +10 -5
- package/dist/core/__tests__/full/human-new-window-regression.test.js +6 -3
- package/dist/core/__tests__/full/review-render-pane-regression.test.js +2 -2
- package/dist/core/__tests__/helpers/harness.d.ts +1 -0
- package/dist/core/__tests__/helpers/harness.js +28 -4
- package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -4
- package/dist/core/__tests__/review-model-floor.test.js +1 -1
- package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
- package/dist/core/__tests__/stream-watchdog.test.js +70 -0
- package/dist/core/__tests__/tmux-surface.test.js +72 -0
- package/dist/core/canvas/__tests__/remote-event-stream.test.js +11 -9
- package/dist/core/canvas/__tests__/render-remote.test.js +7 -4
- package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +30 -13
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +164 -5
- package/dist/core/canvas/browse/model.d.ts +10 -6
- package/dist/core/canvas/browse/model.js +20 -12
- package/dist/core/canvas/browse/pins.d.ts +4 -0
- package/dist/core/canvas/browse/pins.js +29 -0
- package/dist/core/canvas/browse/render.d.ts +13 -1
- package/dist/core/canvas/browse/render.js +72 -15
- package/dist/core/canvas/nav-model.js +5 -11
- package/dist/core/canvas/paths.d.ts +5 -0
- package/dist/core/canvas/paths.js +7 -0
- package/dist/core/canvas/pid.d.ts +14 -10
- package/dist/core/canvas/pid.js +14 -10
- package/dist/core/config.d.ts +53 -2
- package/dist/core/config.js +228 -110
- package/dist/core/fault-classifier.js +1 -1
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +15 -1
- package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
- package/dist/core/keybindings/catalog.d.ts +14 -0
- package/dist/core/keybindings/catalog.js +256 -0
- package/dist/core/keybindings/index.d.ts +5 -0
- package/dist/core/keybindings/index.js +4 -0
- package/dist/core/keybindings/match.d.ts +29 -0
- package/dist/core/keybindings/match.js +99 -0
- package/dist/core/keybindings/persistence.d.ts +14 -0
- package/dist/core/keybindings/persistence.js +31 -0
- package/dist/core/keybindings/resolve.d.ts +14 -0
- package/dist/core/keybindings/resolve.js +305 -0
- package/dist/core/keybindings/types.d.ts +37 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/memory-resolver.d.ts +1 -1
- package/dist/core/memory-resolver.js +10 -3
- package/dist/core/predicate.d.ts +5 -3
- package/dist/core/predicate.js +5 -3
- package/dist/core/runtime/broker-protocol.d.ts +33 -5
- package/dist/core/runtime/broker.js +165 -14
- package/dist/core/runtime/launch.d.ts +39 -6
- package/dist/core/runtime/launch.js +79 -21
- package/dist/core/runtime/naming.js +3 -3
- package/dist/core/runtime/pi-cli.d.ts +6 -0
- package/dist/core/runtime/pi-cli.js +16 -2
- package/dist/core/runtime/placement.d.ts +2 -2
- package/dist/core/runtime/placement.js +4 -1
- package/dist/core/runtime/promote.js +4 -0
- package/dist/core/runtime/recap.d.ts +3 -3
- package/dist/core/runtime/recap.js +47 -44
- package/dist/core/runtime/recycle.js +6 -1
- package/dist/core/runtime/reset.js +5 -0
- package/dist/core/runtime/revive.js +28 -18
- package/dist/core/runtime/session-list-cache.d.ts +23 -0
- package/dist/core/runtime/session-list-cache.js +270 -0
- package/dist/core/runtime/spawn.js +51 -32
- package/dist/core/runtime/stream-watchdog.d.ts +26 -0
- package/dist/core/runtime/stream-watchdog.js +75 -0
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +2 -2
- package/dist/core/runtime/tmux.d.ts +18 -15
- package/dist/core/runtime/tmux.js +216 -110
- package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
- package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
- package/dist/core/tui/host.d.ts +30 -1
- package/dist/core/tui/host.js +148 -34
- package/dist/core/view/__tests__/transport-remote.test.js +9 -6
- package/dist/core/view/contract.d.ts +17 -3
- package/dist/daemon/crtrd-cli.d.ts +1 -1
- package/dist/daemon/crtrd-cli.js +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
- package/dist/pi-extensions/canvas-recap.d.ts +4 -0
- package/dist/pi-extensions/canvas-recap.js +38 -37
- package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
- package/dist/pi-extensions/canvas-stophook.js +33 -4
- package/dist/suppress-experimental-warnings.d.ts +1 -0
- package/dist/suppress-experimental-warnings.js +15 -0
- package/dist/types.d.ts +3 -13
- package/dist/types.js +12 -36
- package/dist/web-client/assets/{index-CnF5r8ky.js → index--md2ylfi.js} +18 -17
- package/dist/web-client/assets/index-BUdm9s9s.css +2 -0
- package/dist/web-client/index.html +3 -3
- package/dist/web-client/sw.js +1 -1
- package/package.json +11 -8
- package/scripts/postinstall.mjs +1 -1
- package/dist/pi-extensions/canvas-nav.d.ts +0 -43
- package/dist/pi-extensions/canvas-nav.js +0 -640
- package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
- package/dist/pi-extensions/widget-order-bus.js +0 -34
- package/dist/web-client/assets/index-BnmSLNLa.css +0 -2
|
@@ -25,6 +25,55 @@
|
|
|
25
25
|
// (kind 'final') ALSO steers — a completion the subscriber may be blocked on
|
|
26
26
|
// must interrupt the current turn, not wait behind it as a follow-up.
|
|
27
27
|
//
|
|
28
|
+
// Durable delivery — honest at-least-once with one conservative boundary:
|
|
29
|
+
// pi's sendUserMessage/sendMessage return `void`. Their loader dispatches an
|
|
30
|
+
// asynchronous runtime action and does NOT return the promise; a later
|
|
31
|
+
// delivery/prompt/queue/persist failure is caught and reported by the runtime,
|
|
32
|
+
// NOT thrown back here. So a send returning is a HANDOFF, not an acknowledgment
|
|
33
|
+
// — pi now owns the message, but has not necessarily accepted it.
|
|
34
|
+
//
|
|
35
|
+
// We do NOT try to correlate an exactly-once acknowledgment (every scheme —
|
|
36
|
+
// content-matching message_start, getEntries confirmation, per-send identity —
|
|
37
|
+
// adds worse defects than it closes: input-hook transforms break content
|
|
38
|
+
// matching, message_start precedes persistence, getEntries matches historical
|
|
39
|
+
// entries). Instead the model is deliberately at-least-once:
|
|
40
|
+
// • The read `cursor` advances at buffer time (so a burst is never re-read
|
|
41
|
+
// into the same watcher's buffer) — NOT the durability boundary.
|
|
42
|
+
// • `handedOffWatermark` advances when a send call RETURNS NORMALLY. A sync
|
|
43
|
+
// throw or a critical-abort re-buffer never advances it.
|
|
44
|
+
// • The durable cursor commits `handedOffWatermark` (clamped below anything
|
|
45
|
+
// still in `buffer`) ONLY at `agent_settled` — and at `session_shutdown` as
|
|
46
|
+
// the graceful safety net. NOT at agent_end (fires on failure/abort before
|
|
47
|
+
// pi decides on retry/continuation), NOT on the idle send return, NOT on
|
|
48
|
+
// message_start. One rule: an idle send starts a turn, and that turn's
|
|
49
|
+
// settle commits it.
|
|
50
|
+
// • Defensive hold at settle when pi still reports unconsumed sends. In the
|
|
51
|
+
// installed pi 0.80.6 `AgentSession`, retry/compaction/queued
|
|
52
|
+
// continuations are all drained WITHIN the same prompt cycle (the
|
|
53
|
+
// `_handlePostAgentRun`→`agent.continue()` loop) BEFORE the sole
|
|
54
|
+
// extension-facing `agent_settled` fires — a low-level failure/abort emits
|
|
55
|
+
// `agent_end` first, but any handed-off steer/followUp is consumed before
|
|
56
|
+
// that same cycle's settle. So the NORMAL production settle already sees
|
|
57
|
+
// `ctx.hasPendingMessages() === false`. The hold is therefore a
|
|
58
|
+
// conservative coarse guard, not a load-bearing part of a
|
|
59
|
+
// failure→pending-settle→later-settle sequence: IF a context ever reports
|
|
60
|
+
// pending sends at settle (steer+followUp queue length > 0), committing
|
|
61
|
+
// would durably consume a message pi has not yet delivered, so we DECLINE
|
|
62
|
+
// and let a later drained settle advance the cursor. It is a global
|
|
63
|
+
// boolean we never match to a specific send, and it clears on its own once
|
|
64
|
+
// pi drains, so it can never permanently stall the cursor.
|
|
65
|
+
//
|
|
66
|
+
// Accepted, deliberate residual windows (at-least-once by design):
|
|
67
|
+
// • A send whose promise pi later rejects INTERNALLY (swallowed by the
|
|
68
|
+
// runtime) never enters the queue, so hasPendingMessages() cannot see it
|
|
69
|
+
// and settle commits past it → rare silent loss. The alternative
|
|
70
|
+
// (correlation) demonstrably breaks the normal flow, so we accept this.
|
|
71
|
+
// • Crash/SIGKILL before settle → the fresh watcher re-reads from the last
|
|
72
|
+
// durable cursor and re-delivers (a duplicate coalesced pointer list, and
|
|
73
|
+
// an idempotent situational upsert re-read). Duplicate re-delivery, never
|
|
74
|
+
// silent loss.
|
|
75
|
+
// See `commitTarget` / `commitDurableCursor` / the agent_settled handler below.
|
|
76
|
+
//
|
|
28
77
|
// Double-notify prevention (copied from legacy watcher):
|
|
29
78
|
// A module-level `liveTimer` ensures that a /reload re-init clears the previous
|
|
30
79
|
// setInterval before starting a new one — exactly one watcher is live at a time.
|
|
@@ -91,6 +140,12 @@ export function registerCanvasInboxWatcher(pi) {
|
|
|
91
140
|
pi.on('agent_end', (_e, ctx) => {
|
|
92
141
|
captureCtx(_e, ctx);
|
|
93
142
|
streaming = false;
|
|
143
|
+
// agent_end is NOT a durable-commit point. It fires when a low-level run
|
|
144
|
+
// ends, but pi may still auto-retry, auto-compact, or run a queued
|
|
145
|
+
// continuation (it fires on the failure/abort path too, before pi decides).
|
|
146
|
+
// Committing here would durably consume a still-queued followUp that a
|
|
147
|
+
// failure agent_end preceded. Commit happens only at agent_settled (or the
|
|
148
|
+
// session_shutdown safety net), never here.
|
|
94
149
|
});
|
|
95
150
|
/**
|
|
96
151
|
* True when pi is not currently streaming a response.
|
|
@@ -108,21 +163,117 @@ export function registerCanvasInboxWatcher(pi) {
|
|
|
108
163
|
return !streaming;
|
|
109
164
|
};
|
|
110
165
|
// ---------------------------------------------------------------------------
|
|
111
|
-
// Debounce state
|
|
166
|
+
// Debounce + durable-cursor state
|
|
112
167
|
// ---------------------------------------------------------------------------
|
|
113
168
|
/** Entries received since the last flush — coalesced into one message. */
|
|
114
169
|
let buffer = [];
|
|
115
170
|
/** Epoch-ms of the most recent entry arrival. Used to detect burst-quiet. */
|
|
116
171
|
let lastArrival = 0;
|
|
117
172
|
/**
|
|
118
|
-
*
|
|
119
|
-
* Seeded from the persisted cursor
|
|
120
|
-
* "read from the beginning" (no prior cursor → process all existing
|
|
121
|
-
* NOT reset to `now` on first tick: that would silently drop
|
|
122
|
-
* arrived between node creation and watcher startup (the
|
|
173
|
+
* In-memory read watermark — ISO 8601 of the last entry we've consumed FROM
|
|
174
|
+
* THE FILE. Seeded from the persisted cursor on first resolution; undefined
|
|
175
|
+
* means "read from the beginning" (no prior cursor → process all existing
|
|
176
|
+
* entries). NOT reset to `now` on first tick: that would silently drop
|
|
177
|
+
* entries that arrived between node creation and watcher startup (the
|
|
178
|
+
* startup race).
|
|
179
|
+
*
|
|
180
|
+
* This advances at buffer time (tick(), before delivery) purely to stop the
|
|
181
|
+
* same entries being re-read into `buffer` on the next tick — it is NOT the
|
|
182
|
+
* durability boundary. See `handedOffWatermark` / `commitDurableCursor` for that.
|
|
123
183
|
*/
|
|
124
184
|
let cursor;
|
|
185
|
+
/**
|
|
186
|
+
* Cursor value actually persisted to `inbox.jsonl.cursor` on disk — the
|
|
187
|
+
* durability boundary a fresh watcher reseeds from after a crash/kick. It
|
|
188
|
+
* tracks work committed at a settle, never merely read.
|
|
189
|
+
*/
|
|
190
|
+
let durableCursor;
|
|
191
|
+
/**
|
|
192
|
+
* High-water ts of batches whose send call RETURNED NORMALLY — pi now owns
|
|
193
|
+
* them (a handoff, not an acknowledgment). It never advances on a sync send
|
|
194
|
+
* throw or a critical-abort re-buffer (those roll back to `buffer`), and it is
|
|
195
|
+
* never itself the durability boundary: it is only committed, clamped, at a
|
|
196
|
+
* settle. The read `cursor` races ahead of it (advancing at buffer time before
|
|
197
|
+
* any send), which is why `cursor` is never persisted.
|
|
198
|
+
*/
|
|
199
|
+
let handedOffWatermark;
|
|
125
200
|
let seeded = false;
|
|
201
|
+
/** Record a batch handoff — pi's send returned normally. Never moves back. */
|
|
202
|
+
const handOff = (maxTs) => {
|
|
203
|
+
if (handedOffWatermark === undefined || maxTs > handedOffWatermark)
|
|
204
|
+
handedOffWatermark = maxTs;
|
|
205
|
+
};
|
|
206
|
+
/** Max ts across a batch of entries (its high-water for advancement). */
|
|
207
|
+
const batchMaxTs = (entries) => entries.reduce((a, b) => (a.ts > b.ts ? a : b)).ts;
|
|
208
|
+
/**
|
|
209
|
+
* The ts safe to persist right now: the handed-off high-water, but NEVER at or
|
|
210
|
+
* past anything still in `buffer` (debounced, re-buffered after a sync send
|
|
211
|
+
* throw, or held for a critical-abort retry). When a buffered entry sits at or
|
|
212
|
+
* below the handed-off high-water — a rare out-of-order case — we decline to
|
|
213
|
+
* advance at all rather than risk committing past it. Conservative by design:
|
|
214
|
+
* at worst a fresh watcher re-delivers an already-handled entry, never loses
|
|
215
|
+
* one.
|
|
216
|
+
*/
|
|
217
|
+
const commitTarget = () => {
|
|
218
|
+
if (handedOffWatermark === undefined)
|
|
219
|
+
return undefined;
|
|
220
|
+
let earliest;
|
|
221
|
+
for (const e of buffer)
|
|
222
|
+
if (earliest === undefined || e.ts < earliest)
|
|
223
|
+
earliest = e.ts;
|
|
224
|
+
if (earliest === undefined)
|
|
225
|
+
return handedOffWatermark;
|
|
226
|
+
return handedOffWatermark < earliest ? handedOffWatermark : undefined;
|
|
227
|
+
};
|
|
228
|
+
/** Persist the commit watermark to disk if it has advanced past what's durable. */
|
|
229
|
+
const commitDurableCursor = (nodeId) => {
|
|
230
|
+
const target = commitTarget();
|
|
231
|
+
if (target === undefined)
|
|
232
|
+
return;
|
|
233
|
+
if (durableCursor === undefined || target > durableCursor) {
|
|
234
|
+
writeCursor(nodeId, target);
|
|
235
|
+
durableCursor = target;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const commitIfNode = () => {
|
|
239
|
+
const nodeId = process.env['CRTR_NODE_ID'];
|
|
240
|
+
if (nodeId !== undefined && nodeId.trim() !== '')
|
|
241
|
+
commitDurableCursor(nodeId);
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Commit at a settle boundary, with a defensive HOLD while pi still reports
|
|
245
|
+
* unconsumed sends. `ctx.hasPendingMessages()` is the steer+followUp queue
|
|
246
|
+
* length — pi's own coarse truth. In the installed pi 0.80.6 `AgentSession`
|
|
247
|
+
* retry/compaction/queued continuations drain within the same prompt cycle
|
|
248
|
+
* before the sole extension-facing settle, so the normal production settle
|
|
249
|
+
* already sees this false. The hold is a conservative guard: IF a context ever
|
|
250
|
+
* reports pending sends at settle, committing would durably consume an entry
|
|
251
|
+
* pi has not yet delivered, so we decline. It is a global boolean, never
|
|
252
|
+
* matched to a specific send, and it clears on its own once pi drains, so it
|
|
253
|
+
* can never permanently stall the cursor. When the API is absent (older pi) we
|
|
254
|
+
* fall back to the plain commit-at-settle baseline.
|
|
255
|
+
*/
|
|
256
|
+
const commitAtSettle = (ctx) => {
|
|
257
|
+
try {
|
|
258
|
+
if (ctx?.hasPendingMessages?.() === true)
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
/* can't tell — fall through to the commit-at-settle baseline */
|
|
263
|
+
}
|
|
264
|
+
commitIfNode();
|
|
265
|
+
};
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// agent_settled — the ONLY durable-commit boundary (session_shutdown is its
|
|
268
|
+
// graceful safety net). It fires once pi will not continue automatically (no
|
|
269
|
+
// retry, compaction, or queued continuation left). Commit the handed-off
|
|
270
|
+
// high-water, clamped below anything still buffered, and only when pi holds no
|
|
271
|
+
// unconsumed sends (hasPendingMessages()).
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
pi.on('agent_settled', (event, ctx) => {
|
|
274
|
+
captureCtx(event, ctx);
|
|
275
|
+
commitAtSettle(ctx);
|
|
276
|
+
});
|
|
126
277
|
// ---------------------------------------------------------------------------
|
|
127
278
|
// Flush: deliver the buffered entries as a single pi user message.
|
|
128
279
|
// ---------------------------------------------------------------------------
|
|
@@ -145,28 +296,34 @@ export function registerCanvasInboxWatcher(pi) {
|
|
|
145
296
|
// (context alone, no accompanying visible body) are dropped from the
|
|
146
297
|
// visible batch entirely — they exist purely as a wake marker for this
|
|
147
298
|
// delivery and must never surface a line in the digest.
|
|
148
|
-
const
|
|
299
|
+
const situationalEntries = batch.filter((e) => e.data?.['situational'] === true);
|
|
149
300
|
const visibleBatch = batch.filter((e) => e.data?.['situationalOnly'] !== true);
|
|
150
|
-
if (
|
|
301
|
+
if (situationalEntries.length > 0) {
|
|
151
302
|
const nodeId = process.env['CRTR_NODE_ID'];
|
|
152
303
|
const text = nodeId !== undefined ? readSituationalContext(nodeId) : null;
|
|
153
304
|
if (text !== null) {
|
|
305
|
+
const situationalContent = `<situational-context>\n${text}\n</situational-context>`;
|
|
154
306
|
try {
|
|
155
307
|
pi.sendMessage({
|
|
156
308
|
customType: SITUATIONAL_CONTEXT_CUSTOM_TYPE,
|
|
157
|
-
content:
|
|
309
|
+
content: situationalContent,
|
|
158
310
|
display: true,
|
|
159
311
|
details: { nodeId },
|
|
160
312
|
}, { deliverAs: 'followUp', triggerTurn: true });
|
|
313
|
+
// Handed off — pi's send returned normally. It commits at the next
|
|
314
|
+
// agent_settled (a custom send that async-rejected is the accepted
|
|
315
|
+
// silent-loss window; hasPendingMessages() covers only user steer/
|
|
316
|
+
// followUp, so a situational miss is not held).
|
|
317
|
+
handOff(batchMaxTs(situationalEntries));
|
|
161
318
|
}
|
|
162
319
|
catch {
|
|
163
|
-
// Re-queue on
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
// the situational-only entries go back;
|
|
168
|
-
// CURRENT sidecar value (upsert, not
|
|
169
|
-
// even if the text changed again
|
|
320
|
+
// Re-queue on a SYNCHRONOUS send failure (a stale-extension assertion
|
|
321
|
+
// — an async delivery failure is swallowed by the runtime, not thrown
|
|
322
|
+
// here). The read cursor already advanced past these entries in
|
|
323
|
+
// tick(), so a swallowed failure with no re-queue would drop the
|
|
324
|
+
// hidden wake permanently. Only the situational-only entries go back;
|
|
325
|
+
// a real re-send re-reads the CURRENT sidecar value (upsert, not
|
|
326
|
+
// append), so retrying is idempotent even if the text changed again.
|
|
170
327
|
buffer = batch.filter((e) => e.data?.['situationalOnly'] === true).concat(buffer);
|
|
171
328
|
}
|
|
172
329
|
}
|
|
@@ -184,8 +341,11 @@ export function registerCanvasInboxWatcher(pi) {
|
|
|
184
341
|
const steerMidStream = anyCritical || visibleBatch.some((e) => e.tier === 'urgent' || e.kind === 'final');
|
|
185
342
|
try {
|
|
186
343
|
if (isIdle()) {
|
|
187
|
-
// Idle → trigger a new turn immediately (sendUserMessage always
|
|
344
|
+
// Idle → trigger a new turn immediately (sendUserMessage always
|
|
345
|
+
// triggers). The send returns void (a handoff, not an ack), so we do NOT
|
|
346
|
+
// commit here — the turn this starts commits at its agent_settled.
|
|
188
347
|
pi.sendUserMessage(digest);
|
|
348
|
+
handOff(batchMaxTs(visibleBatch));
|
|
189
349
|
}
|
|
190
350
|
else if (anyCritical) {
|
|
191
351
|
// Critical mid-stream → TRUE preempt. ctx.abort() cancels the live LLM
|
|
@@ -194,7 +354,9 @@ export function registerCanvasInboxWatcher(pi) {
|
|
|
194
354
|
// path — by then the turn has torn down and sendUserMessage starts a fresh
|
|
195
355
|
// turn. Relying on the proven idle path (not steer-after-abort semantics)
|
|
196
356
|
// keeps this robust; if abort hasn't settled by the next tick we simply
|
|
197
|
-
// abort again and retry — idempotent and self-healing.
|
|
357
|
+
// abort again and retry — idempotent and self-healing. Nothing is handed
|
|
358
|
+
// off here (handedOffWatermark stays put) and the entry stays buffered,
|
|
359
|
+
// so the abort-generated agent_end/settle cannot commit past it.
|
|
198
360
|
try {
|
|
199
361
|
lastCtx?.abort?.();
|
|
200
362
|
}
|
|
@@ -203,13 +365,19 @@ export function registerCanvasInboxWatcher(pi) {
|
|
|
203
365
|
}
|
|
204
366
|
else {
|
|
205
367
|
// Mid-stream → steer on urgency or a finished node, else enqueue for the
|
|
206
|
-
// turn after this one.
|
|
368
|
+
// turn after this one. Handed to pi's in-memory queue now (returns
|
|
369
|
+
// normally = handoff). It commits at the turn's agent_settled — and if
|
|
370
|
+
// the turn fails/aborts before pi consumes it, hasPendingMessages() holds
|
|
371
|
+
// the commit until a later settle drains it.
|
|
207
372
|
pi.sendUserMessage(digest, { deliverAs: steerMidStream ? 'steer' : 'followUp' });
|
|
373
|
+
handOff(batchMaxTs(visibleBatch));
|
|
208
374
|
}
|
|
209
375
|
}
|
|
210
376
|
catch {
|
|
211
|
-
// Re-queue on delivery failure so a transient error doesn't
|
|
212
|
-
// inbox entries.
|
|
377
|
+
// Re-queue on a SYNCHRONOUS delivery failure so a transient error doesn't
|
|
378
|
+
// silently drop inbox entries. handedOffWatermark was not advanced and the
|
|
379
|
+
// entries are back in `buffer`, so no settle can commit past them. They
|
|
380
|
+
// will be retried on the next flush.
|
|
213
381
|
buffer = visibleBatch.concat(buffer);
|
|
214
382
|
}
|
|
215
383
|
};
|
|
@@ -230,6 +398,7 @@ export function registerCanvasInboxWatcher(pi) {
|
|
|
230
398
|
// with undefined returns ALL entries (no truncation to `now`).
|
|
231
399
|
if (!seeded) {
|
|
232
400
|
cursor = readCursor(nodeId);
|
|
401
|
+
durableCursor = cursor;
|
|
233
402
|
seeded = true;
|
|
234
403
|
}
|
|
235
404
|
const newEntries = readInboxSince(nodeId, cursor);
|
|
@@ -244,12 +413,12 @@ export function registerCanvasInboxWatcher(pi) {
|
|
|
244
413
|
return;
|
|
245
414
|
}
|
|
246
415
|
if (newEntries.length > 0) {
|
|
247
|
-
// Advance
|
|
248
|
-
//
|
|
249
|
-
//
|
|
416
|
+
// Advance the in-memory read watermark BEFORE buffering, so a re-tick
|
|
417
|
+
// during the debounce window never re-reads the same entries into
|
|
418
|
+
// `buffer`. This does NOT persist to disk — the durable cursor commits
|
|
419
|
+
// the handed-off high-water only at agent_settled (see commitDurableCursor).
|
|
250
420
|
const latest = newEntries.reduce((a, b) => (a.ts > b.ts ? a : b));
|
|
251
421
|
cursor = latest.ts;
|
|
252
|
-
writeCursor(nodeId, cursor);
|
|
253
422
|
buffer.push(...newEntries);
|
|
254
423
|
lastArrival = Date.now();
|
|
255
424
|
}
|
|
@@ -277,6 +446,15 @@ export function registerCanvasInboxWatcher(pi) {
|
|
|
277
446
|
// pi DOES fire session_shutdown — use it as the authoritative teardown so a
|
|
278
447
|
// re-init (e.g. /reload) never discovers a live sibling timer.
|
|
279
448
|
pi.on('session_shutdown', () => {
|
|
449
|
+
// Safety net on the graceful-teardown path: a last chance to persist the
|
|
450
|
+
// handed-off high-water. Same settle gate — commitTarget clamps below
|
|
451
|
+
// anything still buffered, and we HOLD if pi still holds unconsumed sends
|
|
452
|
+
// (they die with the process on shutdown, so a fresh watcher must re-read
|
|
453
|
+
// them). This does NOT cover the abrupt-kill path: the broker's SIGTERM
|
|
454
|
+
// handler runs disposeAndExit (session.dispose() + process.exit) without
|
|
455
|
+
// emitting this extension's session_shutdown, so this never fires on a kill
|
|
456
|
+
// — exactly why the durable cursor only ever advances at a settle boundary.
|
|
457
|
+
commitAtSettle(lastCtx);
|
|
280
458
|
clearInterval(timer);
|
|
281
459
|
if (liveTimer === timer)
|
|
282
460
|
liveTimer = undefined;
|
|
@@ -22,6 +22,10 @@ interface ExtensionCtxLike {
|
|
|
22
22
|
ui?: UIContextLike;
|
|
23
23
|
mode?: string;
|
|
24
24
|
sessionManager?: SessionManagerLike;
|
|
25
|
+
model?: {
|
|
26
|
+
provider?: string;
|
|
27
|
+
id?: string;
|
|
28
|
+
};
|
|
25
29
|
}
|
|
26
30
|
type PiEvents = 'session_start' | 'input' | 'turn_end' | 'session_shutdown';
|
|
27
31
|
interface PiLike {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// canvas-recap.ts — pi extension for pi-native canvas agent nodes.
|
|
2
2
|
//
|
|
3
|
-
// A per-node INACTIVITY
|
|
4
|
-
// When a node has had NO new message (user or assistant) for IDLE_MS (
|
|
5
|
-
// default), it runs
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// clock
|
|
3
|
+
// A per-node INACTIVITY BRIEF, drawn faintly immediately above the editor border.
|
|
4
|
+
// When a node has had NO new message (user or assistant) for IDLE_MS (60s by
|
|
5
|
+
// default), it runs a stripped-down headless call on the conversation's current
|
|
6
|
+
// working model and pins one concise, wrapped briefing above the editor, so a
|
|
7
|
+
// developer returning to the conversation sees where it left off at a glance.
|
|
8
|
+
// The card disappears the instant the next message arrives, and the idle clock
|
|
9
|
+
// restarts.
|
|
9
10
|
//
|
|
10
11
|
// trigger/clear are keyed off MESSAGES, not turns-of-work:
|
|
11
12
|
// • a user `input` event OR an assistant `turn_end` resets the idle clock
|
|
@@ -13,15 +14,12 @@
|
|
|
13
14
|
// • a single low-rate timer (no busy-loop) checks elapsed-since-last-message
|
|
14
15
|
// and, on crossing IDLE_MS, generates + shows the recap once.
|
|
15
16
|
//
|
|
16
|
-
//
|
|
17
|
+
// Brief input = ALL user + assistant LITERAL text, most-recent within a char
|
|
17
18
|
// budget. Tool calls, tool results, thinking blocks, and extension-injected
|
|
18
19
|
// custom messages are excluded (only `message` entries with role user/assistant,
|
|
19
20
|
// and only their `text` content blocks).
|
|
20
21
|
//
|
|
21
|
-
// Placement: key `crtr-recap`, placement `aboveEditor
|
|
22
|
-
// `crtr-managers` via the widget-order bus (pi's widget store is insertion-
|
|
23
|
-
// ordered and re-setting moves a key to the bottom; after we paint the recap
|
|
24
|
-
// we ask canvas-nav to re-assert its manager line, which drops it below us).
|
|
22
|
+
// Placement: key `crtr-recap`, placement `aboveEditor`.
|
|
25
23
|
//
|
|
26
24
|
// Surface gating: shows in BOTH the in-pane pi TUI (mode 'tui') and the
|
|
27
25
|
// headless broker → attach/web viewers (mode 'print', setWidget broadcasts to
|
|
@@ -35,21 +33,19 @@
|
|
|
35
33
|
// Plain TS-with-types — no imports from @earendil-works/* so this compiles
|
|
36
34
|
// inside crouter's own tsc build without a dep on the pi packages.
|
|
37
35
|
import { generateRecap } from '../core/runtime/recap.js';
|
|
38
|
-
import { requestNavRerender } from './widget-order-bus.js';
|
|
39
36
|
// ---------------------------------------------------------------------------
|
|
40
37
|
// Tuning constants
|
|
41
38
|
// ---------------------------------------------------------------------------
|
|
42
|
-
/** No-new-message window before a recap is shown.
|
|
39
|
+
/** No-new-message window before a recap is shown. 60s by default; override with
|
|
43
40
|
* CRTR_RECAP_IDLE_MS (for fast runtime verification). */
|
|
44
41
|
function idleMs() {
|
|
45
42
|
const raw = process.env['CRTR_RECAP_IDLE_MS'];
|
|
46
43
|
const n = raw !== undefined ? parseInt(raw, 10) : NaN;
|
|
47
|
-
return Number.isFinite(n) && n > 0 ? n :
|
|
44
|
+
return Number.isFinite(n) && n > 0 ? n : 60_000;
|
|
48
45
|
}
|
|
49
|
-
/** How often the
|
|
50
|
-
* Mirrors canvas-nav's ASK_POLL_MS — cheap and tmux/crtr-free. */
|
|
46
|
+
/** How often the single low-rate timer checks elapsed-since-last-message. */
|
|
51
47
|
const POLL_MS = 7_000;
|
|
52
|
-
/** Char budget on the conversation fed to
|
|
48
|
+
/** Char budget on the conversation fed to the brief model — it only needs the gist,
|
|
53
49
|
* so we keep the MOST-RECENT text within this cap (a recap is about where we
|
|
54
50
|
* left off, not the whole history). */
|
|
55
51
|
const CONVO_CHAR_BUDGET = 12_000;
|
|
@@ -124,12 +120,22 @@ export function registerCanvasRecap(pi) {
|
|
|
124
120
|
// Captured on session_start; used by the timer and the message handlers.
|
|
125
121
|
let ui;
|
|
126
122
|
let sessionManager;
|
|
123
|
+
/** Exact provider/model that most recently ran this conversation successfully.
|
|
124
|
+
* The recap subprocess prefers it over a hard-pinned provider that may be in
|
|
125
|
+
* cooldown (the failure mode that made the old recap silently disappear). */
|
|
126
|
+
let liveModel;
|
|
127
|
+
const captureLiveModel = (ctx) => {
|
|
128
|
+
const provider = ctx.model?.provider?.trim();
|
|
129
|
+
const id = ctx.model?.id?.trim();
|
|
130
|
+
if (provider && id)
|
|
131
|
+
liveModel = `${provider}/${id}`;
|
|
132
|
+
};
|
|
127
133
|
/** Wall-clock of the most recent user/assistant message. The idle clock. */
|
|
128
134
|
let lastMessageAt = Date.now();
|
|
129
135
|
/** True while a recap is currently shown (so we generate at most once per
|
|
130
136
|
* idle window, and know whether a clear is needed on the next message). */
|
|
131
137
|
let recapShown = false;
|
|
132
|
-
/** True while a
|
|
138
|
+
/** True while a briefing generation is in flight (so we don't fire a second). */
|
|
133
139
|
let generating = false;
|
|
134
140
|
const canPaint = () => ui !== undefined && typeof ui.setWidget === 'function';
|
|
135
141
|
const clearRecap = () => {
|
|
@@ -140,21 +146,14 @@ export function registerCanvasRecap(pi) {
|
|
|
140
146
|
}
|
|
141
147
|
catch { /* best-effort */ }
|
|
142
148
|
};
|
|
143
|
-
/** Render the
|
|
144
|
-
const showRecap = (
|
|
145
|
-
if (!canPaint())
|
|
146
|
-
return;
|
|
147
|
-
const labels = ['◷ goal', '· now ', '→ next'];
|
|
148
|
-
const lines = fragments.slice(0, 3).map((f, i) => `${labels[i] ?? ' '} ${f}`);
|
|
149
|
-
if (lines.length === 0)
|
|
149
|
+
/** Render the briefing into the viewer's low-contrast above-editor slot. */
|
|
150
|
+
const showRecap = (briefing) => {
|
|
151
|
+
if (!canPaint() || briefing === '')
|
|
150
152
|
return;
|
|
151
153
|
try {
|
|
152
|
-
ui.setWidget(WIDGET_KEY,
|
|
154
|
+
ui.setWidget(WIDGET_KEY, [`Brief: ${briefing}`], { placement: 'aboveEditor' });
|
|
153
155
|
}
|
|
154
156
|
catch { /* best-effort */ }
|
|
155
|
-
// Re-assert the manager line BELOW us (pi's widget store is insertion-
|
|
156
|
-
// ordered; re-setting crtr-managers drops it under crtr-recap).
|
|
157
|
-
requestNavRerender();
|
|
158
157
|
};
|
|
159
158
|
/** A user or assistant message landed: reset the idle clock and clear any
|
|
160
159
|
* shown recap (the conversation has moved on). */
|
|
@@ -163,7 +162,6 @@ export function registerCanvasRecap(pi) {
|
|
|
163
162
|
if (recapShown) {
|
|
164
163
|
recapShown = false;
|
|
165
164
|
clearRecap();
|
|
166
|
-
requestNavRerender();
|
|
167
165
|
}
|
|
168
166
|
};
|
|
169
167
|
// -------------------------------------------------------------------------
|
|
@@ -172,6 +170,7 @@ export function registerCanvasRecap(pi) {
|
|
|
172
170
|
pi.on('session_start', (_event, ctx) => {
|
|
173
171
|
ui = ctx.ui;
|
|
174
172
|
sessionManager = ctx.sessionManager;
|
|
173
|
+
captureLiveModel(ctx);
|
|
175
174
|
// Fresh session / hot-swap: start the idle clock now and drop any stale
|
|
176
175
|
// recap that bled through a /reload.
|
|
177
176
|
lastMessageAt = Date.now();
|
|
@@ -183,12 +182,14 @@ export function registerCanvasRecap(pi) {
|
|
|
183
182
|
// Every user message (any source) is activity — reset + clear.
|
|
184
183
|
onMessage();
|
|
185
184
|
});
|
|
186
|
-
pi.on('turn_end', (_event,
|
|
187
|
-
// A completed assistant turn is activity — reset + clear
|
|
185
|
+
pi.on('turn_end', (_event, ctx) => {
|
|
186
|
+
// A completed assistant turn is activity — reset + clear, and remember the
|
|
187
|
+
// exact model/provider that successfully produced it for the later recap.
|
|
188
|
+
captureLiveModel(ctx);
|
|
188
189
|
onMessage();
|
|
189
190
|
});
|
|
190
191
|
// -------------------------------------------------------------------------
|
|
191
|
-
// The single low-rate idle poll
|
|
192
|
+
// The single low-rate idle poll.
|
|
192
193
|
// -------------------------------------------------------------------------
|
|
193
194
|
if (liveTimer !== undefined)
|
|
194
195
|
clearInterval(liveTimer);
|
|
@@ -203,19 +204,19 @@ export function registerCanvasRecap(pi) {
|
|
|
203
204
|
const convo = buildConversation(sessionManager);
|
|
204
205
|
if (convo === '')
|
|
205
206
|
return; // nothing to summarize yet
|
|
206
|
-
// Snapshot the clock: if a message arrives while
|
|
207
|
+
// Snapshot the clock: if a message arrives while generation runs, discard the
|
|
207
208
|
// (now-stale) result instead of painting over the live conversation.
|
|
208
209
|
const startedAt = lastMessageAt;
|
|
209
210
|
generating = true;
|
|
210
|
-
generateRecap(convo, (
|
|
211
|
+
generateRecap(convo, (briefing) => {
|
|
211
212
|
generating = false;
|
|
212
213
|
if (lastMessageAt !== startedAt)
|
|
213
214
|
return; // a message landed mid-flight
|
|
214
215
|
if (!canPaint())
|
|
215
216
|
return;
|
|
216
217
|
recapShown = true;
|
|
217
|
-
showRecap(
|
|
218
|
-
});
|
|
218
|
+
showRecap(briefing);
|
|
219
|
+
}, liveModel);
|
|
219
220
|
// generateRecap is silent on failure (never calls back), so clear the
|
|
220
221
|
// in-flight flag after the timeout window even if no callback fires.
|
|
221
222
|
setTimeout(() => { generating = false; }, 30_000).unref?.();
|
|
@@ -4,6 +4,17 @@ interface PiLike {
|
|
|
4
4
|
sendUserMessage: (content: string, options?: {
|
|
5
5
|
deliverAs?: 'steer' | 'followUp';
|
|
6
6
|
}) => void;
|
|
7
|
+
/** Custom-message injection — used for the 'nextTurn' hold (queued silently,
|
|
8
|
+
* injected alongside the NEXT user message, never triggers a turn). */
|
|
9
|
+
sendMessage: (message: {
|
|
10
|
+
customType: string;
|
|
11
|
+
content: string;
|
|
12
|
+
display?: boolean;
|
|
13
|
+
details?: Record<string, unknown>;
|
|
14
|
+
}, options?: {
|
|
15
|
+
deliverAs?: 'steer' | 'followUp' | 'nextTurn';
|
|
16
|
+
triggerTurn?: boolean;
|
|
17
|
+
}) => void;
|
|
7
18
|
}
|
|
8
19
|
/** The nudge text for a crossed band, specialized to the node's (mode,
|
|
9
20
|
* lifecycle) persona + how far along the escalation it is. `crtr node yield` is
|
|
@@ -481,12 +481,35 @@ export function registerCanvasStophook(pi) {
|
|
|
481
481
|
flushTelemetry(jobDirPath, totalIn, totalOut, model, contextTokens, lastActivity);
|
|
482
482
|
touchBusy(nodeId); // wedge heartbeat: a turn ending is a sign of life
|
|
483
483
|
clearFault(nodeId, { link: 'daemon→node' }); // reaching turn_end proves this turn wasn't wedged
|
|
484
|
+
clearFault(nodeId, { link: 'pi→provider' }); // a completed provider round-trip proves this episode is over
|
|
485
|
+
// Will the agent run CONTINUE past this turn? Deterministic at turn_end:
|
|
486
|
+
// the loop keeps going exactly when this assistant message carried tool
|
|
487
|
+
// calls and the run wasn't aborted/errored (those end unconditionally,
|
|
488
|
+
// before tools execute). A tool-calling turn ends with stopReason
|
|
489
|
+
// 'toolUse' — or 'length' when truncation failed the calls, which still
|
|
490
|
+
// continues the loop — NEVER 'stop', so gate on the abort/error reasons,
|
|
491
|
+
// not on an allowlist that excludes 'toolUse'.
|
|
492
|
+
// This handler runs BEFORE the loop drains the steering queue, so the read
|
|
493
|
+
// is race-free: zero tool calls ⇒ this is the run's FINAL message, and a
|
|
494
|
+
// steer sent now would not redirect at a boundary — it would EXTEND the run
|
|
495
|
+
// past the agent's closing reply, burying the summary the human comes back
|
|
496
|
+
// to read once the conversation goes quiet.
|
|
497
|
+
const turnContent = Array.isArray(event?.message?.content) ? event.message.content : [];
|
|
498
|
+
const turnStopReason = event?.message?.stopReason ?? '';
|
|
499
|
+
const runContinues = turnStopReason !== 'error' &&
|
|
500
|
+
turnStopReason !== 'aborted' &&
|
|
501
|
+
turnContent.some((c) => c?.type === 'toolCall');
|
|
484
502
|
// Context-size steering: fire the current band once, with lifecycle-specific
|
|
485
503
|
// guidance (lifecycle is read live — a terminal worker may have promoted to
|
|
486
504
|
// resident since launch).
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
505
|
+
// Delivery splits on runContinues:
|
|
506
|
+
// • mid-run → STEER, not a followUp: guidance to become an orchestrator /
|
|
507
|
+
// delegate / yield must redirect the node at the turn boundary, not queue
|
|
508
|
+
// behind whatever it does next (where it rides along, easy to ignore).
|
|
509
|
+
// • final message → HOLD via pi's 'nextTurn' queue: it injects alongside
|
|
510
|
+
// the next user message without triggering a turn, so the nudge is read
|
|
511
|
+
// at the start of the next engagement instead of spawning a phantom
|
|
512
|
+
// continuation after the agent already said its piece.
|
|
490
513
|
// Never the cumulative totalIn: under prompt caching that never grows (input
|
|
491
514
|
// is a ~2-token uncached delta each turn), so the bands were unreachable and
|
|
492
515
|
// the nudge never fired.
|
|
@@ -497,7 +520,13 @@ export function registerCanvasStophook(pi) {
|
|
|
497
520
|
const at = contextTokens !== null ? steerBand(contextTokens) : null;
|
|
498
521
|
if (at !== null && !firedBands.has(at)) {
|
|
499
522
|
firedBands.add(at);
|
|
500
|
-
|
|
523
|
+
const note = `[crtr] ${steerNote(at, lifecycle, mode)}`;
|
|
524
|
+
if (runContinues) {
|
|
525
|
+
pi.sendUserMessage(note, { deliverAs: 'steer' });
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
pi.sendMessage({ customType: 'crtr-context-nudge', content: note, display: true }, { deliverAs: 'nextTurn' });
|
|
529
|
+
}
|
|
501
530
|
}
|
|
502
531
|
}
|
|
503
532
|
catch {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const originalEmit = process.emit;
|
|
2
|
+
process.emit = function (name, ...args) {
|
|
3
|
+
const [data] = args;
|
|
4
|
+
if (name === 'warning' &&
|
|
5
|
+
data instanceof Error &&
|
|
6
|
+
data.name === 'ExperimentalWarning' &&
|
|
7
|
+
data.message.includes('SQLite')) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
return originalEmit.apply(this, [
|
|
11
|
+
name,
|
|
12
|
+
...args,
|
|
13
|
+
]);
|
|
14
|
+
};
|
|
15
|
+
export {};
|