@north-light/crouter 0.3.157 → 0.3.158

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/api/client.d.ts +4 -1
  2. package/dist/api/client.js +5 -0
  3. package/dist/api/dto/broker.d.ts +1 -1
  4. package/dist/api/dto/nodes.d.ts +15 -0
  5. package/dist/api/routes.d.ts +1 -0
  6. package/dist/api/routes.js +1 -0
  7. package/dist/builtin-views/chat/core.mjs +6 -51
  8. package/dist/builtin-views/chat/tui.mjs +6 -14
  9. package/dist/builtin-views/chat/web.jsx +2 -7
  10. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +1 -1
  11. package/dist/clients/attach/chrome/roster.d.ts +1 -1
  12. package/dist/clients/attach/chrome/roster.js +9 -1
  13. package/dist/clients/attach/command.js +5 -5
  14. package/dist/clients/attach/input/controller.d.ts +23 -8
  15. package/dist/clients/attach/input/controller.js +59 -29
  16. package/dist/clients/attach/overlays/dialogs.d.ts +1 -2
  17. package/dist/clients/attach/overlays/graph.d.ts +4 -1
  18. package/dist/clients/attach/overlays/graph.js +25 -7
  19. package/dist/clients/attach/session/context.d.ts +7 -0
  20. package/dist/clients/attach/session/frames.js +1 -8
  21. package/dist/clients/attach/session/input-wiring.d.ts +1 -1
  22. package/dist/clients/attach/session/input-wiring.js +21 -11
  23. package/dist/clients/attach/session/mode.d.ts +4 -3
  24. package/dist/clients/attach/session/mode.js +6 -1
  25. package/dist/clients/attach/session/reconnect.d.ts +3 -3
  26. package/dist/clients/attach/session/reconnect.js +6 -7
  27. package/dist/clients/attach/session/state-sync.d.ts +1 -1
  28. package/dist/clients/attach/session/state-sync.js +2 -2
  29. package/dist/clients/attach/slash/dispatch.d.ts +13 -1
  30. package/dist/clients/attach/slash/dispatch.js +65 -17
  31. package/dist/clients/attach/viewer.js +523 -523
  32. package/dist/clients/web/web-client/shared/protocol.d.ts +5 -3
  33. package/dist/commands/node.js +1 -1
  34. package/dist/core/__tests__/broker-sdk-wiring.test.js +18 -18
  35. package/dist/core/__tests__/chat-view-reconnect.test.js +44 -23
  36. package/dist/core/__tests__/full/broker-attach-limits.test.js +60 -36
  37. package/dist/core/__tests__/full/broker-attach-stream.test.js +4 -4
  38. package/dist/core/__tests__/full/broker-dialogs.test.js +121 -62
  39. package/dist/core/__tests__/helpers/broker-clients.js +2 -2
  40. package/dist/core/__tests__/session-model.test.js +15 -26
  41. package/dist/core/keybindings/__tests__/resolve.test.js +1 -1
  42. package/dist/core/keybindings/catalog.d.ts +2 -2
  43. package/dist/core/keybindings/catalog.js +0 -2
  44. package/dist/core/runtime/auth-reload.d.ts +4 -4
  45. package/dist/core/runtime/auth-reload.js +9 -13
  46. package/dist/core/runtime/boot-root.d.ts +2 -2
  47. package/dist/core/runtime/boot-root.js +7 -7
  48. package/dist/core/runtime/broker-protocol.d.ts +23 -27
  49. package/dist/core/runtime/broker-protocol.js +1 -1
  50. package/dist/core/runtime/broker-request.js +5 -11
  51. package/dist/core/runtime/broker.d.ts +21 -13
  52. package/dist/core/runtime/broker.js +151 -186
  53. package/dist/core/runtime/interactive-deliver.js +4 -5
  54. package/dist/core/runtime/model-swap.d.ts +2 -3
  55. package/dist/core/runtime/model-swap.js +3 -4
  56. package/dist/core/runtime/node-read.d.ts +20 -0
  57. package/dist/core/runtime/node-read.js +34 -1
  58. package/dist/core/runtime/resume-root.d.ts +1 -1
  59. package/dist/core/runtime/resume-root.js +6 -6
  60. package/dist/core/runtime/spawn.js +3 -3
  61. package/dist/core/session-model/session-state.d.ts +8 -6
  62. package/dist/core/session-model/session-state.js +6 -16
  63. package/dist/daemon/api/handlers/nodes.js +18 -1
  64. package/dist/daemon/manage.js +2 -2
  65. package/dist/index.d.ts +1 -1
  66. package/dist/pi-extensions/canvas-bash-valve.js +4 -3
  67. package/dist/web-client/assets/{index-DJhQZoAj.css → index-CpEl9LTS.css} +1 -1
  68. package/dist/web-client/assets/{index--SsQYcKu.js → index-CsuwzlcQ.js} +19 -19
  69. package/dist/web-client/index.html +2 -2
  70. package/dist/web-client/sw.js +1 -1
  71. package/docs/compat/hearth-crtr-v5.md +175 -0
  72. package/docs/public-api.md +2 -2
  73. package/package.json +2 -2
  74. package/runtime.lock.json +6 -6
  75. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +0 -1
  76. package/dist/core/__tests__/full/broker-control-preempt.test.js +0 -61
