@north-light/crouter 0.3.258 → 0.3.259

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 (30) hide show
  1. package/dist/api/dto/attach.d.ts +6 -0
  2. package/dist/api/dto/nodes.d.ts +15 -0
  3. package/dist/clients/attach/command.js +3 -1
  4. package/dist/clients/attach/input/capabilities.d.ts +3 -0
  5. package/dist/clients/attach/input/capabilities.js +7 -3
  6. package/dist/clients/attach/input/controller.d.ts +1 -1
  7. package/dist/clients/attach/input/controller.js +31 -17
  8. package/dist/clients/attach/session/connection.d.ts +12 -3
  9. package/dist/clients/attach/session/connection.js +40 -25
  10. package/dist/clients/attach/session/input-wiring.d.ts +1 -1
  11. package/dist/clients/attach/session/input-wiring.js +6 -2
  12. package/dist/clients/attach/session/reconnect.d.ts +7 -3
  13. package/dist/clients/attach/session/reconnect.js +13 -2
  14. package/dist/clients/attach/slash/dispatch.js +1 -1
  15. package/dist/clients/attach/viewer.d.ts +1 -1
  16. package/dist/clients/attach/viewer.js +689 -689
  17. package/dist/commands/surface/node/cycle.js +2 -2
  18. package/dist/commands/surface/node/focus.js +11 -11
  19. package/dist/commands/surface/node/placement.js +14 -5
  20. package/dist/core/broker-client/client.d.ts +4 -5
  21. package/dist/core/broker-client/client.js +9 -5
  22. package/dist/core/canvas/browse/model.d.ts +3 -3
  23. package/dist/core/canvas/browse/model.js +3 -3
  24. package/dist/core/canvas/browse/render.js +3 -3
  25. package/dist/core/runtime/revive.d.ts +19 -6
  26. package/dist/core/runtime/revive.js +34 -9
  27. package/dist/daemon/api/handlers/attach.js +24 -20
  28. package/dist/daemon/api/handlers/nodes.js +7 -0
  29. package/package.json +1 -1
  30. package/runtime.lock.json +2 -2
@@ -5,6 +5,9 @@ export interface AttachEnsureRequest {
5
5
  resume?: boolean;
6
6
  /** Revive if dormant (default true); false returns a possibly-dead socket. */
7
7
  revive?: boolean;
8
+ /** Wake an exact unfinalized parked resident (default true). Focus/navigation
9
+ * sets false so it can open the saved conversation without launching. */
10
+ wakeParked?: boolean;
8
11
  }
9
12
  /** Result of a local attach-ensure. */
