@comput/pi-intercom 0.8.1 → 0.9.0

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 (42) hide show
  1. package/README.md +48 -2
  2. package/dist/config.d.ts +7 -0
  3. package/dist/config.js +10 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/core-monitor.d.ts +30 -0
  6. package/dist/core-monitor.js +528 -0
  7. package/dist/core-monitor.js.map +1 -0
  8. package/dist/core-pane-close.d.ts +34 -0
  9. package/dist/core-pane-close.js +335 -0
  10. package/dist/core-pane-close.js.map +1 -0
  11. package/dist/core-worker-layout.d.ts +50 -0
  12. package/dist/core-worker-layout.js +236 -0
  13. package/dist/core-worker-layout.js.map +1 -0
  14. package/dist/herdr-core.d.ts +53 -0
  15. package/dist/herdr-core.js +380 -0
  16. package/dist/herdr-core.js.map +1 -0
  17. package/dist/index.js +42 -10
  18. package/dist/index.js.map +1 -1
  19. package/dist/launcher.d.ts +2 -0
  20. package/dist/launcher.js +48 -2
  21. package/dist/launcher.js.map +1 -1
  22. package/dist/monitor-launcher.d.ts +8 -0
  23. package/dist/monitor-launcher.js +111 -25
  24. package/dist/monitor-launcher.js.map +1 -1
  25. package/dist/pane-close.js +3 -0
  26. package/dist/pane-close.js.map +1 -1
  27. package/dist/runtime.d.ts +17 -1
  28. package/dist/runtime.js +51 -1
  29. package/dist/runtime.js.map +1 -1
  30. package/package.json +5 -3
  31. package/references/herdr-core-migration.md +44 -0
  32. package/references/v1-specification.md +7 -4
  33. package/src/config.ts +10 -1
  34. package/src/core-monitor.ts +410 -0
  35. package/src/core-pane-close.ts +328 -0
  36. package/src/core-worker-layout.ts +240 -0
  37. package/src/herdr-core.ts +287 -0
  38. package/src/index.ts +38 -11
  39. package/src/launcher.ts +46 -2
  40. package/src/monitor-launcher.ts +95 -21
  41. package/src/pane-close.ts +2 -0
  42. package/src/runtime.ts +47 -1
