@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.
- package/README.md +48 -2
- package/dist/config.d.ts +7 -0
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -1
- package/dist/core-monitor.d.ts +30 -0
- package/dist/core-monitor.js +528 -0
- package/dist/core-monitor.js.map +1 -0
- package/dist/core-pane-close.d.ts +34 -0
- package/dist/core-pane-close.js +335 -0
- package/dist/core-pane-close.js.map +1 -0
- package/dist/core-worker-layout.d.ts +50 -0
- package/dist/core-worker-layout.js +236 -0
- package/dist/core-worker-layout.js.map +1 -0
- package/dist/herdr-core.d.ts +53 -0
- package/dist/herdr-core.js +380 -0
- package/dist/herdr-core.js.map +1 -0
- package/dist/index.js +42 -10
- package/dist/index.js.map +1 -1
- package/dist/launcher.d.ts +2 -0
- package/dist/launcher.js +48 -2
- package/dist/launcher.js.map +1 -1
- package/dist/monitor-launcher.d.ts +8 -0
- package/dist/monitor-launcher.js +111 -25
- package/dist/monitor-launcher.js.map +1 -1
- package/dist/pane-close.js +3 -0
- package/dist/pane-close.js.map +1 -1
- package/dist/runtime.d.ts +17 -1
- package/dist/runtime.js +51 -1
- package/dist/runtime.js.map +1 -1
- package/package.json +5 -3
- package/references/herdr-core-migration.md +44 -0
- package/references/v1-specification.md +7 -4
- package/src/config.ts +10 -1
- package/src/core-monitor.ts +410 -0
- package/src/core-pane-close.ts +328 -0
- package/src/core-worker-layout.ts +240 -0
- package/src/herdr-core.ts +287 -0
- package/src/index.ts +38 -11
- package/src/launcher.ts +46 -2
- package/src/monitor-launcher.ts +95 -21
- package/src/pane-close.ts +2 -0
- package/src/runtime.ts +47 -1
package/src/monitor-launcher.ts
CHANGED
|
@@ -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
|
-
|
|
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]
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
77
|
-
throw new Error('Unowned monitor label in
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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(); }
|