@@ -7,11 +7,10 @@
7
7
  // two-channel inbox path had (send rode the watcher's ~1.8s inject; stop rode a
8
8
  // direct abort frame; no shared ordering).
9
9
  //
10
- // Both ride the shared one-shot controller round-trip (broker-request.ts):
11
- // connect → `hello` + `request_control` (preempting any attached viewer for one
12
- // frame) → send the correlated frame → await its ack. A connection failure
13
- // BEFORE establishment surfaces as `BrokerUnreachableError` so the caller can
14
- // fall back to the durable path.
10
+ // Both ride the shared one-shot writable round-trip (broker-request.ts):
11
+ // connect → `hello` as controller → send the correlated frame → await its ack.
12
+ // A connection failure BEFORE establishment surfaces as `BrokerUnreachableError`
13
+ // so the caller can fall back to the durable path.
15
14
  import { oneShotControllerRequest } from './broker-request.js';
16
15
  /** Deliver `text` to a LIVE node's engine on its serialized frame loop. The
17
16
  * broker routes it itself (idle → prompt, streaming → steer) and acks once
@@ -3,9 +3,8 @@ import { type NodeMeta } from '../canvas/index.js';
3
3
  * AND its view.sock exists. Both are required — a dead pid with a stale socket,
4
4
  * or a live pid mid-boot before it binds the socket, is NOT a live broker. */
5
5
  export declare function isBrokerLive(meta: NodeMeta): boolean;