@@ -2,17 +2,38 @@ import path from 'node:path';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { access, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
4
4
  import { run, shQuote, psQuote, encoded, type Run } from './launcher.js';
5
+ import { ensureCoreMonitorPane, closeCoreMonitorPane } from './core-monitor.js';
6
+ import { withCoreSession, type CoreCall } from './herdr-core.js';
5
7
 
6
8
  interface Pane { pane_id: string; tab_id: string; workspace_id?: string; label?: string }
7
- interface RecordState { version: 1; sessionId: string; workspace: string; caller: string; pane?: string; stage: string }
9
+ export type MonitorPlacement = 'coordinator-tab' | 'new-tab';
10
+ interface RecordState { version: 1; sessionId: string; workspace: string; caller: string; pane?: string; tab?: string; placement?: MonitorPlacement; stage: string }
8
11
  export interface MonitorLaunchOptions {
9
12
  root: string; sessionId: string; script: string;
10
13
  node?: string; platform?: string; env?: NodeJS.ProcessEnv; run?: Run;
14
+ placement?: MonitorPlacement;
15
+ coreCall?: CoreCall;
11
16
  assertCurrent?(): void;
12
17
  }
13
18
  /** Pane ownership is separate from agent config. Pending records fence uncertain
14
19
  * mutations across reloads; neither errors nor absence of readiness cause retries. */
15
20
  export async function ensureMonitorPane(options: MonitorLaunchOptions): Promise<{ outcome: string; pane?: string }> {
21
+ if ((options.env ?? process.env).HC_TERMINAL_ID !== undefined) {
22
+ const open = (call: CoreCall) => ensureCoreMonitorPane({ ...options, placement: options.placement ?? 'coordinator-tab', call });
23
+ return options.coreCall ? open(options.coreCall) : withCoreSession(open);
24
+ }
25
+ return monitorPane(options, false);
26
+ }
27
+ export async function closeMonitorPane(options: MonitorLaunchOptions): Promise<{ outcome: string; pane?: string }> {
28
+ if ((options.env ?? process.env).HC_TERMINAL_ID !== undefined) {
29
+ const close = (call: CoreCall) => closeCoreMonitorPane({ ...options, call });
30
+ return options.coreCall ? close(options.coreCall) : withCoreSession(close);
31
+ }
32
+ return monitorPane(options, true);
33
+ }
34
+ async function monitorPane(options: MonitorLaunchOptions, closing: boolean): Promise<{ outcome: string; pane?: string }> {
35
+ const placement = options.placement ?? 'coordinator-tab';
36
+ if (!['coordinator-tab', 'new-tab'].includes(placement)) throw new Error('Invalid monitor placement');
16
37
  const env = options.env ?? process.env;
17
38
  if (env.HERDR_ENV !== '1') return { outcome: 'unavailable: coordinator is not inside Herdr' };
18
39
  if (!env.HERDR_PANE_ID) throw new Error('Monitor requires caller pane identity; no focused-pane fallback');
@@ -25,41 +46,59 @@ export async function ensureMonitorPane(options: MonitorLaunchOptions): Promise<
25
46
  const output = await exec('herdr', args);
26
47
  check();
27
48
  // pane run acknowledges command submission with an empty stdout on current Herdr.
28
- if (!output.trim() && args[1] === 'run') return {};
49
+ if (!output.trim() && ['run', 'close'].includes(args[1])) return {};
29
50
  return JSON.parse(output).result;
30
51
  };
31
52
  const caller = (await call(['pane', 'current', '--current']))?.pane as Pane;
32
- if (!caller?.pane_id || !caller.tab_id || !caller.workspace_id) throw new Error('Missing current Herdr pane identity');
53
+ if (!caller?.pane_id || !caller.tab_id || !caller.workspace_id) throw new Error('Missing current Herdr caller pane identity');
54
+ // Herdr documents inherited moved-pane aliases for --current. Preserve that
55
+ // resolution for opening; destructive close conservatively requires exact ID.
56
+ if (closing && caller.pane_id !== env.HERDR_PANE_ID) throw new Error('Mismatched current Herdr caller pane identity; refusing close');
33
57
  const label = 'Intercom monitor';
34
58
  const legacyMarker = `Intercom monitor [${options.sessionId}]`;
35
59
  const key = createHash('sha256').update(options.sessionId).digest('hex').slice(0, 24);
36
60
  const directory = path.join(options.root, '.pi-intercom');
61
+ check();
37
62
  await mkdir(directory, { recursive: true });
63
+ check();
38
64
  const file = path.join(directory, `monitor-${key}.json`), lock = `${file}.lock`;
39
65
  check();
40
66
  try { await writeFile(lock, '', { flag: 'wx', mode: 0o600 }); }
41
67
  catch { throw new Error(`Monitor launch locked; inspect ${lock} before recovery. No pane created.`); }
42
68
  let state: RecordState | undefined;
43
69
  const save = async () => {
70
+ check();
44
71
  const temporary = `${file}.${randomUUID()}.tmp`;
45
72
  try {
46
73
  await writeFile(temporary, JSON.stringify(state), { flag: 'wx', mode: 0o600 });
74
+ check();
47
75
  await rename(temporary, file);
48
76
  } finally { await unlink(temporary).catch(() => {}); }
49
77
  };
50
78
  try {
79
+ check();
51
80
  try {
52
81
  state = JSON.parse(await readFile(file, 'utf8'));
53
- if (state?.version !== 1 || state.sessionId !== options.sessionId || typeof state.stage !== 'string') throw new Error('Invalid monitor ownership record');
82
+ check();
83
+ if (state?.version !== 1 || state.sessionId !== options.sessionId || typeof state.stage !== 'string' ||
84
+ typeof state.workspace !== 'string' || typeof state.caller !== 'string' ||
85
+ (state.placement !== undefined && !['coordinator-tab', 'new-tab'].includes(state.placement)) ||
86
+ (state.tab !== undefined && typeof state.tab !== 'string') ||
87
+ (state.pane !== undefined && typeof state.pane !== 'string') ||
88
+ (state.placement === 'new-tab' && ['submitted', 'observed'].includes(state.stage) && !state.tab)) throw new Error('Invalid monitor ownership record');
54
89
  } catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; }
55
90
  const panes = (await call(['pane', 'list', '--workspace', caller.workspace_id]))?.panes as Pane[];
56
- if (!Array.isArray(panes) || panes.some(p => !p?.pane_id || !p.tab_id)) throw new Error('Invalid Herdr pane inventory');
91
+ if (!Array.isArray(panes) || panes.some(p => !p?.pane_id || !p.tab_id || p.workspace_id && p.workspace_id !== caller.workspace_id) || new Set(panes.map(p => p.pane_id)).size !== panes.length || !panes.some(p => p.pane_id === caller.pane_id && p.tab_id === caller.tab_id)) throw new Error('Invalid Herdr pane inventory');
57
92
  const matches = panes.filter(p => p.label === legacyMarker ||
58
93
  (state && state.workspace === caller.workspace_id && state.pane === p.pane_id));
59
94
  if (matches.length > 1) throw new Error('Multiple monitor markers; inspect panes. No automatic changes.');
60
95
  if (matches.length === 1) {
61
96
  const pane = matches[0];
62
- if (pane.pane_id === caller.pane_id || pane.tab_id !== caller.tab_id) throw new Error('Monitor marker is on caller or moved to another tab; no duplicate created');
97
+ if (panes.some(other => other.pane_id !== pane.pane_id && other.label === label)) throw new Error('Multiple monitor labels in workspace; inspect panes. No automatic changes.');
98
+ const savedPlacement = state?.placement ?? 'coordinator-tab';
99
+ if (pane.pane_id === caller.pane_id || pane.tab_id !== (state?.tab ?? caller.tab_id) ||
100
+ (savedPlacement === 'coordinator-tab' ? pane.tab_id !== caller.tab_id : pane.tab_id === caller.tab_id)) throw new Error('Monitor marker is on caller or moved to another tab; no automatic changes');
101
+ if (!closing && savedPlacement !== placement) throw new Error('Monitor already exists in a different placement; close it first before opening elsewhere');
63
102
  if (state && (state.workspace !== caller.workspace_id || state.caller !== caller.pane_id || (state.pane && state.pane !== pane.pane_id))) {
64
103
  throw new Error('Monitor ownership differs from current caller; no automatic changes');
65
104
  }
@@ -67,35 +106,70 @@ export async function ensureMonitorPane(options: MonitorLaunchOptions): Promise<
67
106
  throw new Error(`Monitor partial launch needs inspection (${state.stage}); no retry or duplicate`);
68
107
  }
69
108
  if (!state) {
70
- state = { version: 1, sessionId: options.sessionId, workspace: caller.workspace_id, caller: caller.pane_id, pane: pane.pane_id, stage: 'observed' };
109
+ if (closing) throw new Error('Legacy monitor marker has no saved ownership; manual inspection required, no automatic close');
110
+ state = { version: 1, sessionId: options.sessionId, workspace: caller.workspace_id, caller: caller.pane_id, pane: pane.pane_id, tab: pane.tab_id, placement: 'coordinator-tab', stage: 'observed' };
71
111
  await save();
72
112
  }
113
+ if (closing) {
114
+ if (state.stage !== 'submitted') throw new Error('Legacy observed monitor lacks launcher-created ownership proof; refusing close');
115
+ if (pane.label !== label && pane.label !== legacyMarker) throw new Error('Owned monitor label changed; refusing to close a potentially reused pane');
116
+ state.stage = 'close_pending'; await save();
117
+ const finalCaller = (await call(['pane', 'current', '--current']))?.pane as Pane;
118
+ const finalPanes = (await call(['pane', 'list', '--workspace', caller.workspace_id]))?.panes as Pane[];
119
+ if (finalCaller?.pane_id !== caller.pane_id || finalCaller.tab_id !== caller.tab_id || finalCaller.workspace_id !== caller.workspace_id ||
120
+ !Array.isArray(finalPanes) || finalPanes.some(p => !p?.pane_id || !p.tab_id || p.workspace_id && p.workspace_id !== caller.workspace_id) ||
121
+ new Set(finalPanes.map(p => p.pane_id)).size !== finalPanes.length ||
122
+ !finalPanes.some(p => p.pane_id === caller.pane_id && p.tab_id === caller.tab_id) ||
123
+ finalPanes.filter(p => p.pane_id === pane.pane_id && p.tab_id === pane.tab_id && p.label === pane.label).length !== 1 ||
124
+ finalPanes.some(p => p.pane_id !== pane.pane_id && [label, legacyMarker].includes(p.label ?? ''))) throw new Error('Monitor ownership changed before close; pending outcome fenced, inspect manually');
125
+ // Herdr has no compare-and-close primitive: this minimizes but cannot
126
+ // eliminate an external topology race between inventory and pane close.
127
+ await call(['pane', 'close', pane.pane_id]);
128
+ const remaining = (await call(['pane', 'list', '--workspace', caller.workspace_id]))?.panes as Pane[];
129
+ if (!Array.isArray(remaining) || remaining.some(p => !p?.pane_id || !p.tab_id || p.workspace_id && p.workspace_id !== caller.workspace_id) || new Set(remaining.map(p => p.pane_id)).size !== remaining.length || remaining.some(p => p.pane_id === pane.pane_id) || !remaining.some(p => p.pane_id === caller.pane_id && p.tab_id === caller.tab_id)) throw new Error('Monitor pane absence not confirmed; close outcome uncertain, do not retry');
130
+ state.stage = 'closed'; await save();
131
+ return { outcome: 'monitor pane absence observed; no claim about child processes', pane: pane.pane_id };
132
+ }
73
133
  if (pane.label !== label) await call(['pane', 'rename', pane.pane_id, label]);
74
134
  return { outcome: 'existing pane preserved; process readiness not checked', pane: pane.pane_id };
75
135
  }
