@crouton-kit/crouter 0.3.82 → 0.3.83
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-pi-packages/pi-mode-switch/README.md +3 -3
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +12 -4
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -3
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +104 -0
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +16 -44
- package/dist/clients/attach/attach-cmd.d.ts +1 -2
- package/dist/clients/attach/attach-cmd.js +685 -685
- package/dist/clients/attach/attach-def.d.ts +2 -0
- package/dist/clients/attach/attach-def.js +93 -0
- package/dist/clients/attach/canvas-panels.d.ts +10 -3
- package/dist/clients/attach/canvas-panels.js +42 -16
- package/dist/clients/attach/chat-view.d.ts +20 -1
- package/dist/clients/attach/chat-view.js +85 -2
- package/dist/commands/__tests__/human.test.js +7 -22
- package/dist/commands/attention.js +31 -30
- package/dist/commands/human/prompts.js +51 -38
- package/dist/commands/human/queue.d.ts +1 -28
- package/dist/commands/human/queue.js +156 -520
- package/dist/commands/human/shared.d.ts +28 -68
- package/dist/commands/human/shared.js +37 -120
- package/dist/commands/human.js +13 -17
- package/dist/commands/node.js +4 -2
- package/dist/commands/surface.js +1 -1
- package/dist/commands/sys/__tests__/setup-core.test.js +1 -1
- package/dist/commands/sys/__tests__/setup-front-door.test.js +98 -0
- package/dist/commands/sys/setup-core.d.ts +22 -0
- package/dist/commands/sys/setup-core.js +62 -0
- package/dist/commands/sys/setup.js +8 -1
- package/dist/core/__tests__/human-deliver-e2e.test.js +95 -0
- package/dist/core/__tests__/human-deliver.test.js +332 -0
- package/dist/core/__tests__/human-node-not-supervised.test.js +5 -4
- package/dist/core/__tests__/pid-identity-match.test.js +83 -0
- package/dist/core/canvas/__tests__/attention.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/attention.test.js +126 -0
- package/dist/core/canvas/__tests__/remote-canvas-source.test.js +4 -4
- package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
- package/dist/core/canvas/attention.d.ts +16 -14
- package/dist/core/canvas/attention.js +34 -29
- package/dist/core/canvas/boot.d.ts +1 -0
- package/dist/core/canvas/boot.js +1 -1
- package/dist/core/canvas/browse/app.js +3 -3
- package/dist/core/canvas/pid.d.ts +43 -17
- package/dist/core/canvas/pid.js +104 -35
- package/dist/core/canvas/remote-canvas-source.d.ts +1 -1
- package/dist/core/canvas/remote-canvas-source.js +4 -2
- package/dist/core/canvas/render.d.ts +5 -5
- package/dist/core/canvas/render.js +14 -14
- package/dist/core/canvas/source.d.ts +2 -2
- package/dist/core/canvas/source.js +3 -3
- package/dist/core/keybindings/__tests__/resolve.test.js +1 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +3 -2
- package/dist/core/preview-registry.js +22 -29
- package/dist/core/profiles/select.js +4 -4
- package/dist/core/runtime/pi-vendored.js +15 -1
- package/dist/core/runtime/placement.d.ts +1 -10
- package/dist/core/runtime/placement.js +43 -26
- package/dist/core/runtime/session-list-cache.d.ts +22 -2
- package/dist/core/runtime/session-list-cache.js +92 -24
- package/dist/core/runtime/spawn.js +9 -0
- package/dist/core/spawn.d.ts +0 -59
- package/dist/core/spawn.js +5 -163
- package/dist/daemon/crtrd.js +7 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/web-client/assets/index-DZdKS-FB.js +78 -0
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +2 -2
- package/dist/core/__tests__/full/dead-pane-regression.test.js +0 -154
- package/dist/core/__tests__/full/human-new-window-regression.test.js +0 -104
- package/dist/core/__tests__/full/review-render-pane-regression.test.js +0 -133
- package/dist/core/__tests__/human-stranded-deliver.test.js +0 -145
- package/dist/core/__tests__/human-surface-target.test.js +0 -98
- package/dist/web-client/assets/index--md2ylfi.js +0 -78
- /package/dist/{core/__tests__/full/dead-pane-regression.test.d.ts → clients/attach/__tests__/chat-view-snapshot-ordering.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/human-new-window-regression.test.d.ts → commands/sys/__tests__/setup-front-door.test.d.ts} +0 -0
- /package/dist/core/__tests__/{full/review-render-pane-regression.test.d.ts → human-deliver-e2e.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-stranded-deliver.test.d.ts → human-deliver.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-surface-target.test.d.ts → pid-identity-match.test.d.ts} +0 -0
|
@@ -12,16 +12,6 @@ export declare function focusByPane(pane: string): FocusRow | null;
|
|
|
12
12
|
export declare function focusedNodes(): Set<string>;
|
|
13
13
|
/** Every live viewer row (GC'ing any whose pane has gone). */
|
|
14
14
|
export declare function listFocuses(): FocusRow[];
|
|
15
|
-
/** The on-screen viewer a human-in-the-loop prompt raised by `nodeId` should
|
|
16
|
-
* surface into: the HIGHEST node of nodeId's graph that has a viewer — the
|
|
17
|
-
* viewer closest to the graph root, i.e. the session/window the user is actually
|
|
18
|
-
* watching this work in. Walks nodeId's spine to its root, enumerates the whole
|
|
19
|
-
* tree root-first (`view` is BFS ⇒ shallowest first), and returns the viewer row
|
|
20
|
-
* of the first node that has one. null when nothing in the graph is on screen —
|
|
21
|
-
* the caller then surfaces in the user's attached pane. PURE (db reads only): no
|
|
22
|
-
* tmux probe, so the pane may be stale; the caller liveness-checks before
|
|
23
|
-
* targeting it. */
|
|
24
|
-
export declare function graphSurfaceTarget(nodeId: string): FocusRow | null;
|
|
25
15
|
/** Wait until a broker's view.sock accepts a connection. The launching caller
|
|
26
16
|
* stays async, so the broker's real exit signal can fail fast the wait instead
|
|
27
17
|
* of being masked by a zombie pid. Success proves more than file existence: it
|
|
@@ -84,6 +74,7 @@ export declare function openViewerWindow(nodeId: string, session: string, opts?:
|
|
|
84
74
|
export declare function focus(nodeId: string, opts: {
|
|
85
75
|
pane?: string;
|
|
86
76
|
newPane?: boolean;
|
|
77
|
+
inPlace?: boolean;
|
|
87
78
|
revive: Reviver;
|
|
88
79
|
}): Promise<FocusResult>;
|
|
89
80
|
/** Tear a node off its viewer (close/reset/cancel teardown). The broker engine
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
// tmux-chrome.ts import tmux.ts") holds — every other module reaches the
|
|
25
25
|
// driver verbs through here.
|
|
26
26
|
import { createConnection } from 'node:net';
|
|
27
|
-
import { getNode, deleteNode, listNodes, openFocusRow, closeFocusRow, getFocusByNode, getFocusByPane, getFocusById, listFocuses as listFocusRows,
|
|
27
|
+
import { getNode, fullName, deleteNode, listNodes, openFocusRow, closeFocusRow, getFocusByNode, getFocusByPane, getFocusById, listFocuses as listFocusRows, } from '../canvas/index.js';
|
|
28
28
|
import { nodeHasAssistantMessage, isNodeStreaming } from '../canvas/render.js';
|
|
29
29
|
import { headlessBrokerHost } from './host.js';
|
|
30
|
-
import { paneExists, paneLocation, ensureSession, openNodeWindow, splitWindow, closePane, currentTmux, switchClient, selectWindow, selectLayout, selectPane, getPaneOption, } from './tmux.js';
|
|
31
|
-
import { newNodeId
|
|
30
|
+
import { paneExists, paneLocation, ensureSession, openNodeWindow, splitWindow, closePane, currentTmux, switchClient, selectWindow, selectLayout, selectPane, getPaneOption, setPaneOption, respawnPaneSync, windowOfPane, renameWindow, } from './tmux.js';
|
|
31
|
+
import { newNodeId } from './nodes.js';
|
|
32
32
|
import { viewSocketPath } from '../canvas/paths.js';
|
|
33
33
|
import { isPidAlive } from '../canvas/pid.js';
|
|
34
34
|
// Placement is the sanctioned model-over-driver (§2.1): non-placement runtime /
|
|
@@ -61,8 +61,7 @@ installTmuxBindings as installMenuBinding, setPaneOption, getPaneOption, piComma
|
|
|
61
61
|
//
|
|
62
62
|
// GC is lazy on read: a row whose viewer pane no longer exists (the user closed
|
|
63
63
|
// the window, the attach client died) is pruned the next time it is read, so the
|
|
64
|
-
// registry self-heals without a sweeper.
|
|
65
|
-
// tmux probe — by design; its caller liveness-checks the pane it returns.)
|
|
64
|
+
// registry self-heals without a sweeper.
|
|
66
65
|
// ---------------------------------------------------------------------------
|
|
67
66
|
/** Prune a viewer row whose pane is gone; return it iff its pane is still live. */
|
|
68
67
|
function liveOrPrune(f) {
|
|
@@ -100,27 +99,6 @@ export function listFocuses() {
|
|
|
100
99
|
return live;
|
|
101
100
|
}
|
|
102
101
|
// ---------------------------------------------------------------------------
|
|
103
|
-
// Graph → viewer routing (for surfacing human-in-the-loop prompts)
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
/** The on-screen viewer a human-in-the-loop prompt raised by `nodeId` should
|
|
106
|
-
* surface into: the HIGHEST node of nodeId's graph that has a viewer — the
|
|
107
|
-
* viewer closest to the graph root, i.e. the session/window the user is actually
|
|
108
|
-
* watching this work in. Walks nodeId's spine to its root, enumerates the whole
|
|
109
|
-
* tree root-first (`view` is BFS ⇒ shallowest first), and returns the viewer row
|
|
110
|
-
* of the first node that has one. null when nothing in the graph is on screen —
|
|
111
|
-
* the caller then surfaces in the user's attached pane. PURE (db reads only): no
|
|
112
|
-
* tmux probe, so the pane may be stale; the caller liveness-checks before
|
|
113
|
-
* targeting it. */
|
|
114
|
-
export function graphSurfaceTarget(nodeId) {
|
|
115
|
-
const root = rootOfSpine(nodeId);
|
|
116
|
-
for (const id of [root, ...view(root)]) {
|
|
117
|
-
const f = getFocusByNode(id);
|
|
118
|
-
if (f !== null && f.pane !== null)
|
|
119
|
-
return f;
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
102
|
// view.sock readiness — the one shared broker-cold-start primitive
|
|
125
103
|
// ---------------------------------------------------------------------------
|
|
126
104
|
const BROKER_FOCUS_SOCKET_WAIT_MS = 30_000;
|
|
@@ -311,6 +289,45 @@ export async function focus(nodeId, opts) {
|
|
|
311
289
|
// split pane's location — the caller's own session.
|
|
312
290
|
const callerSession = paneLocation(callerPane)?.session ?? null;
|
|
313
291
|
const fallbackSession = callerSession ?? '';
|
|
292
|
+
// (b0) --in-place → the CALLER pane BECOMES this node's viewer, replacing
|
|
293
|
+
// whatever it showed (the attach subagent scroll+swap: ↓ into the reports
|
|
294
|
+
// roster, Enter dives into that child right here, no new split). This
|
|
295
|
+
// re-execs the caller pane via `respawn-pane -k`, so it MUST be driven
|
|
296
|
+
// from OUTSIDE that pane's process tree (the viewer shells this through
|
|
297
|
+
// `tmux run-shell`, never a direct child) — else the respawn kills its own
|
|
298
|
+
// driver mid-call. Move the child's viewer here if it had one elsewhere
|
|
299
|
+
// (UNIQUE one-viewer-per-node) and drop the caller pane's prior viewer row
|
|
300
|
+
// (that node's broker keeps running headless, reconnectable by a later
|
|
301
|
+
// focus). The respawned `crtr surface attach` replays scrollback from the
|
|
302
|
+
// welcome snapshot and self-tags the pane `@crtr_node`.
|
|
303
|
+
if (opts.inPlace === true) {
|
|
304
|
+
const prior = focusOf(nodeId);
|
|
305
|
+
if (prior !== null) {
|
|
306
|
+
if (prior.pane !== null && prior.pane !== callerPane && paneExists(prior.pane))
|
|
307
|
+
closePane(prior.pane);
|
|
308
|
+
closeFocusRow(prior.focus_id);
|
|
309
|
+
}
|
|
310
|
+
const here = getFocusByPane(callerPane);
|
|
311
|
+
if (here !== null && here.node_id !== nodeId)
|
|
312
|
+
closeFocusRow(here.focus_id);
|
|
313
|
+
// Clear the stale tag before respawn so it never names the wrong node during
|
|
314
|
+
// the gap before the fresh attach re-tags on connect.
|
|
315
|
+
try {
|
|
316
|
+
setPaneOption(callerPane, '@crtr_node', '');
|
|
317
|
+
}
|
|
318
|
+
catch { /* best-effort */ }
|
|
319
|
+
const window = windowOfPane(callerPane);
|
|
320
|
+
// Continuity: the fresh viewer re-enters the inline roster on the node just
|
|
321
|
+
// swapped to (self), so the user keeps scrolling from where they were rather
|
|
322
|
+
// than being dropped back into the text box.
|
|
323
|
+
const ok = respawnPaneSync({ pane: callerPane, cwd: meta.cwd, env: { ...viewerSplitEnv(), CRTR_ATTACH_ROSTER: '1' }, command: `crtr surface attach to ${nodeId}` });
|
|
324
|
+
if (ok) {
|
|
325
|
+
if (window !== null)
|
|
326
|
+
renameWindow(window, fullName(meta));
|
|
327
|
+
registerViewerFocus(nodeId, callerPane, callerSession, window);
|
|
328
|
+
}
|
|
329
|
+
return { focused: ok, session: callerSession, inPlace: true, revived };
|
|
330
|
+
}
|
|
314
331
|
// (b) --new-pane → always a fresh viewer beside the caller. Drop any prior
|
|
315
332
|
// viewer this node holds elsewhere (UNIQUE(node_id): one viewer per node).
|
|
316
333
|
if (opts.newPane === true) {
|
|
@@ -7,7 +7,13 @@ export declare function compactSessionSearchText(text: string): string;
|
|
|
7
7
|
export declare class SessionListCache {
|
|
8
8
|
private readonly cacheFile;
|
|
9
9
|
private cache;
|
|
10
|
-
|
|
10
|
+
/** In-flight cwd scans keyed by normalized session dir, so an immediate
|
|
11
|
+
* /resume that races the background prewarm (or a second identical open)
|
|
12
|
+
* shares the one running jsonlFiles/stat/build pass instead of queueing a
|
|
13
|
+
* redundant one behind it. */
|
|
14
|
+
private inflightDir;
|
|
15
|
+
private loadPromise;
|
|
16
|
+
private buildChain;
|
|
11
17
|
private persisting;
|
|
12
18
|
private persistAgain;
|
|
13
19
|
private persistTimer;
|
|
@@ -18,12 +24,26 @@ export declare class SessionListCache {
|
|
|
18
24
|
* broker loop — pre-warming beats worker threads here. Errors are swallowed
|
|
19
25
|
* by the same paths that guard a normal open. */
|
|
20
26
|
prewarm(sessionDir: string): void;
|
|
21
|
-
/** Sessions for one cwd's session dir (the picker's default `cwd` scope).
|
|
27
|
+
/** Sessions for one cwd's session dir (the picker's default `cwd` scope).
|
|
28
|
+
* Single-flight: an identical concurrent cwd open (prewarm ⇄ immediate
|
|
29
|
+
* /resume) shares the in-flight promise rather than repeating the scan.
|
|
30
|
+
* Incompatible cwd/all builds still serialize through the build chain. */
|
|
22
31
|
listDir(sessionDir: string): Promise<WireSessionInfo[]>;
|
|
32
|
+
private buildDir;
|
|
23
33
|
/** Sessions across every cwd's dir under the sessions root (`all` scope). */
|
|
24
34
|
listAll(sessionsRoot: string): Promise<WireSessionInfo[]>;
|
|
35
|
+
/** Serialize builds through a single chain: they mutate the shared cache map
|
|
36
|
+
* and, cold, scan the same large corpus. Identical cwd opens are already
|
|
37
|
+
* collapsed upstream by `listDir`'s in-flight sharing; chaining serializes
|
|
38
|
+
* the remaining incompatible builds (a cwd scan vs an `all` scan) so they
|
|
39
|
+
* never race map mutation or launch overlapping cold scans. */
|
|
25
40
|
private build;
|
|
41
|
+
private buildLocked;
|
|
42
|
+
/** Load the persisted cache exactly once, single-flight. Returns a shared
|
|
43
|
+
* promise so a build that starts while the read is still in flight awaits the
|
|
44
|
+
* same load rather than proceeding against a not-yet-populated map. */
|
|
26
45
|
private ensureLoaded;
|
|
46
|
+
private loadFromDisk;
|
|
27
47
|
/** Coalesce persists onto a single trailing timer so the tens-of-MB
|
|
28
48
|
* `JSON.stringify` never runs on the reply path and rapid opens write once. */
|
|
29
49
|
private schedulePersist;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { createReadStream } from 'node:fs';
|
|
20
20
|
import { readFile, readdir, rename, stat, unlink, writeFile } from 'node:fs/promises';
|
|
21
21
|
import { createInterface } from 'node:readline';
|
|
22
|
-
import { dirname, join } from 'node:path';
|
|
22
|
+
import { dirname, join, normalize } from 'node:path';
|
|
23
23
|
/** Search-text bound: `allMessagesText` is searched on every query edit; pi's
|
|
24
24
|
* unbounded value retains gigabytes in the viewer and turns key-repeat into GC
|
|
25
25
|
* jitter. The opening and latest text keep search useful. */
|
|
@@ -41,18 +41,41 @@ const CACHE_SCHEMA_VERSION = 1;
|
|
|
41
41
|
* are coalesced onto this trailing timer so rapid opens write at most once per
|
|
42
42
|
* window, off the hot path. */
|
|
43
43
|
const PERSIST_DEBOUNCE_MS = 2000;
|
|
44
|
+
function isFiniteNonNeg(v) {
|
|
45
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
46
|
+
}
|
|
47
|
+
/** An optional field is valid iff absent/undefined or a string. */
|
|
48
|
+
function isOptionalString(v) {
|
|
49
|
+
return v === undefined || typeof v === 'string';
|
|
50
|
+
}
|
|
51
|
+
/** Validate the full persisted `WireSessionInfo` shape. A same-version file whose
|
|
52
|
+
* entry drifts from the current wire schema (any required field missing or
|
|
53
|
+
* mistyped, any optional field the wrong type, non-finite/negative metadata)
|
|
54
|
+
* must discard the whole cache and rebuild rather than feed a malformed entry to
|
|
55
|
+
* the viewer or the sort. */
|
|
56
|
+
function isValidWireInfo(v) {
|
|
57
|
+
if (typeof v !== 'object' || v === null)
|
|
58
|
+
return false;
|
|
59
|
+
const i = v;
|
|
60
|
+
return (typeof i['path'] === 'string' &&
|
|
61
|
+
typeof i['id'] === 'string' &&
|
|
62
|
+
typeof i['cwd'] === 'string' &&
|
|
63
|
+
typeof i['created'] === 'string' &&
|
|
64
|
+
typeof i['modified'] === 'string' &&
|
|
65
|
+
isFiniteNonNeg(i['messageCount']) &&
|
|
66
|
+
typeof i['firstMessage'] === 'string' &&
|
|
67
|
+
typeof i['allMessagesText'] === 'string' &&
|
|
68
|
+
isOptionalString(i['name']) &&
|
|
69
|
+
isOptionalString(i['parentSessionPath']));
|
|
70
|
+
}
|
|
44
71
|
/** Shape-check a loaded entry — a well-formed-JSON file with the right version
|
|
45
|
-
* but a stale entry shape
|
|
46
|
-
*
|
|
72
|
+
* but a stale entry shape would otherwise feed a malformed `WireSessionInfo` to
|
|
73
|
+
* the viewer/sort. Any failure discards the whole file (rebuild). */
|
|
47
74
|
function isValidEntry(e) {
|
|
48
75
|
if (typeof e !== 'object' || e === null)
|
|
49
76
|
return false;
|
|
50
77
|
const c = e;
|
|
51
|
-
return (
|
|
52
|
-
typeof c['size'] === 'number' &&
|
|
53
|
-
typeof c['info'] === 'object' &&
|
|
54
|
-
c['info'] !== null &&
|
|
55
|
-
typeof c['info']['modified'] === 'string');
|
|
78
|
+
return isFiniteNonNeg(c['mtimeMs']) && isFiniteNonNeg(c['size']) && isValidWireInfo(c['info']);
|
|
56
79
|
}
|
|
57
80
|
// ---- faithful mirror of pi's private buildSessionInfo -----------------------
|
|
58
81
|
function parseLine(line) {
|
|
@@ -172,7 +195,13 @@ async function buildWireSessionInfo(filePath) {
|
|
|
172
195
|
export class SessionListCache {
|
|
173
196
|
cacheFile;
|
|
174
197
|
cache = new Map();
|
|
175
|
-
|
|
198
|
+
/** In-flight cwd scans keyed by normalized session dir, so an immediate
|
|
199
|
+
* /resume that races the background prewarm (or a second identical open)
|
|
200
|
+
* shares the one running jsonlFiles/stat/build pass instead of queueing a
|
|
201
|
+
* redundant one behind it. */
|
|
202
|
+
inflightDir = new Map();
|
|
203
|
+
loadPromise = null;
|
|
204
|
+
buildChain = Promise.resolve();
|
|
176
205
|
persisting = false;
|
|
177
206
|
persistAgain = false;
|
|
178
207
|
persistTimer = null;
|
|
@@ -187,9 +216,32 @@ export class SessionListCache {
|
|
|
187
216
|
prewarm(sessionDir) {
|
|
188
217
|
void this.listDir(sessionDir);
|
|
189
218
|
}
|
|
190
|
-
/** Sessions for one cwd's session dir (the picker's default `cwd` scope).
|
|
191
|
-
|
|
192
|
-
|
|
219
|
+
/** Sessions for one cwd's session dir (the picker's default `cwd` scope).
|
|
220
|
+
* Single-flight: an identical concurrent cwd open (prewarm ⇄ immediate
|
|
221
|
+
* /resume) shares the in-flight promise rather than repeating the scan.
|
|
222
|
+
* Incompatible cwd/all builds still serialize through the build chain. */
|
|
223
|
+
listDir(sessionDir) {
|
|
224
|
+
const key = normalize(sessionDir);
|
|
225
|
+
const existing = this.inflightDir.get(key);
|
|
226
|
+
if (existing)
|
|
227
|
+
return existing;
|
|
228
|
+
const op = this.buildDir(key);
|
|
229
|
+
this.inflightDir.set(key, op);
|
|
230
|
+
// Attach both settlement handlers so the slot is cleared on success OR
|
|
231
|
+
// rejection, a rejected scan can never poison the next open, and a
|
|
232
|
+
// discarded prewarm rejection never becomes unhandled.
|
|
233
|
+
const clear = () => {
|
|
234
|
+
if (this.inflightDir.get(key) === op)
|
|
235
|
+
this.inflightDir.delete(key);
|
|
236
|
+
};
|
|
237
|
+
op.then(clear, clear);
|
|
238
|
+
return op;
|
|
239
|
+
}
|
|
240
|
+
buildDir(sessionDir) {
|
|
241
|
+
// Prune only keys directly in this cwd's dir. A cwd-scope open must not evict
|
|
242
|
+
// other cwds' cached entries: they weren't rescanned, so their absence from
|
|
243
|
+
// `files` says nothing about whether they still exist on disk.
|
|
244
|
+
return jsonlFiles(sessionDir).then((files) => this.build(files, (key) => dirname(key) === sessionDir));
|
|
193
245
|
}
|
|
194
246
|
/** Sessions across every cwd's dir under the sessions root (`all` scope). */
|
|
195
247
|
async listAll(sessionsRoot) {
|
|
@@ -205,19 +257,30 @@ export class SessionListCache {
|
|
|
205
257
|
const files = [];
|
|
206
258
|
for (const dir of dirEntries)
|
|
207
259
|
files.push(...(await jsonlFiles(dir)));
|
|
208
|
-
|
|
260
|
+
// Prune every cached key that is a direct grandchild of the sessions root
|
|
261
|
+
// (`<root>/<cwd-dir>/<file>.jsonl`). Using the path boundary rather than the
|
|
262
|
+
// set of dirs that still exist means a wholly-deleted cwd dir — absent from
|
|
263
|
+
// this scan — also has its stale entries evicted, so the persisted file never
|
|
264
|
+
// accumulates bloat from removed cwds.
|
|
265
|
+
return this.build(files, (key) => dirname(dirname(key)) === sessionsRoot);
|
|
266
|
+
}
|
|
267
|
+
/** Serialize builds through a single chain: they mutate the shared cache map
|
|
268
|
+
* and, cold, scan the same large corpus. Identical cwd opens are already
|
|
269
|
+
* collapsed upstream by `listDir`'s in-flight sharing; chaining serializes
|
|
270
|
+
* the remaining incompatible builds (a cwd scan vs an `all` scan) so they
|
|
271
|
+
* never race map mutation or launch overlapping cold scans. */
|
|
272
|
+
build(files, shouldPrune) {
|
|
273
|
+
const run = this.buildChain.then(() => this.buildLocked(files, shouldPrune));
|
|
274
|
+
// Keep the chain alive past a rejected build without leaking the rejection.
|
|
275
|
+
this.buildChain = run.then(() => { }, () => { });
|
|
276
|
+
return run;
|
|
209
277
|
}
|
|
210
|
-
async
|
|
278
|
+
async buildLocked(files, shouldPrune) {
|
|
211
279
|
await this.ensureLoaded();
|
|
212
280
|
const present = new Set(files);
|
|
213
281
|
let changed = false;
|
|
214
|
-
// Prune ONLY keys under the dirs we actually scanned. A cwd-scope open must
|
|
215
|
-
// not evict other cwds' cached entries: they weren't rescanned, so their
|
|
216
|
-
// absence from `files` says nothing about whether they still exist on disk.
|
|
217
|
-
// (Pruning the whole map here would self-destruct the global/`all` cache on
|
|
218
|
-
// every cwd open.)
|
|
219
282
|
for (const key of this.cache.keys()) {
|
|
220
|
-
if (!present.has(key) &&
|
|
283
|
+
if (!present.has(key) && shouldPrune(key)) {
|
|
221
284
|
this.cache.delete(key);
|
|
222
285
|
changed = true;
|
|
223
286
|
}
|
|
@@ -262,10 +325,15 @@ export class SessionListCache {
|
|
|
262
325
|
result.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
263
326
|
return result;
|
|
264
327
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
328
|
+
/** Load the persisted cache exactly once, single-flight. Returns a shared
|
|
329
|
+
* promise so a build that starts while the read is still in flight awaits the
|
|
330
|
+
* same load rather than proceeding against a not-yet-populated map. */
|
|
331
|
+
ensureLoaded() {
|
|
332
|
+
if (!this.loadPromise)
|
|
333
|
+
this.loadPromise = this.loadFromDisk();
|
|
334
|
+
return this.loadPromise;
|
|
335
|
+
}
|
|
336
|
+
async loadFromDisk() {
|
|
269
337
|
try {
|
|
270
338
|
const raw = JSON.parse(await readFile(this.cacheFile, 'utf8'));
|
|
271
339
|
// Version mismatch or shape drift → discard and rebuild. No lenient
|
|
@@ -24,6 +24,7 @@ import { appendSituationalContext } from './situational-context.js';
|
|
|
24
24
|
import { hasRoadmap, seedRoadmap } from './roadmap.js';
|
|
25
25
|
import { buildIdentityAssertion, buildWakeBearings } from './bearings.js';
|
|
26
26
|
import { installTmuxBindings } from './tmux-chrome.js';
|
|
27
|
+
import { installInboxBinding } from '@crouton-kit/humanloop';
|
|
27
28
|
import { getNode, fullName, recordPid } from '../canvas/index.js';
|
|
28
29
|
import { registerViewerFocus, openViewerWindow, waitForBrokerViewSocket, viewerSplitEnv, windowOfPane, currentTmux, inTmux, focusWindow, } from './placement.js';
|
|
29
30
|
import { transition } from './lifecycle.js';
|
|
@@ -102,6 +103,14 @@ export async function bootRoot(opts) {
|
|
|
102
103
|
installTmuxBindings();
|
|
103
104
|
}
|
|
104
105
|
catch { /* best-effort */ }
|
|
106
|
+
// Reconcile humanloop's collision-safe M-i inbox popup binding on the same
|
|
107
|
+
// server so the inbox is reachable from any window. Crouter calls humanloop's
|
|
108
|
+
// installer — it never authors or parses its own tmux command; the installer
|
|
109
|
+
// is idempotent and leaves any pre-existing M-i binding untouched.
|
|
110
|
+
try {
|
|
111
|
+
installInboxBinding();
|
|
112
|
+
}
|
|
113
|
+
catch { /* best-effort */ }
|
|
105
114
|
// This terminal becomes the root's attach VIEWER in the user's CURRENT session,
|
|
106
115
|
// so its window stays where the user already is. The node row carries NO
|
|
107
116
|
// presence — a broker node's pane/window/tmux_session stay NULL; the viewer
|
package/dist/core/spawn.d.ts
CHANGED
|
@@ -14,62 +14,3 @@ export declare function isInTmux(): boolean;
|
|
|
14
14
|
* degrade to the inbox-drain follow-up. */
|
|
15
15
|
export declare function tmuxServerReachable(): boolean;
|
|
16
16
|
export declare function shellQuote(s: string): string;
|
|
17
|
-
/** Count panes in a tmux window (0 outside tmux / on error). With `targetPane`,
|
|
18
|
-
* counts the window THAT pane lives in (the placement decision must reflect the
|
|
19
|
-
* window the new pane will actually open into, not the caller's backstage one);
|
|
20
|
-
* without it, the caller's current window. */
|
|
21
|
-
export declare function countPanesInWindow(targetPane?: string): number;
|
|
22
|
-
/** Back-compat alias: panes in the caller's current window. */
|
|
23
|
-
export declare function countPanesInCurrentWindow(): number;
|
|
24
|
-
/** Does this tmux pane id still exist? `display-message` EXITS 0 with EMPTY
|
|
25
|
-
* output on an unresolvable pane, so test for non-empty stdout, not just `.ok`.
|
|
26
|
-
* False outside tmux / on error. */
|
|
27
|
-
export declare function paneAlive(pane: string): boolean;
|
|
28
|
-
/** Resolve a tmux pane id to its `session:window_index` — the target form
|
|
29
|
-
* `new-window -t` accepts. tmux REJECTS a pane id for new-window ("can't
|
|
30
|
-
* specify pane here"); only split-window -t takes a pane. null outside tmux /
|
|
31
|
-
* on a bad pane id / on error / empty. */
|
|
32
|
-
export declare function paneWindowTarget(pane: string): string | null;
|
|
33
|
-
/** The active pane of the user's attached tmux client — where they are looking
|
|
34
|
-
* right now. `list-clients` first attached client, then its current pane. Used
|
|
35
|
-
* to surface a human prompt in the user's view when nothing in the asking
|
|
36
|
-
* node's graph is focused. null outside tmux / no client / on error. */
|
|
37
|
-
export declare function attachedClientPane(): string | null;
|
|
38
|
-
/**
|
|
39
|
-
* Schedule a kill-pane on the *current* tmux pane after `delaySeconds`, detached
|
|
40
|
-
* so the caller can return normally before the pane dies. No-op outside tmux,
|
|
41
|
-
* when TMUX_PANE is unset, or when delaySeconds <= 0.
|
|
42
|
-
*/
|
|
43
|
-
export declare function scheduleKillCurrentPane(delaySeconds: number): boolean;
|
|
44
|
-
export interface DetachOptions {
|
|
45
|
-
/** Inner command to run in the new pane. */
|
|
46
|
-
command: string;
|
|
47
|
-
cwd: string;
|
|
48
|
-
/** Optional id injected as the CRTR_JOB_ID env var in the pane. */
|
|
49
|
-
jobId?: string;
|
|
50
|
-
/** Where to open the new pane. */
|
|
51
|
-
placement: 'split-h' | 'split-v' | 'new-window';
|
|
52
|
-
/** Seconds before killing the originating pane so the caller can finish. */
|
|
53
|
-
killAfterSeconds: number;
|
|
54
|
-
/** Pin the new pane to this tmux pane: split-window splits it; new-window is
|
|
55
|
-
* inserted immediately after its window (-a -t <pane>). Without this, tmux
|
|
56
|
-
* uses the attached client's currently-focused pane — which drifts if the
|
|
57
|
-
* user switches windows between kickoff and spawn. */
|
|
58
|
-
targetPane?: string;
|
|
59
|
-
/** Pass tmux `-d` to new-window so CREATING the window never switches the
|
|
60
|
-
* attached client to it (split-window already leaves the client's view put).
|
|
61
|
-
* The prompt lands in the target session/window without jumping the user out
|
|
62
|
-
* of what they are looking at. No effect on split-h/split-v. */
|
|
63
|
-
detached?: boolean;
|
|
64
|
-
}
|
|
65
|
-
export interface DetachResult {
|
|
66
|
-
status: 'spawned' | 'spawn-failed' | 'not-in-tmux';
|
|
67
|
-
paneId?: string;
|
|
68
|
-
message: string;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Fire-and-forget: launch `opts.command` in a new pane (or window), then
|
|
72
|
-
* schedule the originating pane to be killed after `killAfterSeconds`. Returns
|
|
73
|
-
* as soon as the new pane is up; does NOT wait for the command to finish.
|
|
74
|
-
*/
|
|
75
|
-
export declare function spawnAndDetach(opts: DetachOptions): DetachResult;
|
package/dist/core/spawn.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
// Tmux
|
|
1
|
+
// Tmux reachability + quoting helpers for the `human` command tree.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// machinery in core/runtime/tmux.ts; this module is only the pane-split path the
|
|
8
|
-
// human TUI needs.
|
|
3
|
+
// The human surface gates on whether the canvas tmux server is reachable
|
|
4
|
+
// (tmuxServerReachable) so it can decide whether to surface a prompt or degrade
|
|
5
|
+
// to the inbox-drain follow-up. The canvas runtime has its own
|
|
6
|
+
// one-window-per-node machinery in core/runtime/tmux.ts.
|
|
9
7
|
import { spawnSync } from 'node:child_process';
|
|
10
8
|
/** Does THIS process sit inside a tmux client (its own $TMUX is set)? NOT the
|
|
11
9
|
* gate for the human surface — see tmuxServerReachable. A headless-broker child
|
|
@@ -30,159 +28,3 @@ export function tmuxServerReachable() {
|
|
|
30
28
|
export function shellQuote(s) {
|
|
31
29
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
32
30
|
}
|
|
33
|
-
/** Count panes in a tmux window (0 outside tmux / on error). With `targetPane`,
|
|
34
|
-
* counts the window THAT pane lives in (the placement decision must reflect the
|
|
35
|
-
* window the new pane will actually open into, not the caller's backstage one);
|
|
36
|
-
* without it, the caller's current window. */
|
|
37
|
-
export function countPanesInWindow(targetPane) {
|
|
38
|
-
const args = targetPane !== undefined && targetPane !== ''
|
|
39
|
-
? ['list-panes', '-t', targetPane, '-F', '#{pane_id}']
|
|
40
|
-
: ['list-panes', '-F', '#{pane_id}'];
|
|
41
|
-
const result = spawnSync('tmux', args, { encoding: 'utf8' });
|
|
42
|
-
if (result.status !== 0)
|
|
43
|
-
return 0;
|
|
44
|
-
return result.stdout.split('\n').filter((line) => line.trim() !== '').length;
|
|
45
|
-
}
|
|
46
|
-
/** Back-compat alias: panes in the caller's current window. */
|
|
47
|
-
export function countPanesInCurrentWindow() {
|
|
48
|
-
return countPanesInWindow();
|
|
49
|
-
}
|
|
50
|
-
/** Does this tmux pane id still exist? `display-message` EXITS 0 with EMPTY
|
|
51
|
-
* output on an unresolvable pane, so test for non-empty stdout, not just `.ok`.
|
|
52
|
-
* False outside tmux / on error. */
|
|
53
|
-
export function paneAlive(pane) {
|
|
54
|
-
if (!/^%\d+$/.test(pane))
|
|
55
|
-
return false;
|
|
56
|
-
const r = spawnSync('tmux', ['display-message', '-p', '-t', pane, '#{pane_id}'], {
|
|
57
|
-
encoding: 'utf8',
|
|
58
|
-
});
|
|
59
|
-
return r.status === 0 && r.stdout.trim() !== '';
|
|
60
|
-
}
|
|
61
|
-
/** Resolve a tmux pane id to its `session:window_index` — the target form
|
|
62
|
-
* `new-window -t` accepts. tmux REJECTS a pane id for new-window ("can't
|
|
63
|
-
* specify pane here"); only split-window -t takes a pane. null outside tmux /
|
|
64
|
-
* on a bad pane id / on error / empty. */
|
|
65
|
-
export function paneWindowTarget(pane) {
|
|
66
|
-
if (!/^%\d+$/.test(pane))
|
|
67
|
-
return null;
|
|
68
|
-
const r = spawnSync('tmux', ['display-message', '-p', '-t', pane, '#{session_name}:#{window_index}'], { encoding: 'utf8' });
|
|
69
|
-
if (r.status !== 0)
|
|
70
|
-
return null;
|
|
71
|
-
const t = r.stdout.trim();
|
|
72
|
-
return t !== '' ? t : null;
|
|
73
|
-
}
|
|
74
|
-
/** The active pane of the user's attached tmux client — where they are looking
|
|
75
|
-
* right now. `list-clients` first attached client, then its current pane. Used
|
|
76
|
-
* to surface a human prompt in the user's view when nothing in the asking
|
|
77
|
-
* node's graph is focused. null outside tmux / no client / on error. */
|
|
78
|
-
export function attachedClientPane() {
|
|
79
|
-
const clients = spawnSync('tmux', ['list-clients', '-F', '#{client_name}'], {
|
|
80
|
-
encoding: 'utf8',
|
|
81
|
-
});
|
|
82
|
-
if (clients.status !== 0)
|
|
83
|
-
return null;
|
|
84
|
-
const name = clients.stdout
|
|
85
|
-
.split('\n')
|
|
86
|
-
.map((l) => l.trim())
|
|
87
|
-
.find((l) => l !== '');
|
|
88
|
-
if (name === undefined)
|
|
89
|
-
return null;
|
|
90
|
-
const pane = spawnSync('tmux', ['display-message', '-p', '-c', name, '#{pane_id}'], {
|
|
91
|
-
encoding: 'utf8',
|
|
92
|
-
});
|
|
93
|
-
if (pane.status !== 0)
|
|
94
|
-
return null;
|
|
95
|
-
const id = pane.stdout.trim();
|
|
96
|
-
return id !== '' ? id : null;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Schedule a kill-pane on the *current* tmux pane after `delaySeconds`, detached
|
|
100
|
-
* so the caller can return normally before the pane dies. No-op outside tmux,
|
|
101
|
-
* when TMUX_PANE is unset, or when delaySeconds <= 0.
|
|
102
|
-
*/
|
|
103
|
-
export function scheduleKillCurrentPane(delaySeconds) {
|
|
104
|
-
const currentPane = process.env.TMUX_PANE;
|
|
105
|
-
if (currentPane === undefined || currentPane === '' || delaySeconds <= 0) {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
const killCmd = `sleep ${delaySeconds}; tmux kill-pane -t ${currentPane}`;
|
|
109
|
-
spawnSync('sh', ['-c', `nohup sh -c ${shellQuote(killCmd)} </dev/null >/dev/null 2>&1 &`], {
|
|
110
|
-
stdio: 'ignore',
|
|
111
|
-
});
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Fire-and-forget: launch `opts.command` in a new pane (or window), then
|
|
116
|
-
* schedule the originating pane to be killed after `killAfterSeconds`. Returns
|
|
117
|
-
* as soon as the new pane is up; does NOT wait for the command to finish.
|
|
118
|
-
*/
|
|
119
|
-
export function spawnAndDetach(opts) {
|
|
120
|
-
// Gate on the CANVAS tmux server's reachability, NOT the caller's $TMUX. A
|
|
121
|
-
// headless-broker child strips its own $TMUX (broker.ts) but the canvas server
|
|
122
|
-
// is on the default socket, so `split-window -t <pane>` against a resolved
|
|
123
|
-
// target pane (resolveHumanTarget) works fine from it. Only a genuine
|
|
124
|
-
// no-server case degrades to the inbox-drain follow-up.
|
|
125
|
-
if (!tmuxServerReachable()) {
|
|
126
|
-
return {
|
|
127
|
-
status: 'not-in-tmux',
|
|
128
|
-
message: 'handoff requires a reachable tmux server',
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
const splitArgs = [];
|
|
132
|
-
if (opts.placement === 'new-window') {
|
|
133
|
-
splitArgs.push('new-window');
|
|
134
|
-
if (opts.detached === true)
|
|
135
|
-
splitArgs.push('-d'); // don't switch the client to it
|
|
136
|
-
if (opts.targetPane !== undefined && opts.targetPane !== '') {
|
|
137
|
-
// new-window -t REJECTS a pane id (tmux exits 1: "can't specify pane
|
|
138
|
-
// here") — only split-window -t accepts a pane. Resolve the target pane to
|
|
139
|
-
// its session:window first; -a then inserts the new window right after it.
|
|
140
|
-
// If the pane can't be resolved, fall back to no -t (tmux uses current).
|
|
141
|
-
const winTarget = paneWindowTarget(opts.targetPane);
|
|
142
|
-
if (winTarget !== null)
|
|
143
|
-
splitArgs.push('-a', '-t', winTarget);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
splitArgs.push('split-window');
|
|
148
|
-
splitArgs.push(opts.placement === 'split-h' ? '-h' : '-v');
|
|
149
|
-
if (opts.targetPane !== undefined && opts.targetPane !== '') {
|
|
150
|
-
splitArgs.push('-t', opts.targetPane);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
splitArgs.push('-P', '-F', '#{pane_id}');
|
|
154
|
-
splitArgs.push('-c', opts.cwd);
|
|
155
|
-
if (opts.jobId !== undefined) {
|
|
156
|
-
splitArgs.push('-e', `CRTR_JOB_ID=${opts.jobId}`);
|
|
157
|
-
}
|
|
158
|
-
splitArgs.push(opts.command);
|
|
159
|
-
const split = spawnSync('tmux', splitArgs, { encoding: 'utf8' });
|
|
160
|
-
if (split.status !== 0) {
|
|
161
|
-
const stderrText = split.stderr.trim();
|
|
162
|
-
const msg = stderrText === '' ? 'tmux split-window/new-window failed' : stderrText;
|
|
163
|
-
return { status: 'spawn-failed', message: msg };
|
|
164
|
-
}
|
|
165
|
-
const paneId = split.stdout.trim();
|
|
166
|
-
// Force `remain-on-exit off` at PANE scope on the new pane. remain-on-exit is
|
|
167
|
-
// a pane option (tmux 3.x) inherited from the window-scoped value, and the
|
|
168
|
-
// canvas runtime arms `remain-on-exit on` on a node's vehicle/focus WINDOW
|
|
169
|
-
// (F3 freeze, see runtime/tmux.ts setRemainOnExit). A split-window pane opened
|
|
170
|
-
// into that window inherits the `on`, so the humanloop TUI pane would linger
|
|
171
|
-
// as a dead pane ("pane is dead (status 0, …)") when `crtr human _run` exits 0
|
|
172
|
-
// instead of closing. Overriding at pane scope destroys this pane on clean
|
|
173
|
-
// exit WITHOUT touching the window's value (focus freeze still works) or the
|
|
174
|
-
// user's global config. Best-effort: harmless no-op on tmux where the option
|
|
175
|
-
// is window-only.
|
|
176
|
-
if (paneId !== '') {
|
|
177
|
-
spawnSync('tmux', ['set-option', '-p', '-t', paneId, 'remain-on-exit', 'off'], {
|
|
178
|
-
stdio: 'ignore',
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
// Schedule self-kill of the originating pane.
|
|
182
|
-
scheduleKillCurrentPane(opts.killAfterSeconds);
|
|
183
|
-
return {
|
|
184
|
-
status: 'spawned',
|
|
185
|
-
paneId,
|
|
186
|
-
message: `handed off to pane ${paneId}`,
|
|
187
|
-
};
|
|
188
|
-
}
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -1205,8 +1205,8 @@ function reapDeadResidue(now) {
|
|
|
1205
1205
|
* row when its pane dies outside the sanctioned teardown paths (a user
|
|
1206
1206
|
* kill-pane, a tmux server restart, a crashed swap), so dead-pane rows
|
|
1207
1207
|
* accumulate forever — the audit found 15 of 21 rows pointing at gone panes,
|
|
1208
|
-
* and a stale row both
|
|
1209
|
-
* being adopted into a fresh viewport (UNIQUE node_id). Sweep them here: one
|
|
1208
|
+
* and a stale row both misreports the node's on-screen viewer and blocks its
|
|
1209
|
+
* node from being adopted into a fresh viewport (UNIQUE node_id). Sweep them here: one
|
|
1210
1210
|
* batched `list-panes -a` probe (never a per-row display-message), deleting
|
|
1211
1211
|
* rows whose recorded pane no longer exists. A pane-less row (a bridge/unplaced
|
|
1212
1212
|
* viewport) has nothing to verify and is left alone. On a FAILED probe (tmux
|
|
@@ -1294,10 +1294,11 @@ export async function superviseTick(now = Date.now()) {
|
|
|
1294
1294
|
return;
|
|
1295
1295
|
}
|
|
1296
1296
|
// kind:'human' rows are NOT broker-hosted agent nodes — they are bookkeeping
|
|
1297
|
-
// rows for the `crtr human` bridge.
|
|
1298
|
-
//
|
|
1299
|
-
//
|
|
1300
|
-
// a `pi_pid` or `pi_session_id`, so the universal broker-liveness
|
|
1297
|
+
// rows for the `crtr human` bridge. Their lifecycle is driven entirely by
|
|
1298
|
+
// humanloop's registered `_deliver` handler — which pushFinals the answer when
|
|
1299
|
+
// the human responds from the humanloop inbox — or by `human cancel`. They
|
|
1300
|
+
// never record a `pi_pid` or `pi_session_id`, so the universal broker-liveness
|
|
1301
|
+
// path would
|
|
1301
1302
|
// (after the boot grace) read every one as a never-booted broker and crash it
|
|
1302
1303
|
// with surfaceBootFailure — exactly the regression where a headless
|
|
1303
1304
|
// orchestrator's `human ask` nodes all died at boot. The daemon must never
|
package/dist/index.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export { appendSituationalContext } from './core/runtime/situational-context.js'
|
|
|
10
10
|
export { getNode, listNodes } from './core/canvas/canvas.js';
|
|
11
11
|
export { nodeDir } from './core/canvas/paths.js';
|
|
12
12
|
export type { NodeMeta, NodeRow, NodeStatus, Lifecycle, Mode, } from './core/canvas/types.js';
|
|
13
|
-
export {
|
|
14
|
-
export type {
|
|
13
|
+
export { ticketCountsForNodes, ticketsAcrossCanvas } from './core/canvas/attention.js';
|
|
14
|
+
export type { TicketEntry } from './core/canvas/attention.js';
|
|
15
15
|
export { readTelemetry, readContextTokens } from './core/canvas/telemetry.js';
|
|
16
16
|
export type { Telemetry } from './core/canvas/telemetry.js';
|
|
17
17
|
export { ViewSocketClient, BrokerUnavailableError } from './clients/attach/view-socket.js';
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ export { appendSituationalContext } from './core/runtime/situational-context.js'
|
|
|
23
23
|
// ── Canvas reads ─────────────────────────────────────────────────────────
|
|
24
24
|
export { getNode, listNodes } from './core/canvas/canvas.js';
|
|
25
25
|
export { nodeDir } from './core/canvas/paths.js';
|
|
26
|
-
export {
|
|
26
|
+
export { ticketCountsForNodes, ticketsAcrossCanvas } from './core/canvas/attention.js';
|
|
27
27
|
export { readTelemetry, readContextTokens } from './core/canvas/telemetry.js';
|
|
28
28
|
// ── Broker client ────────────────────────────────────────────────────────
|
|
29
29
|
export { ViewSocketClient, BrokerUnavailableError } from './clients/attach/view-socket.js';
|