10
13
  export interface AttachEnsureResultDTO {
@@ -13,4 +16,7 @@ export interface AttachEnsureResultDTO {
13
16
  socket_path: string;
14
17
  revived: boolean;
15
18
  resumed: boolean;
19
+ /** True when a non-waking focus ensure found an exact parked resident with no
20
+ * live broker. `socket_path` is then the target the viewer will later redial. */
21
+ parked: boolean;
16
22
  }
@@ -239,8 +239,23 @@ export interface NodeSnapshotDTO {
239
239
  node_id: NodeIdDTO;
240
240
  snapshot: {
241
241
  messages: unknown[];
242
+ messageIds?: string[];
242
243
  stats: unknown;
243
244
  state: Record<string, unknown>;
245
+ display: {
246
+ statuses: Record<string, string>;
247
+ widgets: Record<string, {
248
+ lines: string[];
249
+ placement: 'aboveEditor' | 'belowEditor';
250
+ }>;
251
+ title?: string;
252
+ };
253
+ queued?: {
254
+ steering: string[];
255
+ followUp: string[];
256
+ };
257
+ toolGroupSummaries?: Record<string, unknown>;
258
+ workingActivity?: string;
244
259
  };
245
260
  commands: {
246
261
  name: string;
@@ -60,6 +60,8 @@ function attachToLeaf(namespace) {
60
60
  const nodeId = input['node'];
61
61
  const observer = input['observer'] ?? false;
62
62
  const canvasName = input['canvas'];
63
+ const wakeParked = process.env['CRTR_ATTACH_WAKE_PARKED'] !== '0';
64
+ delete process.env['CRTR_ATTACH_WAKE_PARKED'];
63
65
  // attach IS the in-pane interactive program — it only needs a TTY (crtr is
64
66
  // tmux-only, but the pane itself is the surface). Non-TTY (piped) → static
65
67
  // notice + exit, never a non-tmux interactive fallback. Guarded BEFORE the
@@ -73,7 +75,7 @@ function attachToLeaf(namespace) {
73
75
  mark('viewer.bundle_import_start');
74
76
  const { runAttach } = await import('./viewer.js');
75
77
  mark('viewer.bundle_loaded');
76
- await runAttach(nodeId, observer, canvasName);
78
+ await runAttach(nodeId, observer, canvasName, wakeParked);
77
79
  // The session has sent its final broker frame and restored the terminal.
78
80
  // Force this one-shot viewer process out now: pi's bundled UI dependencies
79
81
  // can retain libuv handles after their visible work is finished. The exit
@@ -16,6 +16,9 @@ export interface ViewerCapabilityInput {
16
16
  remote: boolean;
17
17
  nodeId: string;
18
18
  profileId: string | undefined;
19
+ /** Dynamic transport state. Authorization stays role-based; broker-only
20
+ * operations refuse locally while a parked viewer has no live engine. */
21
+ isBrokerOffline?: () => boolean;
19
22
  }
20
23
  /** Build the one authority for a viewer session. Remote viewers are observers
21
24
  * even when a caller supplied a controller role; keep that invariant local to
@@ -25,14 +25,18 @@ export function createViewerCapabilities(input) {
25
25
  permittedOperations,
26
26
  permits: (operation) => permittedOperations.has(operation),
27
27
  refusal: (operation, command) => {
28
+ const prefix = command === undefined ? '' : `/${command} `;
29
+ if (input.isBrokerOffline?.() === true
30
+ && (operation === 'engine-drive' || operation === 'dequeue' || operation === 'auth')) {
31
+ return `${prefix}needs a live broker; this node is parked — submit a message to wake it`;
32
+ }
28
33
  if (permittedOperations.has(operation))
29
34
  return undefined;
30
- const prefix = command === undefined ? '' : `/${command} `;
31
35
  if (remote && (operation === 'canvas-resume' || operation === 'local-canvas-inspect' || operation === 'canvas-inbox')) {
32
- return `${prefix}is not available for a remote attach (--canvas) — it acts on this machine's local canvas`;
36
+ return `${prefix}Read-only remote attach — not available with --canvas because it acts on this machine's local canvas`;
33
37
  }
34
38
  if (remote)
35
- return `${prefix}is not available for a remote attach (--canvas) — view-only`;
39
+ return `${prefix}Read-only remote attach — not available with --canvas (view-only)`;
36
40
  return `${prefix}is not available to a read-only observer attach (--observer) — it would drive this node`;
37
41
  },
38
42
  };
@@ -61,7 +61,7 @@ export declare class InputController {
61
61
  openAuthPicker(prepare: Parameters<InputOverlayOwner['openAuthPicker']>[0]): Promise<void>;
62
62
  private readonly send;
63
63
  private emitCommand;
64
- private readOnlyLabel;
64
+ private openBrokerPicker;
65
65
  private wire;
66
66
  private slashContext;
67
67
  /** Record a leading slash command as used. Called only once the command has
@@ -69,15 +69,21 @@ export class InputController {
69
69
  };
70
70
  emitCommand(frame) {
71
71
  const operation = viewerOperationForFrame(frame);
72
- if (operation === undefined || this.capabilities.permits(operation)) {
72
+ const refusal = operation === undefined ? undefined : this.capabilities.refusal(operation);
73
+ if (refusal === undefined) {
73
74
  this.hooks.onCommand(frame);
74
75
  return true;
75
76
  }
76
- this.notify(`${this.readOnlyLabel()} — cannot drive the engine`);
77
+ this.notify(refusal);
77
78
  return false;
78
79
  }
79
- readOnlyLabel() {
80
- return this.capabilities.remote ? 'Read-only remote attach' : 'Read-only observer attach';
80
+ openBrokerPicker(open) {
81
+ const refusal = this.capabilities.refusal('engine-drive');
82
+ if (refusal !== undefined) {
83
+ this.notify(refusal);
84
+ return;
85
+ }
86
+ open();
81
87
  }
82
88
  wire() {
83
89
  this.editor.onSubmit = (text) => this.handleSubmit(text);
@@ -91,7 +97,7 @@ export class InputController {
91
97
  const now = Date.now();
92
98
  if (now - this.lastEscapeAt < DOUBLE_ESCAPE_MS) {
93
99
  this.lastEscapeAt = 0;
94
- void this.overlays.openTreePicker();
100
+ this.openBrokerPicker(() => void this.overlays.openTreePicker());
95
101
  }
96
102
  else {
97
103
  this.lastEscapeAt = now;
@@ -109,10 +115,10 @@ export class InputController {
109
115
  this.editor.onAction('app.tools.expand', () => this.hooks.onToggleToolsExpand?.());
110
116
  this.editor.onAction('app.thinking.toggle', () => this.hooks.onToggleThinking?.());
111
117
  this.editor.onAction('app.editor.external', () => void this.openExternalEditor());
112
- this.editor.onAction('app.model.select', () => void this.overlays.openModelPicker());
113
- this.editor.onAction('app.session.tree', () => void this.overlays.openTreePicker());
114
- this.editor.onAction('app.session.fork', () => void this.overlays.openForkPicker());
115
- this.editor.onAction('app.session.resume', () => void this.overlays.openSessionPicker());
118
+ this.editor.onAction('app.model.select', () => this.openBrokerPicker(() => void this.overlays.openModelPicker()));
119
+ this.editor.onAction('app.session.tree', () => this.openBrokerPicker(() => void this.overlays.openTreePicker()));
120
+ this.editor.onAction('app.session.fork', () => this.openBrokerPicker(() => void this.overlays.openForkPicker()));
121
+ this.editor.onAction('app.session.resume', () => this.openBrokerPicker(() => void this.overlays.openSessionPicker()));
116
122
  }
117
123
  slashContext() {
118
124
  return {
@@ -131,12 +137,12 @@ export class InputController {
131
137
  onColor: this.hooks.onColor,
132
138
  onNodeMetadata: this.hooks.onNodeMetadata,
133
139
  onFoldTools: this.hooks.onFoldTools,
134
- openModelPicker: () => void this.overlays.openModelPicker(),
135
- openSessionPicker: () => void this.overlays.openSessionPicker(),
136
- openForkPicker: () => void this.overlays.openForkPicker(),
137
- openTreePicker: () => void this.overlays.openTreePicker(),
138
- openSettingsPicker: () => void this.overlays.openSettingsPicker(),
139
- openScopedModelsPicker: () => void this.overlays.openScopedModelsPicker(),
140
+ openModelPicker: () => this.openBrokerPicker(() => void this.overlays.openModelPicker()),
141
+ openSessionPicker: () => this.openBrokerPicker(() => void this.overlays.openSessionPicker()),
142
+ openForkPicker: () => this.openBrokerPicker(() => void this.overlays.openForkPicker()),
143
+ openTreePicker: () => this.openBrokerPicker(() => void this.overlays.openTreePicker()),
144
+ openSettingsPicker: () => this.openBrokerPicker(() => void this.overlays.openSettingsPicker()),
145
+ openScopedModelsPicker: () => this.openBrokerPicker(() => void this.overlays.openScopedModelsPicker()),
140
146
  openMcpPicker: (serverName) => this.overlays.openMcpPicker(serverName),
141
147
  openLoginPicker: this.hooks.openLoginPicker,
142
148
  openLogoutPicker: this.hooks.openLogoutPicker,
@@ -188,6 +194,13 @@ export class InputController {
188
194
  return;
189
195
  }
190
196
  const isDormant = this.hooks.isDormant?.() === true;
197
+ // Unknown slash commands and `!` bash are broker operations, not human
198
+ // conversation input. Keep their text and refuse visibly while offline;
199
+ // only an ordinary submitted message crosses the durable wake boundary.
200
+ if (isDormant && (trimmed.startsWith('/') || trimmed.startsWith('!'))) {
201
+ this.notify('This node is parked — submit a message to wake it; broker commands need a live engine');
202
+ return;
203
+ }
191
204
  const classification = classifySubmit(text, {
192
205
  isDormant,
193
206
  canWakeDormant: isDormant && this.capabilities.permits('wake-dormant') && this.hooks.onDormantSubmit !== undefined,
@@ -240,8 +253,9 @@ export class InputController {
240
253
  this.clearPastedImages();
241
254
  }
242
255
  async handleDequeue() {
243
- if (!this.capabilities.permits('dequeue')) {
244
- this.notify(`${this.readOnlyLabel()} — cannot restore queued messages`);
256
+ const refusal = this.capabilities.refusal('dequeue');
257
+ if (refusal !== undefined) {
258
+ this.notify(refusal);
245
259
  return;
246
260
  }
247
261
  const request = this.hooks.onRequest;
@@ -1,4 +1,4 @@
1
- import type { ClientRole } from '../../../core/runtime/broker-protocol.js';
1
+ import type { BrokerSnapshot, ClientRole } from '../../../core/runtime/broker-protocol.js';
2
2
  import type { CanvasSource } from '../../../core/canvas/source.js';
3
3
  import type { NodeMeta } from '../../../core/canvas/types.js';
4
4
  import { BrokerClient } from '../../../core/broker-client/index.js';
@@ -31,8 +31,17 @@ export interface AttachConnection {
31
31
  /** The attached node's persona/lifecycle, when this canvas can tell us —
32
32
  * `undefined` for a remote attach until the viewer refetches it. */
33
33
  readonly nodeIdentity: Pick<NodeMeta, 'kind' | 'mode' | 'lifecycle' | 'icon'> | undefined;
34
- /** The one socket to the node's headless broker, already connected. */
34
+ /** The one socket target for the node's headless broker. It is intentionally
35
+ * disconnected only when focus opened an exact parked resident offline. */
35
36
  readonly socket: BrokerClient;
37
+ /** Saved snapshot used to seed an offline parked viewer. Absent for direct
38
+ * attach and for focus paths whose broker was live or legitimately woke. */
39
+ readonly initialSnapshot?: BrokerSnapshot;
40
+ readonly initialCommands?: ReadonlyArray<{
41
+ name: string;
42
+ description: string;
43
+ source: string;
44
+ }>;
36
45
  /** Re-throw a connect-adjacent failure that landed AFTER `connect` resolved.
37
46
  * Call once, synchronously, just before permanent socket wiring takes over;
38
47
  * a no-op if nothing failed. See the never-removed error listener below. */
@@ -41,4 +50,4 @@ export interface AttachConnection {
41
50
  /** Resolve the target, revive the node if it is dormant (server-side), and dial
42
51
  * the broker. Rejects with a plain `InputError` — no terminal has been taken
43
52
  * over at this point, so a failure here is an ordinary non-zero CLI exit. */
44
- export declare function connectToNode(nodeId: string, canvasName?: string): Promise<AttachConnection>;
53
+ export declare function connectToNode(nodeId: string, canvasName?: string, wakeParked?: boolean): Promise<AttachConnection>;
@@ -53,7 +53,7 @@ export async function preWaitForViewSocket(nodeId) {
53
53
  /** Resolve the target, revive the node if it is dormant (server-side), and dial
54
54
  * the broker. Rejects with a plain `InputError` — no terminal has been taken
55
55
  * over at this point, so a failure here is an ordinary non-zero CLI exit. */
56
- export async function connectToNode(nodeId, canvasName) {
56
+ export async function connectToNode(nodeId, canvasName, wakeParked = true) {
57
57
  const remote = canvasName !== undefined;
58
58
  // Attach must NOT silently honor a durable `crtr canvas use` selection: the
59
59
  // local branch has a hard local-unix-socket assumption baked into its
@@ -74,6 +74,8 @@ export async function connectToNode(nodeId, canvasName) {
74
74
  let meta;
75
75
  let nodeIdentity;
76
76
  let transport;
77
+ let initialSnapshot;
78
+ let initialCommands;
77
79
  if (remote) {
78
80
  meta = { cwd: process.cwd() };
79
81
  transport = new RelayTransport(nodeId, resolveRemoteCanvasTarget(canvasName));
@@ -97,14 +99,23 @@ export async function connectToNode(nodeId, canvasName) {
97
99
  // revive here and let BrokerClient(nodeId) connect over the local socket as
98
100
  // before — this LOCAL process never launches pi or writes the session.
99
101
  try {
100
- await cliClient().ensureAttach(nodeId, { resume: true, revive: true });
102
+ const attach = await cliClient().ensureAttach(nodeId, { resume: true, revive: true, wakeParked });
103
+ if (attach.parked) {
104
+ const saved = await cliClient().getSnapshot(nodeId);
105
+ // The API DTO stays dependency-light and structural; this viewer is the
106
+ // consumer that knows the complete broker snapshot contract.
107
+ initialSnapshot = saved.snapshot;
108
+ initialCommands = saved.commands;
109
+ }
101
110
  }
102
111
  catch (err) {
103
112
  rethrowAsCliError(err, 'The node may be finalized or otherwise not attachable.');
104
113
  }
105
114
  // Local only: remote attach reaches crtrd over WebSocket rather than a
106
- // local socket file, so `--canvas` has nothing to pre-wait on.
107
- await preWaitForViewSocket(nodeId);
115
+ // local socket file, so `--canvas` has nothing to pre-wait on. An offline
116
+ // parked focus has deliberately no socket to wait for.
117
+ if (initialSnapshot === undefined)
118
+ await preWaitForViewSocket(nodeId);
108
119
  }
109
120
  const socket = new BrokerClient(nodeId, { transport });
110
121
  /** One error shape for a connect-time failure, whether the connect race below
@@ -137,34 +148,38 @@ export async function connectToNode(nodeId, canvasName) {
137
148
  terminalStartupError = err;
138
149
  });
139
150
  // Connect FIRST (race connect/error) so a missing broker exits non-zero
140
- // cleanly, BEFORE the TUI takes over the terminal.
141
- await new Promise((resolve, reject) => {
142
- const onConnect = () => {
143
- cleanup();
144
- resolve();
145
- };
146
- const onErr = (err) => {
147
- cleanup();
148
- reject(err);
149
- };
150
- const cleanup = () => {
151
- socket.off('connect', onConnect);
152
- socket.off('error', onErr);
153
- };
154
- socket.on('connect', onConnect);
155
- socket.on('error', onErr);
156
- socket.connect();
157
- }).catch((err) => {
158
- throw toAttachError(err);
159
- });
151
+ // cleanly, BEFORE the TUI takes over the terminal. Offline parked focus is
152
+ // the one intentional exception: reconnect owns its first dial after paint.
153
+ if (initialSnapshot === undefined) {
154
+ await new Promise((resolve, reject) => {
155
+ const onConnect = () => {
156
+ cleanup();
157
+ resolve();
158
+ };
159
+ const onErr = (err) => {
160
+ cleanup();
161
+ reject(err);
162
+ };
163
+ const cleanup = () => {
164
+ socket.off('connect', onConnect);
165
+ socket.off('error', onErr);
166
+ };
167
+ socket.on('connect', onConnect);
168
+ socket.on('error', onErr);
169
+ socket.connect();
170
+ }).catch((err) => {
171
+ throw toAttachError(err);
172
+ });
173
+ }
160
174
  return {
161
175
  remote,
162
176
  canvasSource,
163
177
  meta,
164
178
  nodeIdentity,
165
179
  socket,
180
+ ...(initialSnapshot === undefined ? {} : { initialSnapshot, initialCommands }),
166
181
  assertConnected: () => {
167
- if (terminalStartupError !== undefined)
182
+ if (initialSnapshot === undefined && terminalStartupError !== undefined)
168
183
  throw toAttachError(terminalStartupError);
169
184
  },
170
185
  };
@@ -45,6 +45,6 @@ export interface InputWiringHooks {
45
45
  frecency: FrecencyStore;
46
46
  }
47
47
  /** Construct the viewer's one capability authority. */
48
- export declare function createViewerCapabilitiesForInput(ctx: Pick<AttachSession, 'nodeId' | 'remote' | 'role' | 'meta'>): ViewerCapabilities;
48
+ export declare function createViewerCapabilitiesForInput(ctx: Pick<AttachSession, 'nodeId' | 'remote' | 'role' | 'meta'>, isBrokerOffline: () => boolean): ViewerCapabilities;
49
49
  /** Build the viewer's InputController with every hook answered. */
50
50
  export declare function createInputController(ctx: Pick<AttachSession, 'nodeId' | 'remote' | 'role' | 'meta' | 'socket' | 'tui' | 'editor' | 'containers'>, km: KeybindingsManager, capabilities: ViewerCapabilities, hooks: InputWiringHooks): InputController;
@@ -17,12 +17,13 @@ import { cliClient } from '../../../commands/api-client.js';
17
17
  import { buildLoginPicker, buildLogoutPicker } from '../overlays/auth.js';
18
18
  import { defaultAgentDir } from '../config.js';
19
19
  /** Construct the viewer's one capability authority. */
20
- export function createViewerCapabilitiesForInput(ctx) {
20
+ export function createViewerCapabilitiesForInput(ctx, isBrokerOffline) {
21
21
  return createViewerCapabilities({
22
22
  nodeId: ctx.nodeId,
23
23
  profileId: ctx.meta.profile_id ?? undefined,
24
24
  remote: ctx.remote,
25
25
  role: ctx.role,
26
+ isBrokerOffline,
26
27
  });
27
28
  }
28
29
  /** Build the viewer's InputController with every hook answered. */
@@ -93,7 +94,10 @@ export function createInputController(ctx, km, capabilities, hooks) {
93
94
  // /fork, /tree, /settings, /scoped-models) fetch their payloads over.
94
95
  // Without it every picker degrades to the "isn't available in this viewer"
95
96
  // notice (see slash/dispatch.ts + InputController.openPicker).
96
- onRequest: (frame) => socket.request(frame),
97
+ onRequest: (frame) => {
98
+ const refusal = capabilities.refusal('engine-drive');
99
+ return refusal === undefined ? socket.request(frame) : Promise.reject(new Error(refusal));
100
+ },
97
101
  // Mount pickers inline in the chrome stack, directly under the editor
98
102
  // (queued-panel pattern) rather than as a floating centered modal, so a
99
103
  // picker reads as part of the editor — like pi's own selectors.
@@ -16,15 +16,19 @@ export interface ReconnectHooks {
16
16
  /** End the session. `node-done` is a clean finalize (the node pushed its
17
17
  * final report) and closes the pane; `broker-gone` freezes the corpse. */
18
18
  teardown: (reason: 'node-done' | 'broker-gone') => void;
19
- /** Called once each time the node is first seen as dormant. An idle-released
20
- * broker has settled its turn and will relay no final session event, so the
21
- * viewer must clear its live-only display itself. */
19
+ /** The viewer was bootstrapped from a saved parked snapshot before any socket
20
+ * connection. */
21
+ initialDormant?: boolean;
22
+ /** Called once each time the node is first seen as dormant. A stopped broker
23
+ * will relay no final session event, so the viewer clears live-only display. */
22
24
  onEnterDormancy: () => void;
23
25
  }
24
26
  export interface ReconnectSupervisor {
25
27
  /** Send the hello handshake. Called once after `tui.start()`, then again on
26
28
  * every successful redial. */
27
29
  sendHello: () => void;
30
+ /** Begin redialing an offline parked viewer after its saved snapshot paints. */
31
+ startDormant: () => void;
28
32
  /** True while the node has no live broker — the input layer routes a
29
33
  * submitted prompt through a wake message rather than a dead socket. */
30
34
  isDormant: () => boolean;
@@ -16,7 +16,7 @@ export function createReconnect(ctx, hooks) {
16
16
  const { nodeId, socket, canvasSource } = ctx;
17
17
  let lastSocketError;
18
18
  let reconnecting = false;
19
- let viewerDormant = false;
19
+ let viewerDormant = hooks.initialDormant === true;
20
20
  // Captured at give-up: a terminal `done` is a CLEAN finalize — the viewer
21
21
  // closes its pane instead of freezing a scary "broker gone" corpse.
22
22
  // `dead`/`canceled` keep the corpse.
@@ -97,7 +97,12 @@ export function createReconnect(ctx, hooks) {
97
97
  // submit shows "waking node…" the moment you send — so a persistent
98
98
  // "node dormant" banner is pure noise. Clear the footer when dormant;
99
99
  // keep the reconnecting notice only for a transient broker relaunch.
100
- const dormant = row?.status === 'idle' || row?.intent === 'idle-release';
100
+ const dormant = row?.status === 'idle'
101
+ || row?.intent === 'idle-release'
102
+ || (row?.lifecycle === 'resident'
103
+ && row.status === 'done'
104
+ && row.intent === 'parked'
105
+ && row.final_report == null);
101
106
  const enteredDormancy = dormant && !viewerDormant;
102
107
  viewerDormant = dormant;
103
108
  if (enteredDormancy)
@@ -150,6 +155,12 @@ export function createReconnect(ctx, hooks) {
150
155
  };
151
156
  return {
152
157
  sendHello,
158
+ startDormant: () => {
159
+ if (reconnecting || stopped || hooks.isTornDown())
160
+ return;
161
+ hooks.onEnterDormancy();
162
+ void attemptReconnect();
163
+ },
153
164
  isDormant: () => viewerDormant,
154
165
  lastSocketError: () => lastSocketError,
155
166
  install,
@@ -38,7 +38,7 @@ export const CANVAS_SLASH_COMMANDS = [
38
38
  { name: 'graph', description: 'Toggle the canvas GRAPH view (your local subscription tree)' },
39
39
  { name: 'inbox', description: 'Toggle the human inbox — pending asks and reviews for you' },
40
40
  { name: 'promote', description: 'Promote this node to an orchestrator — /promote, or /promote <kind> to specialize' },
41
- { name: 'resume-node', description: 'Open the canvas navigator (search/scope/sort/tree) and resume the chosen node' },
41
+ { name: 'resume-node', description: 'Open the canvas navigator and focus the chosen node; parked residents wake only on submitted input' },
42
42
  { name: 'context', description: 'Browse this node + its subscriptions’ context dirs in a read-only nvim popup' },
43
43
  { name: 'plugins', description: 'Open the interactive plugin browser for this machine' },
44
44
  { name: 'node-metadata', description: 'Inspect this node — identity, lifecycle, runtime, graph, artifacts, paths, and agent surfaces' },
@@ -5,4 +5,4 @@ import './session/pi-oauth-flows.js';
5
5
  * always restoring the terminal first. `canvasName`, when set, attaches over
6
6
  * a relayed WebSocket to that `remoteCanvas.targets.<name>` instead of the
7
7
  * local unix socket — see the `--canvas` flag on `attachToLeaf`. */
8
- export declare function runAttach(nodeId: string, observer: boolean, canvasName?: string): Promise<void>;
8
+ export declare function runAttach(nodeId: string, observer: boolean, canvasName?: string, wakeParked?: boolean): Promise<void>;