76
- if (panes.some(p => p.tab_id === caller.tab_id && p.label === label)) {
77
- throw new Error('Unowned monitor label in coordinator tab; inspect before creating another pane');
136
+ if (panes.some(p => p.label === label)) {
137
+ throw new Error('Unowned monitor label in workspace; inspect before creating or closing a pane');
78
138
  }
79
- if (state && (!['submitted', 'observed'].includes(state.stage) || state.workspace !== caller.workspace_id || panes.some(p => p.pane_id === state!.pane))) {
139
+ if (state && (!['submitted', 'observed', 'closed'].includes(state.stage) || state.workspace !== caller.workspace_id || state.caller !== caller.pane_id || panes.some(p => p.pane_id === state!.pane))) {
80
140
  throw new Error(`Monitor ownership/partial launch needs inspection (${state.stage}, pane ${state.pane ?? 'unknown'}); no retry or duplicate`);
81
141
  }
142
+ if (closing) return { outcome: 'no owned monitor pane present; no close submitted' };
82
143
  await access(options.script); // Missing build must not leave a new empty pane.
83
144
  check();
84
- state = { version: 1, sessionId: options.sessionId, workspace: caller.workspace_id, caller: caller.pane_id, stage: 'split_pending' };
145
+ state = { version: 1, sessionId: options.sessionId, workspace: caller.workspace_id, caller: caller.pane_id, placement, tab: placement === 'coordinator-tab' ? caller.tab_id : undefined, stage: placement === 'new-tab' ? 'tab_pending' : 'split_pending' };
85
146
  await save();
86
147
  // Small top quarter after swapping pane identities. Verify actual geometry below.
87
- const split = await call(['pane', 'split', '--pane', caller.pane_id, '--direction', 'down', '--ratio', '0.25', '--cwd', options.root, '--no-focus']);
88
- const pane = split?.pane?.pane_id;
89
- if (typeof pane !== 'string' || pane === caller.pane_id) throw new Error('Split returned no distinct pane ID');
148
+ const created = placement === 'new-tab'
149
+ ? await call(['tab', 'create', '--workspace', caller.workspace_id, '--cwd', options.root, '--label', label, '--no-focus'])
150
+ : await call(['pane', 'split', '--pane', caller.pane_id, '--direction', 'down', '--ratio', '0.25', '--cwd', options.root, '--no-focus']);
151
+ const pane = placement === 'new-tab' ? created?.root_pane?.pane_id : created?.pane?.pane_id;
152
+ if (typeof pane !== 'string' || pane === caller.pane_id || panes.some(p => p.pane_id === pane)) throw new Error('Creation returned no distinct new pane ID');
153
+ if (placement === 'new-tab') {
154
+ const tab = created?.tab?.tab_id;
155
+ if (typeof tab !== 'string' || tab === caller.tab_id || panes.some(p => p.tab_id === tab)) throw new Error('Creation returned no distinct new tab ID');
156
+ state.tab = tab;
157
+ }
90
158
  state.pane = pane; state.stage = 'rename_pending'; await save();
91
159
  await call(['pane', 'rename', pane, label]);
92
- state.stage = 'swap_pending'; await save();
93
- const swapped = await call(['pane', 'swap', '--source-pane', caller.pane_id, '--target-pane', pane]);
94
- if (swapped?.swap?.changed !== true) throw new Error('Herdr did not confirm pane swap');
95
- const layout = (await call(['pane', 'layout', '--pane', caller.pane_id]))?.layout;
96
- const top = layout?.panes?.find((p: { pane_id: string }) => p.pane_id === pane)?.rect;
97
- const bottom = layout?.panes?.find((p: { pane_id: string }) => p.pane_id === caller.pane_id)?.rect;
98
- if (!top || !bottom || top.y >= bottom.y || layout.focused_pane_id !== caller.pane_id) throw new Error('Monitor geometry/focus not confirmed; inspect panes before recovery');
160
+ if (placement === 'coordinator-tab') {
161
+ state.stage = 'swap_pending'; await save();
162
+ const swapped = await call(['pane', 'swap', '--source-pane', caller.pane_id, '--target-pane', pane]);
163
+ if (swapped?.swap?.changed !== true) throw new Error('Herdr did not confirm pane swap');
164
+ const layout = (await call(['pane', 'layout', '--pane', caller.pane_id]))?.layout;
165
+ const top = layout?.panes?.find((p: { pane_id: string }) => p.pane_id === pane)?.rect;
166
+ const bottom = layout?.panes?.find((p: { pane_id: string }) => p.pane_id === caller.pane_id)?.rect;
167
+ if (!top || !bottom || top.y >= bottom.y || layout.focused_pane_id !== caller.pane_id) throw new Error('Monitor geometry/focus not confirmed; inspect panes before recovery');
168
+ } else {
169
+ const inventory = (await call(['pane', 'list', '--workspace', caller.workspace_id]))?.panes as Pane[];
170
+ const current = (await call(['pane', 'current', '--current']))?.pane as Pane;
171
+ if (!Array.isArray(inventory) || inventory.some(p => !p?.pane_id || !p.tab_id || p.workspace_id && p.workspace_id !== caller.workspace_id) || new Set(inventory.map(p => p.pane_id)).size !== inventory.length || inventory.filter(p => p?.pane_id === pane && p.tab_id === state!.tab).length !== 1 || !inventory.some(p => p.pane_id === caller.pane_id && p.tab_id === caller.tab_id) || current?.pane_id !== caller.pane_id || current.tab_id !== caller.tab_id || current.workspace_id !== caller.workspace_id) throw new Error('New monitor tab/caller identity not confirmed; inspect before recovery');
172
+ }
99
173
  state.stage = 'run_pending'; await save();
100
174
  const args = [options.script, '--root', options.root];
101
175
  const command = platform === 'win32'
package/src/pane-close.ts CHANGED
@@ -5,6 +5,7 @@ import { open, readFile, realpath } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import type { Run } from './launcher.js';
7
7
  import type { CloseProvider, WorkerPaneIdentity } from './handoff.js';
8
+ import { createCoreCloseProvider } from './core-pane-close.js';
8
9
 
9
10
  interface ProcessIdentity { start: string; state: string }
10
11
  export interface PaneCloseOptions {
@@ -53,6 +54,7 @@ const alive = (identity: ProcessIdentity | undefined, start: string) =>
53
54
  /** Linux Herdr only. Identity-checked best effort; Herdr has no atomic compare-and-close API. */
54
55
  export function createCloseProvider(options: PaneCloseOptions): CloseProvider | undefined {
55
56
  const env = options.env ?? process.env;
57
+ if (env.HC_TERMINAL_ID !== undefined) return createCoreCloseProvider(options);
56
58
  if ((options.platform ?? process.platform) !== 'linux' || env.HERDR_ENV !== '1' || !env.HERDR_PANE_ID) return;
57
59
  const run = options.run ?? exec;
58
60
  const processIdentity = options.processIdentity ?? linuxProcessIdentity;
package/src/runtime.ts CHANGED
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { realpath } from 'node:fs/promises';
4
4
  import { LocalObserver, LOG_LIMITS, observationError, type Observer, type EventType, type EventMetadata } from './observability.js';
5
- import { ConfigStore, coordinator, directory, fail, named, port, requireCoordinator, text, validateConfig, type Agent, type Config } from './config.js';
5
+ import { ConfigStore, coordinator, directory, fail, named, port, requireCoordinator, text, validateConfig, monitorSettings, validateMonitorSettings, type MonitorSettings, type Agent, type Config } from './config.js';
6
6
  import { listen, send, reportPayload, type Endpoint, type Envelope, type Kind } from './transport.js';
7
7
  import { saveWorkerReport } from './reports.js';
8
8
  import { readObservationSnapshot } from './snapshot.js';
@@ -21,6 +21,11 @@ export interface Host {
21
21
  notify(text: string): void;
22
22
  }
23
23
  export interface LaunchRequest { multiplexer: 'herdr' | 'none'; cwd: string; workspaceRoot: string; sessionId?: string }
24
+ export interface MonitorRequest { root: string; sessionId: string; assertCurrent(): void }
25
+ export interface MonitorOperations {
26
+ open(request: MonitorRequest & { placement: MonitorSettings['placement'] }): Promise<unknown>;
27
+ close(request: MonitorRequest): Promise<unknown>;
28
+ }
24
29
  export interface RuntimeOptions {
25
30
  launch(request: LaunchRequest): Promise<unknown>;
26
31
  listen?: typeof listen;
@@ -32,6 +37,7 @@ export interface RuntimeOptions {
32
37
  /** Test seam; production defaults to 120 seconds, values above that are capped. */
33
38
  closeTimeoutMs?: number;
34
39
  reminderClock?: ReminderClock;
40
+ monitor?: MonitorOperations;
35
41
  /** Explicit launcher-provided root must equal canonical cwd; no parent-root exception. */
36
42
  workspaceRoot?: string;
37
43
  }
@@ -204,6 +210,32 @@ export class Intercom {
204
210
  assertValid();
205
211
  return { id, config, me: config.agents.find(a => a.sessionId === id) };
206
212
  }
213
+ async getMonitorSettings(): Promise<MonitorSettings> {
214
+ const assertValid = this.validity();
215
+ const { id, config } = await this.state();
216
+ assertValid(); requireCoordinator(config, id);
217
+ return monitorSettings(config);
218
+ }
219
+ /** Automatic admission is internal: explicit open remains allowed with disabled auto-open or no workers. */
220
+ async autoOpenMonitor(assertAdmission: () => void = () => {}): Promise<unknown> { return this.operateMonitor('open_monitor', {}, true, assertAdmission); }
221
+ private async operateMonitor(operation: 'open_monitor' | 'close_monitor', args: Record<string, unknown>, automatic = false, assertAdmission: () => void = () => {}): Promise<unknown> {
222
+ const lifecycle = this.validity();
223
+ const assertValid = () => { lifecycle(); assertAdmission(); };
224
+ assertValid();
225
+ if (Object.keys(args).some(key => operation === 'close_monitor' || key !== 'placement')) fail('invalid monitor operation arguments');
226
+ if (operation === 'open_monitor' && !automatic && !['coordinator-tab', 'new-tab'].includes(args.placement as string)) fail('explicit monitor placement is required; ask the user if unknown');
227
+ const fresh = await this.state(); assertValid(); requireCoordinator(fresh.config, fresh.id);
228
+ const settings = monitorSettings(fresh.config);
229
+ if (automatic && (!settings.autoOpen || !fresh.config.agents.some(agent => !agent.coordinator))) return { outcome: 'automatic monitor skipped' };
230
+ if (!this.options.monitor) fail('monitor adapter unavailable; no pane action attempted');
231
+ const request: MonitorRequest = { root: this.store.root, sessionId: fresh.id, assertCurrent: assertValid };
232
+ const result = operation === 'open_monitor'
233
+ ? await this.options.monitor.open({ ...request, placement: (automatic ? settings.placement : args.placement) as MonitorSettings['placement'] })
234
+ : await this.options.monitor.close(request);
235
+ assertValid();
236
+ const after = await this.state(); assertValid(); requireCoordinator(after.config, fresh.id);
237
+ return result;
238
+ }
207
239
  /** Accept old configs, but retire the removed browser dashboard's discovery metadata. */
208
240
  async retireDashboardMetadata(): Promise<void> {
209
241
  const record = this.captureObserver(), assertValid = this.validity();
@@ -412,6 +444,20 @@ export class Intercom {
412
444
  await this.transmit(args.to as string, 'message', { message: args.message }); return { accepted: true, completion: 'not awaited' };
413
445
  }
414
446
  requireCoordinator(config, id);
447
+ if (operation === 'configure_monitor') {
448
+ const keys = Object.keys(args);
449
+ if (!keys.length || keys.some(key => !['autoOpen', 'placement'].includes(key))) fail('configure_monitor requires autoOpen and/or placement only');
450
+ validateMonitorSettings({ ...monitorSettings(config), ...args });
451
+ const updated = await this.store.update(id, current => {
452
+ current.monitor = { ...monitorSettings(current), ...args } as MonitorSettings;
453
+ validateMonitorSettings(current.monitor);
454
+ }, assertValid);
455
+ assertValid();
456
+ return { written: true, monitor: monitorSettings(updated), paneEffects: false };
457
+ }
458
+ if (operation === 'open_monitor' || operation === 'close_monitor') {
459
+ return this.operateMonitor(operation, args);
460
+ }
415
461
  if (operation === 'stop_reminders') {
416
462
  const current = invocation >= this.reminderRevision;
417
463
  if (current) { this.reminderRevision = invocation; this.reminders?.stop(); }