@comput/pi-intercom 0.1.3 → 0.2.1

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/src/runtime.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import path from 'node:path';
2
- import { ConfigStore, coordinator, directory, fail, named, requireCoordinator, type Agent, type Config } from './config.js';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { LocalObserver, LOG_LIMITS, observationError, type Observer, type EventType, type EventMetadata } from './observability.js';
4
+ import { ConfigStore, coordinator, directory, fail, named, port, requireCoordinator, type Agent, type Config } from './config.js';
3
5
  import { listen, send, type Endpoint, type Envelope, type Kind } from './transport.js';
4
6
 
5
7
  export const UNSUPPORTED_CANCELLATION = 'Unsupported Pi host: stop_worker and close_worker are disabled. Pi 0.84.4 extension abort does not cancel retry backoff/continuations; graceful shutdown cannot guarantee no queued work restarts. No cancellation or shutdown was performed. See references/implementation-blocker.md. A verified supported host API is required (no version-only override).';
@@ -17,6 +19,7 @@ export interface RuntimeOptions {
17
19
  listen?: typeof listen;
18
20
  send?: typeof send;
19
21
  store?: (root: string) => ConfigStore;
22
+ observe?: (root: string, sessionId: string) => Observer;
20
23
  }
21
24
  export class Intercom {
22
25
  store!: ConfigStore;
@@ -25,6 +28,15 @@ export class Intercom {
25
28
  private generation = 0;
26
29
  private initialId = '';
27
30
  responsibility?: Agent;
31
+ private observer?: Observer;
32
+ private closing?: Promise<void>;
33
+ private captureObserver(): (event: EventType, metadata?: EventMetadata) => void {
34
+ const observer = this.observer;
35
+ return (event, metadata = {}) => {
36
+ try { observer?.record(event, metadata); } catch { /* Logging cannot affect communication. */ }
37
+ };
38
+ }
39
+ recordObservation(event: EventType, metadata: EventMetadata = {}): void { this.captureObserver()(event, metadata); }
28
40
  constructor(readonly host: Host, readonly options: RuntimeOptions) {}
29
41
  private id(): string {
30
42
  const id = this.host.sessionId();
@@ -40,26 +52,36 @@ export class Intercom {
40
52
  };
41
53
  }
42
54
  async start(): Promise<void> {
55
+ if (this.closing) await this.closing;
43
56
  if (this.active) fail('runtime already active');
44
57
  this.initialId = this.host.sessionId(); this.active = true;
45
58
  const generation = ++this.generation, assertValid = this.validity();
59
+ let record = this.captureObserver();
46
60
  try {
47
61
  const discovered = await ConfigStore.discover(this.host.cwd);
48
62
  assertValid();
49
63
  this.store = this.options.store?.(discovered?.root ?? this.host.cwd) ?? discovered ?? new ConfigStore(this.host.cwd);
64
+ try { this.observer = this.options.observe ? this.options.observe(this.store.root, this.initialId) : new LocalObserver(this.store.root, this.initialId); } catch { /* Logging is optional on failure. */ }
65
+ record = this.captureObserver();
66
+ record('runtime.starting');
50
67
  const previous = discovered ? await this.store.read() : undefined;
51
68
  assertValid();
52
69
  const own = previous?.agents.find(a => a.sessionId === this.id());
53
70
  const endpoint = await (this.options.listen ?? listen)(own?.port, m => { assertValid(); return this.receive(m); });
54
71
  try { assertValid(); } catch (error) { await endpoint.close(); throw error; }
55
72
  this.endpoint = endpoint;
56
- if (!previous) await this.store.initialize(this.id(), this.endpoint.port, assertValid);
73
+ if (!previous) {
74
+ const initialized = await this.store.initialize(this.id(), this.endpoint.port, assertValid);
75
+ if (initialized) record('config.changed', { operation: 'initialize', outcome: 'written' });
76
+ }
57
77
  const config = await this.store.read();
58
78
  assertValid();
59
79
  const me = config.agents.find(a => a.sessionId === this.id());
60
80
  if (me?.coordinator) {
81
+ try { this.observer?.maintain?.(); } catch { /* No cleanup failure affects readiness. */ }
61
82
  await this.store.update(this.id(), c => { requireCoordinator(c, this.id()).port = this.endpoint!.port; }, assertValid);
62
83
  assertValid();
84
+ record('config.changed', { operation: 'port_update', outcome: 'written', port: this.endpoint.port });
63
85
  try { await this.reload(); } catch (e) { assertValid(); this.host.notify(`Name/responsibility synchronization failed: ${String(e)}`); }
64
86
  assertValid();
65
87
  this.host.notify(`Intercom Coordinator ready at 127.0.0.1:${this.endpoint.port}; waiting for user input.`);
@@ -77,13 +99,39 @@ export class Intercom {
77
99
  assertValid();
78
100
  this.host.notify(`Intercom worker ready at 127.0.0.1:${this.endpoint.port}; waiting for explicit work.`);
79
101
  }
80
- } catch (e) { if (generation === this.generation) await this.close(); throw e; }
102
+ record('runtime.ready', { port: this.endpoint.port, role: me?.coordinator ? 'coordinator' : me ? 'worker' : 'anonymous', busy: this.host.busy(), outcome: 'ready' });
103
+ } catch (e) {
104
+ if (generation === this.generation) {
105
+ record('runtime.failed', { errorCode: observationError(e), outcome: 'failed' });
106
+ await this.close();
107
+ }
108
+ throw e;
109
+ }
81
110
  }
82
111
  async close(): Promise<void> {
112
+ if (this.closing) return this.closing;
113
+ this.recordObservation('runtime.closed');
114
+ const observer = this.observer; this.observer = undefined;
83
115
  this.active = false; this.generation++;
84
116
  const endpoint = this.endpoint; this.endpoint = undefined;
85
117
  this.responsibility = undefined;
86
- if (endpoint) await endpoint.close();
118
+ const closing = (async () => {
119
+ try { if (endpoint) await endpoint.close(); }
120
+ finally {
121
+ if (observer) {
122
+ let timer: ReturnType<typeof setTimeout> | undefined;
123
+ try {
124
+ // Only shutdown waits, and only for this bounded deadline. Already-issued I/O cannot be cancelled.
125
+ await Promise.race([
126
+ Promise.resolve().then(() => observer.close()).catch(() => {}),
127
+ new Promise<void>(resolve => { timer = setTimeout(resolve, LOG_LIMITS.closeTimeoutMs); }),
128
+ ]);
129
+ } finally { if (timer) clearTimeout(timer); }
130
+ }
131
+ }
132
+ })();
133
+ this.closing = closing;
134
+ try { await closing; } finally { if (this.closing === closing) this.closing = undefined; }
87
135
  }
88
136
  async state(): Promise<{ id: string; config: Config; me?: Agent }> {
89
137
  const assertValid = this.validity();
@@ -91,7 +139,16 @@ export class Intercom {
91
139
  assertValid();
92
140
  return { id, config, me: config.agents.find(a => a.sessionId === id) };
93
141
  }
142
+ /** Persist an actual dashboard bind; saved discovery metadata, not a live-status assertion. */
143
+ async recordDashboardPort(boundPort: number): Promise<void> {
144
+ const record = this.captureObserver(), assertValid = this.validity();
145
+ const { id, config } = await this.state();
146
+ assertValid(); requireCoordinator(config, id); port(boundPort);
147
+ await this.store.update(id, c => { requireCoordinator(c, id).dashboardPort = boundPort; }, assertValid);
148
+ record('config.changed', { outcome: 'written' });
149
+ }
94
150
  async reload(): Promise<void> {
151
+ const record = this.captureObserver();
95
152
  const assertValid = this.validity();
96
153
  const { me } = await this.state();
97
154
  assertValid();
@@ -99,14 +156,24 @@ export class Intercom {
99
156
  this.responsibility = { ...me };
100
157
  await this.host.setName(me.name);
101
158
  assertValid();
159
+ record('config.reloaded', { role: me.coordinator ? 'coordinator' : 'worker' });
102
160
  this.host.notify(`Intercom responsibility loaded for ${me.name}; no work turn started.`);
103
161
  }
104
162
  async transmit(to: string, kind: Kind, payload: Record<string, unknown> = {}): Promise<void> {
163
+ const record = this.captureObserver();
105
164
  const assertValid = this.validity();
106
165
  const { id, config } = await this.state();
107
166
  assertValid();
108
- const target = named(config, to);
109
- await (this.options.send ?? send)(target.port, { version: 1, from: id, to: target.sessionId, kind, payload });
167
+ const target = named(config, to), correlationId = randomUUID();
168
+ const metadata = { peerSessionId: target.sessionId, peerName: target.name, correlationId, kind };
169
+ record('transport.send', { ...metadata, outcome: 'attempted' });
170
+ try {
171
+ await (this.options.send ?? send)(target.port, { version: 1, from: id, to: target.sessionId, kind, payload, correlationId });
172
+ record('transport.receipt', { ...metadata, outcome: 'http_receipt' });
173
+ } catch (error) {
174
+ record('transport.failed', { ...metadata, outcome: 'failed', errorCode: observationError(error) });
175
+ throw error;
176
+ }
110
177
  }
111
178
  async report(): Promise<void> {
112
179
  const assertValid = this.validity();
@@ -117,15 +184,35 @@ export class Intercom {
117
184
  await this.transmit(coordinator(config).name, 'status', { port: this.endpoint.port, busy: this.host.busy() });
118
185
  }
119
186
  async receive(message: Envelope): Promise<void> {
187
+ const record = this.captureObserver();
188
+ const metadata = { peerSessionId: message.from, correlationId: message.correlationId, kind: message.kind };
189
+ record('transport.received', metadata);
190
+ try { await this.handleReceive(message); }
191
+ catch (error) {
192
+ record('transport.rejected', { ...metadata, outcome: 'handler_failed', errorCode: observationError(error) });
193
+ throw error;
194
+ }
195
+ }
196
+ private async handleReceive(message: Envelope): Promise<void> {
197
+ const record = this.captureObserver();
120
198
  const assertValid = this.validity();
121
199
  const { id, config, me } = await this.state();
122
200
  assertValid();
123
201
  if (message.to !== id) fail('recipient session ID mismatch (stale endpoint)');
124
202
  const sender = config.agents.find(a => a.sessionId === message.from);
125
203
  const busy = this.host.busy();
204
+ const metadata = { peerSessionId: message.from, peerName: sender?.name, correlationId: message.correlationId, kind: message.kind, busy };
126
205
  const notify = (purpose: string, content: string) => {
127
206
  assertValid();
128
- this.host.deliver(`[Intercom ${purpose} from ${sender?.name ?? 'unconfigured worker'} (${message.from})]\n${content}`, busy);
207
+ record('host.submission', { ...metadata, outcome: 'attempted' });
208
+ try {
209
+ this.host.deliver(`[Intercom ${purpose} from ${sender?.name ?? 'unconfigured worker'} (${message.from})]\n${content}`, busy);
210
+ // A synchronous return is not Pi queue acceptance or model completion.
211
+ record('host.submission', { ...metadata, outcome: 'returned' });
212
+ } catch (error) {
213
+ record('host.submission', { ...metadata, outcome: 'failed', errorCode: observationError(error) });
214
+ throw error;
215
+ }
129
216
  };
130
217
  if (message.kind === 'registration' || message.kind === 'status') {
131
218
  requireCoordinator(config, id);
@@ -133,12 +220,15 @@ export class Intercom {
133
220
  if (message.kind === 'registration') {
134
221
  const projectDirectory = await directory(this.store.root, message.payload.projectDirectory as string);
135
222
  // No pending map, placeholder or configuration mutation.
223
+ record('registration.received', { ...metadata, port: message.payload.port as number });
136
224
  notify('registration', JSON.stringify({ sessionId: message.from, port: message.payload.port, projectDirectory }) + '\nThis is connection information, not a work assignment. Configure explicitly, then reload separately.');
137
225
  } else {
138
226
  if (sender) await this.store.update(id, c => {
139
227
  const entry = c.agents.find(a => a.sessionId === message.from && !a.coordinator);
140
228
  if (entry) entry.port = message.payload.port as number;
141
229
  }, assertValid);
230
+ record('status.received', { ...metadata, busy: message.payload.busy as boolean, port: message.payload.port as number });
231
+ if (sender) record('config.changed', { operation: 'port_update', peerSessionId: message.from, port: message.payload.port as number, outcome: 'written' });
142
232
  notify('status', JSON.stringify({ sessionId: message.from, port: message.payload.port, busy: message.payload.busy, configured: !!sender }));
143
233
  }
144
234
  return;
@@ -166,12 +256,14 @@ export class Intercom {
166
256
  fail('unsupported control');
167
257
  }
168
258
  async tool(operation: string, args: Record<string, unknown>): Promise<unknown> {
259
+ const record = this.captureObserver();
169
260
  const assertValid = this.validity();
170
261
  const { id, config, me } = await this.state();
171
262
  assertValid();
172
263
  if (operation === 'list') {
173
264
  if (!me) fail('anonymous intercom_list permissions unresolved (TODO); no coordinator privileges');
174
- return config;
265
+ const dashboardPort = coordinator(config).dashboardPort;
266
+ return { ...config, dashboardUrl: dashboardPort === undefined ? null : `http://127.0.0.1:${dashboardPort}/` };
175
267
  }
176
268
  if (operation === 'report_status') { await this.report(); return { accepted: true }; }
177
269
  if (operation === 'send') {
@@ -182,15 +274,17 @@ export class Intercom {
182
274
  if (operation === 'stop_worker' || operation === 'close_worker') fail(UNSUPPORTED_CANCELLATION);
183
275
  if (operation === 'configure_worker') {
184
276
  await this.store.configure(id, args as unknown as Omit<Agent, 'coordinator'>, assertValid);
277
+ record('config.changed', { operation: 'configure_worker', peerSessionId: args.sessionId as string, peerName: args.name as string, outcome: 'written' });
185
278
  return { written: true, reloaded: false };
186
279
  }
187
280
  if (operation === 'set_multiplexer') {
188
- await this.store.update(id, c => { c.multiplexer = args.multiplexer as Config['multiplexer']; }, assertValid); return { written: true };
281
+ await this.store.update(id, c => { c.multiplexer = args.multiplexer as Config['multiplexer']; }, assertValid);
282
+ record('config.changed', { operation: 'set_multiplexer', outcome: 'written' }); return { written: true };
189
283
  }
190
284
  if (operation === 'create_worker') {
191
285
  const relative = await directory(this.store.root, (args.projectDirectory as string | undefined) ?? '.');
192
286
  assertValid();
193
- return this.options.launch({ multiplexer: config.multiplexer, cwd: path.resolve(this.store.root, relative) });
287
+ return this.launchObserved({ multiplexer: config.multiplexer, cwd: path.resolve(this.store.root, relative) });
194
288
  }
195
289
  const target = named(config, args.to as string);
196
290
  if (target.coordinator) fail('operation requires a worker target');
@@ -201,13 +295,27 @@ export class Intercom {
201
295
  if (operation === 'resume_worker') {
202
296
  const relative = await directory(this.store.root, target.projectDirectory);
203
297
  assertValid();
204
- return this.options.launch({ multiplexer: config.multiplexer, cwd: path.resolve(this.store.root, relative), sessionId: target.sessionId });
298
+ return this.launchObserved({ multiplexer: config.multiplexer, cwd: path.resolve(this.store.root, relative), sessionId: target.sessionId });
205
299
  }
206
300
  if (operation === 'remove_worker') {
207
301
  // No live process claim: the caller must explicitly ensure closure first.
208
302
  await this.store.update(id, c => { c.agents = c.agents.filter(a => a.sessionId !== target.sessionId); }, assertValid);
303
+ record('config.changed', { operation: 'remove_worker', peerSessionId: target.sessionId, peerName: target.name, outcome: 'removed' });
209
304
  return { removed: true, processStopped: false, sessionDeleted: false };
210
305
  }
211
306
  fail(`unknown operation ${operation}`);
212
307
  }
308
+ private async launchObserved(request: LaunchRequest): Promise<unknown> {
309
+ const record = this.captureObserver();
310
+ const metadata: EventMetadata = { operation: request.sessionId ? 'resume_worker' : 'create_worker', peerSessionId: request.sessionId };
311
+ record('launch.result', { ...metadata, outcome: 'attempted' });
312
+ try {
313
+ const result = await this.options.launch(request);
314
+ record('launch.result', { ...metadata, outcome: 'returned' });
315
+ return result;
316
+ } catch (error) {
317
+ record('launch.result', { ...metadata, outcome: 'failed', errorCode: observationError(error) });
318
+ throw error;
319
+ }
320
+ }
213
321
  }
package/src/transport.ts CHANGED
@@ -10,11 +10,14 @@ export interface Envelope {
10
10
  from: string;
11
11
  to: string;
12
12
  payload: Record<string, unknown>;
13
+ /** Optional diagnostics correlation only; never authentication or agent identity. */
14
+ correlationId?: string;
13
15
  }
14
16
  export function envelope(value: unknown): Envelope {
15
17
  const m = value as Envelope;
16
18
  if (!m || m.version !== 1 || !['message', 'registration', 'status', 'request_status', 'reload', 'stop', 'close'].includes(m.kind)) fail('invalid wire schema/version/kind');
17
19
  text(m.from, 'sender sessionId', 256); text(m.to, 'recipient sessionId', 256);
20
+ if (m.correlationId !== undefined && (typeof m.correlationId !== 'string' || !/^[a-zA-Z0-9-]{1,128}$/.test(m.correlationId))) fail('invalid correlation ID');
18
21
  if (!m.payload || Array.isArray(m.payload) || typeof m.payload !== 'object') fail('invalid payload');
19
22
  if (m.kind === 'message') text(m.payload.message, 'message', 48000);
20
23
  if (m.kind === 'registration' || m.kind === 'status') port(m.payload.port);