6
- /** Swap a LIVE node's model over its view.sock. Connects, claims controller
7
- * (hello + request_control, so an attached viewer never blocks the swap), sends
8
- * `set_model`, and resolves with the broker-resolved `provider/id` on its ack —
6
+ /** Swap a LIVE node's model over its view.sock. Connects as a controller,
7
+ * sends `set_model`, and resolves with the broker-resolved `provider/id` on its ack —
9
8
  * read back from the row the broker just persisted (model_changed.model carries
10
9
  * only the bare id). Rejects with the broker's message on an `error` frame (e.g.
11
10
  * `no model matching '<spec>' in the registry`), on connect failure, or timeout.
@@ -3,7 +3,7 @@
3
3
  // it survives revives. Two paths, chosen on broker liveness:
4
4
  //
5
5
  // LIVE — the node's broker is up (recorded pi_pid alive AND view.sock
6
- // present): connect to view.sock, claim controller, send `set_model`,
6
+ // present): connect to view.sock as a controller, send `set_model`,
7
7
  // await the ack. The broker's broadcastModelChanged already persists
8
8
  // the choice via persistModelChoice (model_override + launch.model), so
9
9
  // this path NEVER writes the recipe itself — it reads back the resolved
@@ -23,9 +23,8 @@ import { oneShotControllerRequest } from './broker-request.js';
23
23
  export function isBrokerLive(meta) {
24
24
  return isPidAlive(meta.pi_pid) && existsSync(viewSocketPath(meta.node_id));
25
25
  }
26
- /** Swap a LIVE node's model over its view.sock. Connects, claims controller
27
- * (hello + request_control, so an attached viewer never blocks the swap), sends
28
- * `set_model`, and resolves with the broker-resolved `provider/id` on its ack —
26
+ /** Swap a LIVE node's model over its view.sock. Connects as a controller,
27
+ * sends `set_model`, and resolves with the broker-resolved `provider/id` on its ack —
29
28
  * read back from the row the broker just persisted (model_changed.model carries
30
29
  * only the bare id). Rejects with the broker's message on an `error` frame (e.g.
31
30
  * `no model matching '<spec>' in the registry`), on connect failure, or timeout.
@@ -1,4 +1,24 @@
1
1
  import type { BrokerSnapshot } from './broker-protocol.js';
2
+ /** The node's conversation exactly as it ran: the raw session `.jsonl` bytes
3
+ * plus the assembled system prompt. Nothing here is interpreted — this is the
4
+ * faithful record (every session-tree entry, every cycle, every branch, every
5
+ * custom role), as opposed to `readNodeSnapshot`'s cycle-flattened
6
+ * `AgentMessage[]` reconstruction. */
7
+ export interface NodeSessionRead {
8
+ nodeId: string;
9
+ /** Absolute path of the session file the bytes came from. */
10
+ sessionFile: string;
11
+ /** Verbatim contents of the session `.jsonl`. */
12
+ sessionJsonl: string;
13
+ /** The assembled system prompt as last captured, or `null` if never written. */
14
+ systemPrompt: string | null;
15
+ }
16
+ /** Read a node's raw conversation record off disk — no broker, no attach, no
17
+ * mapping. The system prompt is NOT part of the session file (pi assembles it
18
+ * per boot), so it is read from the sidecar the broker captures on every
19
+ * rebind (`<jobDir>/system-prompt.md`), falling back to the one `launch.ts`
20
+ * writes at spawn (`<nodeDir>/system-prompt.md`). */
21
+ export declare function readNodeSession(nodeId: string): NodeSessionRead;
2
22
  export interface NodeSnapshotRead {
3
23
  nodeId: string;
4
24
  snapshot: BrokerSnapshot;
@@ -12,7 +12,7 @@ import { tmpdir } from 'node:os';
12
12
  import { basename, join } from 'node:path';
13
13
  import { InputError } from '../io.js';
14
14
  import { fullName, getNode } from '../canvas/index.js';
15
- import { sessionPtrPath } from '../canvas/paths.js';
15
+ import { jobDir, nodeDir, sessionPtrPath } from '../canvas/paths.js';
16
16
  import { SessionManager } from './broker-sdk.js';
17
17
  import { piAgentDir } from './package-health.js';
18
18
  import { BUILTIN_SLASH_COMMANDS } from './pi-vendored.js';
@@ -98,6 +98,39 @@ function statsFromMessages(messages, sessionId, sessionFile) {
98
98
  stats.tokens.total = stats.tokens.input + stats.tokens.output + stats.tokens.cacheRead + stats.tokens.cacheWrite;
99
99
  return stats;
100
100
  }
101
+ /** Read a node's raw conversation record off disk — no broker, no attach, no
102
+ * mapping. The system prompt is NOT part of the session file (pi assembles it
103
+ * per boot), so it is read from the sidecar the broker captures on every
104
+ * rebind (`<jobDir>/system-prompt.md`), falling back to the one `launch.ts`
105
+ * writes at spawn (`<nodeDir>/system-prompt.md`). */
106
+ export function readNodeSession(nodeId) {
107
+ const node = getNode(nodeId);
108
+ if (node === null) {
109
+ throw new InputError({ error: 'not_found', message: `no node: ${nodeId}`, field: 'id', next: 'List nodes with `crtr node inspect list`.' });
110
+ }
111
+ const sessionFile = resolveSessionFile(nodeId, node.pi_session_file);
112
+ if (sessionFile === null) {
113
+ throw new InputError({ error: 'no_session', message: `node has no readable session file: ${nodeId}`, field: 'id', next: 'Choose a node with captured conversation history.' });
114
+ }
115
+ return {
116
+ nodeId,
117
+ sessionFile,
118
+ // A live broker appends to this file; a torn read would truncate the last
119
+ // line, so the caller gets whatever was complete at read time (the same
120
+ // at-rest guarantee `readNodeSnapshot`'s copy has).
121
+ sessionJsonl: readFileSync(sessionFile, 'utf8'),
122
+ systemPrompt: readIfPresent(join(jobDir(nodeId), 'system-prompt.md'))
123
+ ?? readIfPresent(join(nodeDir(nodeId), 'system-prompt.md')),
124
+ };
125
+ }
126
+ function readIfPresent(path) {
127
+ try {
128
+ return readFileSync(path, 'utf8');
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
101
134
  /** Reconstruct a persisted session without launching its broker. Backs the
102
135
  * machine-readable snapshot endpoint and the human transcript render. */
103
136
  export async function readNodeSnapshot(nodeId) {
@@ -1,5 +1,5 @@
1
1
  /** Resolve a front-door resume: pick a target root (MRU or via picker), then
2
- * take over this terminal to view it. On success it execs the viewer inline and
2
+ * open this terminal to view it. On success it execs the viewer inline and
3
3
  * never returns. Otherwise reports why it did not: `'empty'` (no resumable root
4
4
  * here → caller boots a fresh one) or `'aborted'` (the picker was dismissed →
5
5
  * caller exits without surprise-booting). */
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // A "session" in the Claude-Code sense is a crtr ROOT node: a durable pi
10
10
  // conversation pinned to a cwd that never dies, only goes dormant. So resuming
11
- // is reviving the last root here and making THIS pane its controller-viewer —
11
+ // is reviving the last root here and making THIS pane its writable viewer —
12
12
  // mirroring the front-door boot tail (register the pane as the viewer focus,
13
13
  // then exec `crtr surface attach` inline). cwd is the key so it never teleports
14
14
  // across projects.
@@ -50,7 +50,7 @@ async function listResumableRoots(cwd) {
50
50
  .sort((a, b) => (a.created < b.created ? 1 : a.created > b.created ? -1 : 0)); // created DESC = MRU
51
51
  }
52
52
  /** Resolve a front-door resume: pick a target root (MRU or via picker), then
53
- * take over this terminal to view it. On success it execs the viewer inline and
53
+ * open this terminal to view it. On success it execs the viewer inline and
54
54
  * never returns. Otherwise reports why it did not: `'empty'` (no resumable root
55
55
  * here → caller boots a fresh one) or `'aborted'` (the picker was dismissed →
56
56
  * caller exits without surprise-booting). */
@@ -70,8 +70,8 @@ export async function resumeRoot(cwd, pick) {
70
70
  await attachInline(target.node_id);
71
71
  return 'aborted'; // unreachable — attachInline execs and exits
72
72
  }
73
- /** Register THIS pane as the target root's one viewer, then exec `crtr surface
74
- * attach` inline so this terminal becomes the controller-viewer. The attach's
73
+ /** Register THIS pane as the target root's foreground viewer, then exec
74
+ * `crtr surface attach` inline so this terminal becomes a writable viewer. The attach's
75
75
  * own ensureAttach revives the dormant broker + waits for its view socket
76
76
  * server-side, so this does NOT revive in-process. Mirrors the front-door boot
77
77
  * tail. Does not return. */
@@ -83,10 +83,10 @@ async function attachInline(nodeId) {
83
83
  installTmuxBindings();
84
84
  }
85
85
  catch { /* best-effort */ }
86
- // Make THIS pane the node's one viewer. A dormant root's prior focus row
86
+ // Make THIS pane the node's foreground viewer. A dormant root's prior focus row
87
87
  // points at a stale/detached pane (the front door keeps the row across a
88
88
  // clean detach), so drop it and claim the current pane — the exact --new-pane
89
- // discipline of registerViewerFocus (UNIQUE(node_id): one viewer per node),
89
+ // discipline of registerViewerFocus,
90
90
  // routed through crtrd since it is a canvas write.
91
91
  const here = currentTmux();
92
92
  if (here !== null) {
@@ -302,8 +302,8 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
302
302
  }
303
303
  // (The three scoped long-term memory stores are seeded for EVERY node at birth
304
304
  // in spawnNode — no orchestrator-gated seeding needed here.)
305
- // An ATTENDED --root spawned from inside tmux opens its one viewer in the
306
- // CALLER'S CURRENT session (below), so it appears where the spawner is
305
+ // An ATTENDED --root spawned from inside tmux opens its foreground viewer
306
+ // in the CALLER'S CURRENT session (below), so it appears where the spawner is
307
307
  // working; a managed child is a headless broker with NO viewer at all, and
308
308
  // so is an unattended terminal root (nobody asked to watch it).
309
309
  const here = attendedRoot ? currentTmux() : null;
@@ -318,7 +318,7 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
318
318
  // Broker is the only host: launch the detached broker ENGINE. A managed child
319
319
  // gets NO viewer — spawning it returns once the engine is launched and a human
320
320
  // opens a viewer on demand via `crtr surface node focus` / surface attach. A --root gets
321
- // one foreground viewer below.
321
+ // a foreground viewer below.
322
322
  const placed = launchBrokerOrCrash(meta.node_id, inv, {
323
323
  cwd: meta.cwd,
324
324
  name: fullName(meta),
@@ -12,10 +12,10 @@ export interface DisplayState {
12
12
  title: string | undefined;
13
13
  }
14
14
  export interface SessionState {
15
- /** This viewer's broker-assigned role. */
15
+ /** This viewer's fixed role, stated by the broker in `welcome`. `controller`
16
+ * means writable and `observer` means read-only; the broker never reassigns
17
+ * it, so nothing after `welcome` can change this field. */
16
18
  role: ClientRole;
17
- /** The client_id currently holding control, or null when nobody does. */
18
- controllerId: string | null;
19
19
  /** The broker process's pi agent dir, from `welcome` — lets a viewer point an
20
20
  * AuthStorage/ModelRegistry at the SAME auth.json. */
21
21
  agentDir: string | undefined;
@@ -29,8 +29,10 @@ export interface SessionState {
29
29
  * this rather than leaving a dead broker's rows on screen. */
30
30
  queued: string[];
31
31
  display: DisplayState;
32
- /** The blocking dialog awaiting an answer, or null. Only a controller is ever
33
- * sent one; `welcome.pending_dialog` carries one in flight at attach time, and
32
+ /** The blocking dialog awaiting an answer, or null. Only a writable client is
33
+ * ever sent one, and every writable client is sent the SAME one — the first
34
+ * answer wins and the broker dismisses the rest by id.
35
+ * `welcome.pending_dialog` carries one in flight at attach time, and
34
36
  * a welcome is a COMPLETE catch-up, so a welcome naming none clears it.
35
37
  * (The terminal viewer does not read this field: its overlay is owned by
36
38
  * `InputController`, which holds a live TUI handle rather than data. The field
@@ -46,4 +48,4 @@ export declare function initialSessionState(role?: ClientRole): SessionState;
46
48
  export declare function applySnapshot(frame: WelcomeFrame): SessionState;
47
49
  /** Fold one broker frame into the session state. Pure and total: a frame that
48
50
  * carries no session state returns `state` UNCHANGED (same reference). */
49
- export declare function reduce(state: SessionState, frame: BrokerToClient, clientId: string): SessionState;
51
+ export declare function reduce(state: SessionState, frame: BrokerToClient): SessionState;
@@ -11,9 +11,10 @@
11
11
  // SESSION state = anything derived SOLELY from what the broker sent.
12
12
  // VIEWER state = anything derived from local input or this terminal/tab.
13
13
  //
14
- // So `role` belongs here even though it differs per viewer (it is BROKER-assigned
15
- // — "identical across viewers" would get it wrong), while a transient notice, a
16
- // connection phase, a correlation id this viewer minted, and the transcript
14
+ // So `role` belongs here even though it differs per viewer (the broker fixes it
15
+ // from this viewer's `hello` and never changes it — "identical across viewers"
16
+ // would get it wrong), while a transient notice, a connection phase, a
17
+ // correlation id this viewer minted, and the transcript
17
18
  // RENDERING all stay with the viewer. The transcript itself is deliberately out
18
19
  // of scope: the terminal accumulates into a scrollback and the web replaces
19
20
  // wholesale, and that difference is legitimate.
@@ -26,7 +27,6 @@ const EMPTY_DISPLAY = { statuses: {}, widgets: {}, title: undefined };
26
27
  export function initialSessionState(role = 'observer') {
27
28
  return {
28
29
  role,
29
- controllerId: null,
30
30
  agentDir: undefined,
31
31
  engine: undefined,
32
32
  contextTokens: undefined,
@@ -48,7 +48,6 @@ export function applySnapshot(frame) {
48
48
  const contextUsage = snapshot.stats.contextUsage;
49
49
  return {
50
50
  role: frame.role,
51
- controllerId: frame.controller_id,
52
51
  agentDir: frame.agentDir,
53
52
  engine: snapshot.state,
54
53
  contextTokens: contextUsage?.tokens ?? undefined,
@@ -63,25 +62,16 @@ export function applySnapshot(frame) {
63
62
  }
64
63
  /** Fold one broker frame into the session state. Pure and total: a frame that
65
64
  * carries no session state returns `state` UNCHANGED (same reference). */
66
- export function reduce(state, frame, clientId) {
65
+ export function reduce(state, frame) {
67
66
  switch (frame.type) {
68
67
  case 'welcome':
69
68
  return applySnapshot(frame);
70
- case 'control_changed': {
71
- if (state.controllerId === frame.controller_id)
72
- return state;
73
- return {
74
- ...state,
75
- controllerId: frame.controller_id,
76
- role: frame.controller_id === clientId ? 'controller' : 'observer',
77
- };
78
- }
79
69
  case 'model_changed':
80
70
  return patchEngine(state, { model: frame.model });
81
71
  case 'extension_ui_request': {
82
72
  // pi's uniform envelope: the three display METHODS are levels this model
83
73
  // retains; every other method is a dialog (a new one supersedes any
84
- // showing one — a control handoff re-routes it).
74
+ // showing one).
85
75
  switch (frame.method) {
86
76
  case 'setStatus': {
87
77
  const current = state.display.statuses[frame.statusKey];
@@ -13,7 +13,7 @@ import { childrenOf, subscribersOf, subscriptionsOf, setMessageWait, updateNode
13
13
  import { subtreeIds } from '../../../core/canvas/nav-model.js';
14
14
  import { contextDir, reportsDir } from '../../../core/canvas/paths.js';
15
15
  import { nodeArtifacts } from '../../../core/canvas/history.js';
16
- import { readNodeSnapshot, transcriptMarkdown } from '../../../core/runtime/node-read.js';
16
+ import { readNodeSession, readNodeSnapshot, transcriptMarkdown } from '../../../core/runtime/node-read.js';
17
17
  import { reviveAll } from '../../../core/runtime/revive-all.js';
18
18
  import { promote, requestYield } from '../../../core/runtime/promote.js';
19
19
  import { recycleNode } from '../../../core/runtime/recycle.js';
@@ -233,6 +233,22 @@ async function handleSnapshot(ctx) {
233
233
  };
234
234
  return { status: 200, body };
235
235
  }
236
+ /** The faithful export read: raw `.jsonl` bytes + assembled system prompt.
237
+ * `/snapshot` reconstructs a cycle-flattened `AgentMessage[]` for renderers and
238
+ * drops what a renderer cannot show; this drops nothing. */
239
+ function handleSession(ctx) {
240
+ const id = ctx.params['id'];
241
+ requireMeta(id);
242
+ const read = readNodeSession(id);
243
+ const body = {
244
+ node_id: id,
245
+ session_file: read.sessionFile,
246
+ session_jsonl: read.sessionJsonl,
247
+ system_prompt: read.systemPrompt,
248
+ captured_at: nowIso(),
249
+ };
250
+ return { status: 200, body };
251
+ }
236
252
  async function handleTranscript(ctx) {
237
253
  const id = ctx.params['id'];
238
254
  requireMeta(id);
@@ -507,6 +523,7 @@ export const nodeRoutes = [
507
523
  { method: 'POST', pattern: '/v1/nodes/revive-all', handler: () => handleReviveAll() },
508
524
  { method: 'GET', pattern: '/v1/nodes/:id', handler: handleDetail },
509
525
  { method: 'GET', pattern: '/v1/nodes/:id/snapshot', handler: handleSnapshot },
526
+ { method: 'GET', pattern: '/v1/nodes/:id/session', handler: handleSession },
510
527
  { method: 'GET', pattern: '/v1/nodes/:id/transcript', handler: handleTranscript },
511
528
  { method: 'GET', pattern: '/v1/nodes/:id/context', handler: handleContext },
512
529
  { method: 'GET', pattern: '/v1/nodes/:id/artifacts', handler: handleArtifacts },
@@ -50,8 +50,8 @@ export const DAEMON_ENV_STRIP_KEYS = [
50
50
  // reads `currentTmux()` for a --root's foreground viewer placement; a stale
51
51
  // inherited $TMUX makes that resolve to an arbitrary "current" pane, so a
52
52
  // front-door root (created through crtrd) opens a SECOND viewer window and
53
- // focuses it — stealing the controller role from the front door's own inline
54
- // attach, which is then demoted to a read-only observer in the user's pane.
53
+ // focuses it — stealing focus from the front door's own inline attach in the
54
+ // user's pane.
55
55
  // Strip them so the daemon is truly paneless and leaves root-viewer placement
56
56
  // to the front door. Explicit `-t <target>` tmux ops (focus/window mgmt) are
57
57
  // unaffected — the tmux CLI reaches the server via its default socket.
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export { readTelemetry, readContextTokens } from './core/canvas/telemetry.js';
14
14
  export type { Telemetry } from './core/canvas/telemetry.js';
15
15
  export { BrokerClient, BrokerUnavailableError } from './core/broker-client/index.js';
16
16
  export { encodeFrame, FrameDecoder, FrameOverflowError, CLIENT_READ_CAPS, BROKER_READ_CAPS, } from './core/runtime/broker-protocol.js';
17
- export type { FrameDecoderCaps, BrokerSnapshot, ClientRole, WelcomeFrame, ControlChangedFrame, AckFrame, ErrorFrame, ExtensionUIRequestFrame, BrokerToClient, HelloFrame, PromptFrame, SteerFrame, FollowUpFrame, AbortFrame, RequestControlFrame, ReleaseControlFrame, ByeFrame, ShutdownFrame, SetModelFrame, CycleModelFrame, SetThinkingLevelFrame, SetAutoRetryFrame, SetAutoCompactionFrame, CompactFrame, NewSessionFrame, SwitchSessionFrame, ForkFrame, SetSessionNameFrame, GetCommandsFrame, NavigateTreeFrame, ReloadFrame, ExportFrame, ClientToBroker, RpcExtensionUIRequest, RpcExtensionUIResponse, ExtensionUIResponseFrame, } from './core/runtime/broker-protocol.js';
17
+ export type { FrameDecoderCaps, BrokerSnapshot, ClientRole, WelcomeFrame, AckFrame, ErrorFrame, ExtensionUIRequestFrame, BrokerToClient, HelloFrame, PromptFrame, SteerFrame, FollowUpFrame, AbortFrame, ByeFrame, ShutdownFrame, SetModelFrame, CycleModelFrame, SetThinkingLevelFrame, SetAutoRetryFrame, SetAutoCompactionFrame, CompactFrame, NewSessionFrame, SwitchSessionFrame, ForkFrame, SetSessionNameFrame, GetCommandsFrame, NavigateTreeFrame, ReloadFrame, ExportFrame, ClientToBroker, RpcExtensionUIRequest, RpcExtensionUIResponse, ExtensionUIResponseFrame, } from './core/runtime/broker-protocol.js';
18
18
  export type { SessionStats } from '@earendil-works/pi-coding-agent';
19
19
  export { general } from './core/errors.js';
20
20
  export type { CrtrError } from './core/errors.js';
@@ -1,5 +1,6 @@
1
1
  // canvas-bash-valve.ts — the bash safety valve: auto-background hanging
2
- // commands, alert via inbox.
2
+ // commands, alert via inbox at tier `urgent` so completion lands as a steering
3
+ // message mid-turn rather than queuing behind the current turn.
3
4
  //
4
5
  // Loaded into every canvas node's pi process via the node's launch.extensions
5
6
  // list. INERT when CRTR_NODE_ID is absent (plain pi session or legacy job
@@ -58,7 +59,7 @@ const SUPERVISOR_SCRIPT = [
58
59
  'ec=$?',
59
60
  'echo "$ec" > "$3"',
60
61
  'if [ -f "$4" ]; then',
61
- ' crtr node message send "Background bash job $5 finished (exit $ec). Log: $2 Command: $1" --to "$6" --tier normal || true',
62
+ ' crtr node message send "Background bash job $5 finished (exit $ec). Log: $2 Command: $1" --to "$6" --tier urgent || true',
62
63
  'fi',
63
64
  ].join('\n');
64
65
  function makeJobPaths(contextDir) {
@@ -215,7 +216,7 @@ export function createValveOperations(nodeId, contextDir) {
215
216
  function background(auto = false) {
216
217
  const elapsed = formatBashElapsed(Date.now() - startedAt);
217
218
  const notice = `\n[backgrounded as job ${paths.jobId} after ${elapsed} in the foreground${auto ? ' (auto)' : ''}. ` +
218
- `You'll get an inbox message when it exits. ` +
219
+ `It will interrupt you with a message when it exits. ` +
219
220
  `Watch: tail -f ${paths.jobLog} ; stop: kill -- -${pgid}]`;
220
221
  onData(Buffer.from(notice));
221
222
  finish(() => resolve({ exitCode: 0 }));