@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.
- package/dist/api/client.d.ts +4 -1
- package/dist/api/client.js +5 -0
- package/dist/api/dto/broker.d.ts +1 -1
- package/dist/api/dto/nodes.d.ts +15 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-views/chat/core.mjs +6 -51
- package/dist/builtin-views/chat/tui.mjs +6 -14
- package/dist/builtin-views/chat/web.jsx +2 -7
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +1 -1
- package/dist/clients/attach/chrome/roster.d.ts +1 -1
- package/dist/clients/attach/chrome/roster.js +9 -1
- package/dist/clients/attach/command.js +5 -5
- package/dist/clients/attach/input/controller.d.ts +23 -8
- package/dist/clients/attach/input/controller.js +59 -29
- package/dist/clients/attach/overlays/dialogs.d.ts +1 -2
- package/dist/clients/attach/overlays/graph.d.ts +4 -1
- package/dist/clients/attach/overlays/graph.js +25 -7
- package/dist/clients/attach/session/context.d.ts +7 -0
- package/dist/clients/attach/session/frames.js +1 -8
- package/dist/clients/attach/session/input-wiring.d.ts +1 -1
- package/dist/clients/attach/session/input-wiring.js +21 -11
- package/dist/clients/attach/session/mode.d.ts +4 -3
- package/dist/clients/attach/session/mode.js +6 -1
- package/dist/clients/attach/session/reconnect.d.ts +3 -3
- package/dist/clients/attach/session/reconnect.js +6 -7
- package/dist/clients/attach/session/state-sync.d.ts +1 -1
- package/dist/clients/attach/session/state-sync.js +2 -2
- package/dist/clients/attach/slash/dispatch.d.ts +13 -1
- package/dist/clients/attach/slash/dispatch.js +65 -17
- package/dist/clients/attach/viewer.js +523 -523
- package/dist/clients/web/web-client/shared/protocol.d.ts +5 -3
- package/dist/commands/node.js +1 -1
- package/dist/core/__tests__/broker-sdk-wiring.test.js +18 -18
- package/dist/core/__tests__/chat-view-reconnect.test.js +44 -23
- package/dist/core/__tests__/full/broker-attach-limits.test.js +60 -36
- package/dist/core/__tests__/full/broker-attach-stream.test.js +4 -4
- package/dist/core/__tests__/full/broker-dialogs.test.js +121 -62
- package/dist/core/__tests__/helpers/broker-clients.js +2 -2
- package/dist/core/__tests__/session-model.test.js +15 -26
- 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 +0 -2
- package/dist/core/runtime/auth-reload.d.ts +4 -4
- package/dist/core/runtime/auth-reload.js +9 -13
- package/dist/core/runtime/boot-root.d.ts +2 -2
- package/dist/core/runtime/boot-root.js +7 -7
- package/dist/core/runtime/broker-protocol.d.ts +23 -27
- package/dist/core/runtime/broker-protocol.js +1 -1
- package/dist/core/runtime/broker-request.js +5 -11
- package/dist/core/runtime/broker.d.ts +21 -13
- package/dist/core/runtime/broker.js +151 -186
- package/dist/core/runtime/interactive-deliver.js +4 -5
- package/dist/core/runtime/model-swap.d.ts +2 -3
- package/dist/core/runtime/model-swap.js +3 -4
- package/dist/core/runtime/node-read.d.ts +20 -0
- package/dist/core/runtime/node-read.js +34 -1
- package/dist/core/runtime/resume-root.d.ts +1 -1
- package/dist/core/runtime/resume-root.js +6 -6
- package/dist/core/runtime/spawn.js +3 -3
- package/dist/core/session-model/session-state.d.ts +8 -6
- package/dist/core/session-model/session-state.js +6 -16
- package/dist/daemon/api/handlers/nodes.js +18 -1
- package/dist/daemon/manage.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/pi-extensions/canvas-bash-valve.js +4 -3
- package/dist/web-client/assets/{index-DJhQZoAj.css → index-CpEl9LTS.css} +1 -1
- package/dist/web-client/assets/{index--SsQYcKu.js → index-CsuwzlcQ.js} +19 -19
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/docs/compat/hearth-crtr-v5.md +175 -0
- package/docs/public-api.md +2 -2
- package/package.json +2 -2
- package/runtime.lock.json +6 -6
- package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +0 -1
- 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
|
|
11
|
-
// connect → `hello`
|
|
12
|
-
//
|
|
13
|
-
//
|
|
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
|
|
7
|
-
*
|
|
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
|
|
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
|
|
27
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
74
|
-
* attach` inline so this terminal becomes
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
33
|
-
* sent one
|
|
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
|
|
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 (
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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
|
|
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
|
|
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 },
|
package/dist/daemon/manage.js
CHANGED
|
@@ -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
|
|
54
|
-
//
|
|
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,
|
|
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
|
|
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
|
-
`
|
|
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 }));
|