@pontia/pi-client-plugin 0.2.0 → 0.2.2

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/index.ts CHANGED
@@ -1,19 +1,61 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { defaultHookLogFile, loadTurnContext, type EnvLike, type LoadTurnContextResult, type TurnContext } from "./context.js";
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { connectPi, RpcError, type PiConnection, type ControlInput } from "./control-socket.js";
4
+ import { defaultHookLogFile, type EnvLike, type TurnContext } from "./context.js";
3
5
  import { appendDiagnostic, type DiagnosticEntry } from "./diagnostics.js";
4
- import { pontiaHomeFromEnv, resolvePontiaConnection } from "./discovery.js";
5
- import { buildSessionContextUsageUpdatedEvent, buildSessionExitedEvent, buildSessionMessageUpdatedEvent, buildSessionReadyEvent, buildTurnCompletedEvent, buildTurnFailedEvent, buildTurnInterruptedEvent, buildTurnOutputEvent, buildTurnStartedEvent, contextUsageFromPiHook, type InternalEvent, type PiTopologyContext, type PiTopologyEntryKind, type SessionMessageUpdatedReason } from "./events.js";
6
- import { asRecord, optionalString, parseJsonResponse } from "./internal-api.js";
7
- import { hasTmuxPaneEnvironment, isPontiaManagedTmuxPane, loadPontiaManagedRuntimeIdentity, type ManagedRuntimeIdentity } from "./managed-runtime.js";
8
- import { agentEndWasInterrupted, assistantDeltaFromEvent, assistantTextFromMessage, errorMessageFromAgentEnd, isTranscriptBoundaryMessageUpdate, lastAssistantTextFromMessages } from "./pi-message.js";
6
+ import { pontiaHomeFromEnv } from "./discovery.js";
7
+ import {
8
+ buildSessionContextUsageUpdatedEvent,
9
+ buildSessionExitedEvent,
10
+ buildSessionMessageUpdatedEvent,
11
+ buildSessionReadyEvent,
12
+ buildTurnCompletedEvent,
13
+ buildTurnFailedEvent,
14
+ buildTurnInterruptedEvent,
15
+ buildTurnOutputEvent,
16
+ buildTurnStartedEvent,
17
+ contextUsageFromPiHook,
18
+ type InternalEvent,
19
+ type PiTopologyContext,
20
+ type PiTopologyEntryKind,
21
+ type SessionMessageUpdatedReason,
22
+ } from "./events.js";
23
+ import { asRecord, optionalString } from "./values.js";
24
+ import {
25
+ completeToolCallFromMessageUpdate,
26
+ LiveOutputPublisher,
27
+ type LiveOutputPublisherLike,
28
+ } from "./live-output.js";
29
+ import {
30
+ hasTmuxPaneEnvironment,
31
+ isPontiaManagedTmuxPane,
32
+ loadPontiaManagedRuntimeIdentity,
33
+ type ManagedRuntimeIdentity,
34
+ } from "./managed-runtime.js";
35
+ import {
36
+ agentEndWasInterrupted,
37
+ assistantDeltaFromEvent,
38
+ assistantTextFromMessage,
39
+ errorMessageFromAgentEnd,
40
+ isTranscriptBoundaryMessageUpdate,
41
+ lastAssistantTextFromMessages,
42
+ } from "./pi-message.js";
9
43
  import { loadProfileSystemPrompt } from "./profile.js";
10
44
  import { EventReporter, type EventReportResult } from "./reporter.js";
11
- import { bindSession, loadExistingSessionContext, piSessionDetailsFromHookContext, type PiSessionDetails } from "./runtime-binding.js";
45
+ import {
46
+ bindSession,
47
+ loadExistingSessionContext,
48
+ piSessionDetailsFromHookContext,
49
+ type PiSessionDetails,
50
+ } from "./runtime-binding.js";
12
51
  import type { SessionContext } from "./session.js";
13
52
  import { isActiveRegisteredWorkspace } from "./workspace.js";
14
53
 
15
54
  interface ReporterLike {
16
- report(context: { internalEventUrl: string }, event: InternalEvent): Promise<EventReportResult | boolean>;
55
+ report(
56
+ context: { runtimeInstanceId: string },
57
+ event: InternalEvent,
58
+ ): Promise<EventReportResult | boolean>;
17
59
  }
18
60
 
19
61
  function reportAccepted(result: EventReportResult | boolean): boolean {
@@ -28,18 +70,19 @@ function isPersistentTuiContext(ctx: unknown): boolean {
28
70
 
29
71
  export interface PontiaPiExtensionDependencies {
30
72
  env?: EnvLike;
31
- loadContext?: (env: EnvLike, sessionContext?: SessionContext) => Promise<LoadTurnContextResult>;
32
73
  makeReporter?: (logFile: string) => ReporterLike;
33
74
  logDiagnostic?: (logFile: string, entry: DiagnosticEntry) => Promise<void>;
34
- fetch?: typeof fetch;
35
75
  loadManagedRuntime?: (env: EnvLike) => Promise<ManagedRuntimeIdentity | undefined>;
36
76
  isManagedPane?: (env: EnvLike) => Promise<boolean>;
77
+ makeLiveOutputPublisher?: (context: TurnContext & { turnId: string }) => LiveOutputPublisherLike;
78
+ connectPi?: typeof connectPi;
37
79
  }
38
80
 
39
81
  interface ActiveTurnState {
40
82
  context: TurnContext & { turnId: string };
41
83
  logFile: string;
42
84
  reporter: ReporterLike;
85
+ liveOutput: LiveOutputPublisherLike;
43
86
  output: string;
44
87
  ended: boolean;
45
88
  }
@@ -68,13 +111,12 @@ const NON_MESSAGE_ENTRY_KINDS = new Set([
68
111
  function topologyEntryKind(entry: Record<string, unknown>): PiTopologyEntryKind {
69
112
  if (entry.type !== "message") {
70
113
  return typeof entry.type === "string" && NON_MESSAGE_ENTRY_KINDS.has(entry.type)
71
- ? entry.type as PiTopologyEntryKind
114
+ ? (entry.type as PiTopologyEntryKind)
72
115
  : "other";
73
116
  }
74
117
  const message = entry.message;
75
- const role = message && typeof message === "object"
76
- ? (message as Record<string, unknown>).role
77
- : undefined;
118
+ const role =
119
+ message && typeof message === "object" ? (message as Record<string, unknown>).role : undefined;
78
120
  if (role === "user") return "user_message";
79
121
  if (role === "assistant") return "assistant_message";
80
122
  if (role === "toolResult") return "tool_result_message";
@@ -103,7 +145,10 @@ function topologyContextFromHookContext(ctx: unknown): PiTopologyContext | undef
103
145
  }
104
146
  }
105
147
 
106
- export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPiExtensionDependencies = {}): void {
148
+ export function createPontiaPiExtension(
149
+ pi: ExtensionAPI,
150
+ dependencies: PontiaPiExtensionDependencies = {},
151
+ ): void {
107
152
  const sourceEnv = dependencies.env ?? process.env;
108
153
  if (!pontiaHomeFromEnv(sourceEnv) || !hasTmuxPaneEnvironment(sourceEnv)) return;
109
154
  // Environment extensions may load PONTIA_HOME after extension registration.
@@ -111,12 +156,26 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
111
156
  const currentEnv = () => ({ ...sourceEnv, PONTIA_HOME: currentPontiaHome() });
112
157
  const currentHookLogFile = () => defaultHookLogFile(currentPontiaHome());
113
158
 
114
- const contextLoader = dependencies.loadContext ?? ((contextEnv, sessionContext) => loadTurnContext(contextEnv, { sessionContext }));
115
- const makeReporter = dependencies.makeReporter ?? ((logFile: string) => new EventReporter({ logFile }));
159
+ const rpcConnection: Pick<PiConnection, "request"> = {
160
+ request(method, params) {
161
+ if (!controlSocket) return Promise.reject(new Error("Pi connection is unavailable"));
162
+ return controlSocket.request(method, params);
163
+ },
164
+ };
165
+ const makeReporter =
166
+ dependencies.makeReporter ??
167
+ ((logFile: string) =>
168
+ new EventReporter({
169
+ logFile,
170
+ connection: rpcConnection,
171
+ }));
116
172
  const logDiagnostic = dependencies.logDiagnostic ?? appendDiagnostic;
117
- const fetchImpl = dependencies.fetch ?? fetch;
118
173
  const loadManagedRuntime = dependencies.loadManagedRuntime ?? loadPontiaManagedRuntimeIdentity;
119
174
  const isManagedPane = dependencies.isManagedPane ?? isPontiaManagedTmuxPane;
175
+ const makeLiveOutputPublisher =
176
+ dependencies.makeLiveOutputPublisher ??
177
+ ((context: TurnContext & { turnId: string }) =>
178
+ new LiveOutputPublisher(context, { connection: rpcConnection }));
120
179
 
121
180
  let activeTurn: ActiveTurnState | undefined;
122
181
  let readyReported = false;
@@ -126,6 +185,169 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
126
185
  let managedPaneConfirmed = false;
127
186
  let lastContextUsageJson: string | undefined;
128
187
  let pendingPrompt: string | undefined;
188
+ let controlSocket: PiConnection | undefined;
189
+ let controlGeneration = 0;
190
+ let piContext: ExtensionContext | undefined;
191
+ let changingModel = false;
192
+ const directInput = new AsyncLocalStorage<{
193
+ submission: ControlInput;
194
+ sessionContext: SessionContext;
195
+ consumed: boolean;
196
+ }>();
197
+ let queuedControlInput: ControlInput | undefined;
198
+
199
+ const modelId = (model: { provider: string; id: string }) => `${model.provider}/${model.id}`;
200
+
201
+ async function reportModel(): Promise<void> {
202
+ const context = boundSessionContext;
203
+ const model = piContext?.model;
204
+ if (!context || !model || !readyReported || reportingDisabled) return;
205
+ const result = await makeReporter(currentHookLogFile()).report(context, {
206
+ session_id: context.sessionId,
207
+ type: "session.model_updated",
208
+ data: { model: modelId(model), runtime_instance_id: context.runtimeInstanceId },
209
+ });
210
+ if (!reportAccepted(result)) throw new Error("Pi model observation was not accepted");
211
+ }
212
+
213
+ function sendControlInput(submission: ControlInput): void {
214
+ if (!boundSessionContext) throw new Error("Pi session is not bound");
215
+ try {
216
+ directInput.run({ submission, sessionContext: boundSessionContext, consumed: false }, () =>
217
+ pi.sendUserMessage(submission.input),
218
+ );
219
+ } catch (error) {
220
+ throw new RpcError(
221
+ -32007,
222
+ `Pi input delivery uncertain: ${error instanceof Error ? error.message : String(error)}`,
223
+ );
224
+ }
225
+ }
226
+
227
+ async function controlError(error: unknown): Promise<void> {
228
+ await logDiagnostic(currentHookLogFile(), {
229
+ level: "warn",
230
+ code: "pi_control_unavailable",
231
+ message: "Pi control channel failed",
232
+ details: error instanceof Error ? error.message : String(error),
233
+ });
234
+ }
235
+
236
+ async function closeControlSocket(): Promise<void> {
237
+ controlGeneration += 1;
238
+ queuedControlInput = undefined;
239
+ const socket = controlSocket;
240
+ controlSocket = undefined;
241
+ if (socket) await socket.close().catch(controlError);
242
+ }
243
+
244
+ async function registrationConnection(): Promise<PiConnection> {
245
+ if (controlSocket) return controlSocket;
246
+ const generation = controlGeneration;
247
+ const lifecycleContext = () => {
248
+ if (
249
+ generation !== controlGeneration ||
250
+ reportingDisabled ||
251
+ !boundSessionContext ||
252
+ !piContext ||
253
+ !readyReported
254
+ ) {
255
+ throw new Error("Pi lifecycle control session is no longer current");
256
+ }
257
+ return piContext;
258
+ };
259
+ const socket = await (dependencies.connectPi ?? connectPi)(
260
+ currentPontiaHome(),
261
+ (error) => {
262
+ void controlError(error).catch(() => {});
263
+ },
264
+ (submission) => {
265
+ if (generation !== controlGeneration || reportingDisabled || !boundSessionContext) {
266
+ throw new Error("Pi control session is no longer current");
267
+ }
268
+ if (queuedControlInput)
269
+ throw new RpcError(-32010, "Pi already has pending input; input was not submitted");
270
+ if (!piContext?.isIdle()) {
271
+ if (!activeTurn?.ended) throw new RpcError(-32010, "Pi is busy; input was not submitted");
272
+ queuedControlInput = submission;
273
+ return;
274
+ }
275
+ sendControlInput(submission);
276
+ },
277
+ {
278
+ listModels() {
279
+ if (
280
+ generation !== controlGeneration ||
281
+ reportingDisabled ||
282
+ !boundSessionContext ||
283
+ !piContext ||
284
+ !readyReported
285
+ ) {
286
+ throw new Error("Pi model control session is no longer current");
287
+ }
288
+ return piContext.modelRegistry.getAvailable().map((model) => ({
289
+ id: modelId(model),
290
+ name: model.name,
291
+ description: model.provider,
292
+ }));
293
+ },
294
+ async setModel(id) {
295
+ this.listModels();
296
+ if (changingModel) throw new Error("Pi already has a model change in progress");
297
+ const model = piContext!.modelRegistry
298
+ .getAvailable()
299
+ .find((model) => modelId(model) === id);
300
+ if (!model) throw new Error("The selected model is not available");
301
+ changingModel = true;
302
+ try {
303
+ if (!(await pi.setModel(model)))
304
+ throw new RpcError(-32006, "Pi could not authenticate the selected model");
305
+ this.listModels();
306
+ // Selecting the current model does not emit model_select; observe it again.
307
+ await reportModel();
308
+ } catch (error) {
309
+ if (error instanceof RpcError) throw error;
310
+ throw new RpcError(
311
+ -32007,
312
+ `Pi model change outcome is unknown: ${error instanceof Error ? error.message : String(error)}`,
313
+ );
314
+ } finally {
315
+ changingModel = false;
316
+ }
317
+ },
318
+ async onReconnect() {
319
+ if (generation !== controlGeneration)
320
+ throw new Error("Pi session changed during reconnect");
321
+ await reportModel();
322
+ },
323
+ },
324
+ (inboxMessageId) => {
325
+ if (generation !== controlGeneration || reportingDisabled || !boundSessionContext) {
326
+ throw new Error("Pi control session is no longer current");
327
+ }
328
+ // Command dispatch supplies the native context required by navigateTree.
329
+ pi.sendUserMessage(`/pontia-edit ${inboxMessageId}`, { expandPromptTemplates: true });
330
+ },
331
+ {
332
+ interrupt() {
333
+ lifecycleContext().abort();
334
+ },
335
+ shutdown() {
336
+ const ctx = lifecycleContext();
337
+ // Ordinary extension context: abort first, then exit after Pi settles.
338
+ // Unlike double Ctrl+C, this allows turn cleanup before session_shutdown.
339
+ ctx.abort();
340
+ ctx.shutdown();
341
+ },
342
+ },
343
+ );
344
+ if (generation !== controlGeneration) {
345
+ await socket.close();
346
+ throw new Error("Pi session changed during connection initialization");
347
+ }
348
+ controlSocket = socket;
349
+ return socket;
350
+ }
129
351
 
130
352
  async function confirmManagedPane(refresh = false): Promise<boolean> {
131
353
  if (managedPaneConfirmed && !refresh) return true;
@@ -137,13 +359,10 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
137
359
  if (boundSessionContext) return boundSessionContext;
138
360
  const runtimeIdentity = await loadManagedRuntime(currentEnv());
139
361
  if (!runtimeIdentity) return undefined;
140
- const connection = await resolvePontiaConnection({ pontiaHome: currentPontiaHome(), fetch: fetchImpl });
141
- if (!connection?.internalEventUrl) return undefined;
142
362
  return {
143
363
  sessionId: runtimeIdentity.sessionId,
144
364
  runtimeInstanceId: runtimeIdentity.runtimeInstanceId,
145
365
  clientType: "pi",
146
- internalEventUrl: connection.internalEventUrl,
147
366
  };
148
367
  }
149
368
 
@@ -172,41 +391,18 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
172
391
  return;
173
392
  }
174
393
  const loaded = { logFile };
175
- const connection = await resolvePontiaConnection({ pontiaHome: currentPontiaHome() });
176
- if (!connection?.externalApiToken) {
177
- await logDiagnostic(loaded.logFile, {
178
- level: "error",
179
- code: "branch_replay_stale_context",
180
- message: "pontia-edit requires an authenticated Pontia connection",
181
- });
182
- return;
183
- }
184
- const resolveUrl = commandContext.internalEventUrl.replace(
185
- /\/events\/?$/,
186
- "/inbox/branch-replay/resolve",
187
- );
188
394
 
189
395
  let replacementInput: string;
190
396
  let targetEntryId: string;
191
397
  try {
192
- const response = await fetchImpl(resolveUrl, {
193
- method: "POST",
194
- headers: {
195
- Authorization: `Bearer ${connection.externalApiToken}`,
196
- "Content-Type": "application/json",
197
- },
198
- body: JSON.stringify({
199
- inbox_message_id: inboxMessageId,
200
- session_id: commandContext.sessionId,
201
- runtime_instance_id: commandContext.runtimeInstanceId,
202
- client_type: "pi",
203
- }),
398
+ const connection = await registrationConnection();
399
+ const body = await connection.request("branch.resolve", {
400
+ inbox_message_id: inboxMessageId,
401
+ session_id: commandContext.sessionId,
402
+ runtime_instance_id: commandContext.runtimeInstanceId,
403
+ client_type: "pi",
204
404
  });
205
- const body = await parseJsonResponse(response);
206
- if (!response.ok) {
207
- throw new Error(`${response.status} ${response.statusText}`);
208
- }
209
- const replay = asRecord(asRecord(asRecord(body)?.data)?.branch_replay);
405
+ const replay = asRecord(asRecord(body)?.branch_replay);
210
406
  replacementInput = optionalString(replay?.replacement_input) ?? "";
211
407
  targetEntryId = optionalString(replay?.target_entry_id) ?? "";
212
408
  if (
@@ -253,7 +449,21 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
253
449
 
254
450
  try {
255
451
  ctx.ui.setEditorText("");
256
- await pi.sendUserMessage(replacementInput);
452
+ const current = await currentManagedSessionContext();
453
+ if (
454
+ current?.sessionId !== commandContext.sessionId ||
455
+ current?.runtimeInstanceId !== commandContext.runtimeInstanceId
456
+ ) {
457
+ throw new Error("branch replay Runtime is no longer current");
458
+ }
459
+ await directInput.run(
460
+ {
461
+ submission: { input: replacementInput, inboxMessageId },
462
+ sessionContext: current,
463
+ consumed: false,
464
+ },
465
+ () => pi.sendUserMessage(replacementInput),
466
+ );
257
467
  } catch (error) {
258
468
  await logDiagnostic(loaded.logFile, {
259
469
  level: "error",
@@ -267,11 +477,17 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
267
477
 
268
478
  async function scheduleMessageRefresh(reason: SessionMessageUpdatedReason): Promise<void> {
269
479
  if (!activeTurn || activeTurn.ended) return;
270
- await activeTurn.reporter.report(activeTurn.context, buildSessionMessageUpdatedEvent(activeTurn.context, reason));
480
+ await activeTurn.reporter.report(
481
+ activeTurn.context,
482
+ buildSessionMessageUpdatedEvent(activeTurn.context, reason),
483
+ );
271
484
  }
272
485
 
273
486
  async function reportFinalMessageRefresh(state: ActiveTurnState): Promise<void> {
274
- await state.reporter.report(state.context, buildSessionMessageUpdatedEvent(state.context, "final"));
487
+ await state.reporter.report(
488
+ state.context,
489
+ buildSessionMessageUpdatedEvent(state.context, "final"),
490
+ );
275
491
  }
276
492
 
277
493
  async function reportContextUsageFromHookEvent(event: unknown, ctx?: unknown): Promise<void> {
@@ -281,24 +497,23 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
281
497
  const usageJson = JSON.stringify(observation);
282
498
  if (usageJson === lastContextUsageJson) return;
283
499
  lastContextUsageJson = usageJson;
284
- await activeTurn.reporter.report(activeTurn.context, buildSessionContextUsageUpdatedEvent(activeTurn.context, observation.context_usage, observation.model));
500
+ await activeTurn.reporter.report(
501
+ activeTurn.context,
502
+ buildSessionContextUsageUpdatedEvent(activeTurn.context, observation.context_usage),
503
+ );
285
504
  }
286
505
 
287
506
  pi.on("before_agent_start", async (event) => {
288
- const eventRecord = event as unknown as Record<string, unknown>;
289
- pendingPrompt = optionalString(eventRecord.prompt);
290
- if (reportingDisabled || !await confirmManagedPane()) {
291
- return { systemPrompt: typeof eventRecord.systemPrompt === "string" ? eventRecord.systemPrompt : "" };
292
- }
293
- const currentSystemPrompt = typeof eventRecord.systemPrompt === "string" ? eventRecord.systemPrompt : "";
507
+ pendingPrompt = event.prompt;
508
+ if (reportingDisabled || !(await confirmManagedPane())) return;
294
509
  try {
295
510
  const profilePrompt = await loadProfileSystemPrompt(
296
- currentPontiaHome(),
297
- fetchImpl,
511
+ rpcConnection,
298
512
  boundSessionContext?.sessionId,
299
513
  );
300
- if (!profilePrompt) return { systemPrompt: currentSystemPrompt };
301
- return { systemPrompt: `${currentSystemPrompt}\n\n${profilePrompt}` };
514
+ if (profilePrompt) {
515
+ event.systemPromptOptions.sections.pontia_execution_profile = profilePrompt;
516
+ }
302
517
  } catch (error) {
303
518
  await logDiagnostic(currentHookLogFile(), {
304
519
  level: "warn",
@@ -306,15 +521,30 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
306
521
  message: "failed to append pontia execution profile system prompt",
307
522
  details: error instanceof Error ? error.message : String(error),
308
523
  });
309
- return { systemPrompt: currentSystemPrompt };
310
524
  }
311
525
  });
312
526
 
527
+ pi.on("model_select", async (_event, ctx) => {
528
+ piContext = ctx;
529
+ await reportModel().catch(controlError);
530
+ });
531
+
313
532
  pi.on("session_start", async (event, ctx) => {
314
533
  if (reportingDisabled) return;
315
534
  const reason = (event as unknown as Record<string, unknown> | undefined)?.reason;
316
- if (readyReported && reason !== "fork" && reason !== "resume") return;
317
- if (reason !== "startup" && reason !== "new" && reason !== "resume" && reason !== "fork") return;
535
+ if (readyReported && reason !== "fork" && reason !== "resume" && reason !== "new") return;
536
+ if (
537
+ reason !== "startup" &&
538
+ reason !== "reload" &&
539
+ reason !== "new" &&
540
+ reason !== "resume" &&
541
+ reason !== "fork"
542
+ )
543
+ return;
544
+ await closeControlSocket();
545
+ const parentSessionId = boundSessionContext?.sessionId;
546
+ boundSessionContext = undefined;
547
+ readyReported = false;
318
548
  if (!isPersistentTuiContext(ctx)) {
319
549
  reportingDisabled = true;
320
550
  return;
@@ -325,26 +555,28 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
325
555
  deferredManualSessionDetails = sessionDetails;
326
556
  const logFile = currentHookLogFile();
327
557
  let context: SessionContext | undefined;
328
- const pontiaHome = currentPontiaHome();
329
558
  const env = currentEnv();
330
559
 
331
- const workspaceActive = await isActiveRegisteredWorkspace(pontiaHome, fetchImpl, sessionDetails.clientCwd);
332
- if (workspaceActive !== true) {
560
+ const workspaceActive = await isActiveRegisteredWorkspace(
561
+ await registrationConnection(),
562
+ sessionDetails.clientCwd,
563
+ );
564
+ if (!workspaceActive) {
565
+ await closeControlSocket();
333
566
  reportingDisabled = true;
334
567
  await logDiagnostic(logFile, {
335
568
  level: "info",
336
- code: workspaceActive === false ? "workspace_not_active" : "workspace_check_unavailable",
337
- message: workspaceActive === false
338
- ? "current pi workspace is not an active registered pontia workspace; pontia reporting disabled"
339
- : "could not verify active registered pontia workspace; pontia reporting disabled",
569
+ code: "workspace_not_active",
570
+ message:
571
+ "current pi workspace is not an active registered pontia workspace; pontia reporting disabled",
340
572
  details: { client_cwd: sessionDetails.clientCwd },
341
573
  });
342
574
  return;
343
575
  }
344
576
 
345
577
  if (reason === "fork") {
346
- const parentSessionId = boundSessionContext?.sessionId;
347
578
  if (!parentSessionId) {
579
+ await closeControlSocket();
348
580
  await logDiagnostic(logFile, {
349
581
  level: "error",
350
582
  code: "missing_fork_parent_session",
@@ -352,21 +584,42 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
352
584
  });
353
585
  return;
354
586
  }
355
- context = await bindSession(pontiaHome, env, fetchImpl, sessionDetails, { startKind: "fork", parentSessionId });
587
+ context = await bindSession(await registrationConnection(), env, sessionDetails, {
588
+ startKind: "fork",
589
+ parentSessionId,
590
+ });
356
591
  readyReported = false;
357
592
  } else {
358
- const existingSession = await loadExistingSessionContext(pontiaHome, fetchImpl, sessionDetails);
593
+ const existingSession = await loadExistingSessionContext(
594
+ await registrationConnection(),
595
+ sessionDetails,
596
+ );
597
+ if (reason === "reload" && !existingSession) {
598
+ reportingDisabled = true;
599
+ deferredManualSessionDetails = undefined;
600
+ await closeControlSocket();
601
+ await logDiagnostic(logFile, {
602
+ level: "error",
603
+ code: "reload_session_context_missing",
604
+ message: "pi reload could not recover the existing pontia runtime binding",
605
+ details: { client_session_key: sessionDetails.clientSessionKey },
606
+ });
607
+ return;
608
+ }
359
609
  if (
360
- existingSession
361
- && ["idle", "busy", "interrupted"].includes(existingSession.sessionState)
610
+ reason !== "reload" &&
611
+ existingSession &&
612
+ ["idle", "busy", "interrupted"].includes(existingSession.sessionState)
362
613
  ) {
614
+ await closeControlSocket();
363
615
  reportingDisabled = true;
364
616
  boundSessionContext = undefined;
365
617
  readyReported = false;
366
618
  await logDiagnostic(logFile, {
367
619
  level: "info",
368
620
  code: "duplicate_active_client_session",
369
- message: "native pi session is already bound to an active pontia session; duplicate TUI reporting disabled",
621
+ message:
622
+ "native pi session is already bound to an active pontia session; duplicate TUI reporting disabled",
370
623
  details: {
371
624
  client_session_key: sessionDetails.clientSessionKey,
372
625
  session_id: existingSession.sessionId,
@@ -375,22 +628,48 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
375
628
  });
376
629
  return;
377
630
  }
378
- if (!existingSession && !await confirmManagedPane()) {
631
+ if (!existingSession && !(await confirmManagedPane())) {
632
+ await closeControlSocket();
379
633
  boundSessionContext = undefined;
380
634
  readyReported = false;
381
635
  return;
382
636
  }
383
637
 
384
- context = await bindSession(pontiaHome, env, fetchImpl, sessionDetails, {
385
- runtimeInstanceId: existingSession?.runtimeInstanceId,
638
+ context = await bindSession(await registrationConnection(), env, sessionDetails, {
639
+ runtimeInstanceId:
640
+ reason === "reload" || existingSession?.sessionState === "starting"
641
+ ? existingSession?.runtimeInstanceId
642
+ : undefined,
386
643
  });
387
- if (reason === "resume" || reason === "new") readyReported = false;
644
+ if (
645
+ reason === "reload" &&
646
+ (context?.sessionId !== existingSession!.sessionId ||
647
+ context.runtimeInstanceId !== existingSession!.runtimeInstanceId)
648
+ ) {
649
+ reportingDisabled = true;
650
+ deferredManualSessionDetails = undefined;
651
+ throw new Error("pi reload runtime binding changed identity");
652
+ }
653
+ if (reason === "reload" || reason === "resume" || reason === "new") readyReported = false;
654
+ }
655
+ if (!context || !(await confirmManagedPane(true))) {
656
+ if (reason === "reload") {
657
+ reportingDisabled = true;
658
+ deferredManualSessionDetails = undefined;
659
+ }
660
+ await closeControlSocket();
661
+ return;
388
662
  }
389
- if (!context || !await confirmManagedPane(true)) return;
390
663
 
391
664
  boundSessionContext = context;
392
- readyReported = reportAccepted(await makeReporter(logFile).report(context, buildSessionReadyEvent(context)));
665
+ piContext = ctx;
666
+ controlSocket!.registered(context);
667
+ readyReported = reportAccepted(
668
+ await makeReporter(logFile).report(context, buildSessionReadyEvent(context)),
669
+ );
670
+ await reportModel().catch(controlError);
393
671
  } catch (error) {
672
+ await closeControlSocket();
394
673
  const logFile = currentHookLogFile();
395
674
  await logDiagnostic(logFile, {
396
675
  level: "error",
@@ -402,14 +681,18 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
402
681
  });
403
682
 
404
683
  pi.on("session_shutdown", async (event) => {
405
- if (reportingDisabled || !await confirmManagedPane()) return;
406
- const reason = (event as unknown as Record<string, unknown> | undefined)?.reason;
407
- if (reason !== "quit" && reason !== "new" && reason !== "resume" && reason !== "fork") return;
408
-
684
+ piContext = undefined;
685
+ readyReported = false;
409
686
  try {
687
+ if (reportingDisabled || !(await confirmManagedPane())) return;
688
+ const reason = (event as unknown as Record<string, unknown> | undefined)?.reason;
689
+ if (reason !== "quit" && reason !== "new" && reason !== "resume" && reason !== "fork") return;
410
690
  const logFile = currentHookLogFile();
411
691
  if (!boundSessionContext) return;
412
- await makeReporter(logFile).report(boundSessionContext, buildSessionExitedEvent(boundSessionContext, reason));
692
+ await makeReporter(logFile).report(
693
+ boundSessionContext,
694
+ buildSessionExitedEvent(boundSessionContext, reason),
695
+ );
413
696
  } catch (error) {
414
697
  const logFile = currentHookLogFile();
415
698
  await logDiagnostic(logFile, {
@@ -418,6 +701,8 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
418
701
  message: "failed to report pontia exited signal",
419
702
  details: error instanceof Error ? error.message : String(error),
420
703
  });
704
+ } finally {
705
+ await closeControlSocket();
421
706
  }
422
707
  });
423
708
 
@@ -426,75 +711,81 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
426
711
  const previousLeafId = leafIdFromHookContext(ctx);
427
712
  if (reportingDisabled) return;
428
713
  try {
429
- const loaded = await contextLoader(currentEnv(), boundSessionContext);
430
- let turnContext: TurnContext | undefined;
431
- let logFile: string;
432
- if (loaded.ok) {
433
- turnContext = loaded.context;
434
- logFile = loaded.logFile;
435
- } else if (loaded.silent) {
436
- if (!loaded.logFile) {
714
+ const dispatch = directInput.getStore();
715
+ const submission =
716
+ dispatch && !dispatch.consumed && dispatch.sessionContext === boundSessionContext
717
+ ? dispatch.submission
718
+ : undefined;
719
+ if (dispatch) dispatch.consumed = true;
720
+ const logFile = currentHookLogFile();
721
+ if (!boundSessionContext) {
722
+ const hookSessionDetails = piSessionDetailsFromHookContext(ctx);
723
+ const sessionDetails = hookSessionDetails.clientSessionKey
724
+ ? hookSessionDetails
725
+ : deferredManualSessionDetails;
726
+ if (!sessionDetails) {
437
727
  activeTurn = undefined;
438
728
  pendingPrompt = undefined;
439
729
  return;
440
730
  }
441
- logFile = loaded.logFile;
442
- if (!boundSessionContext) {
443
- const hookSessionDetails = piSessionDetailsFromHookContext(ctx);
444
- const sessionDetails = hookSessionDetails.clientSessionKey ? hookSessionDetails : deferredManualSessionDetails;
445
- if (!sessionDetails) {
446
- activeTurn = undefined;
447
- pendingPrompt = undefined;
448
- return;
449
- }
450
- const pontiaHome = currentPontiaHome();
451
- const workspaceActive = await isActiveRegisteredWorkspace(pontiaHome, fetchImpl, sessionDetails.clientCwd);
452
- if (workspaceActive !== true) {
453
- reportingDisabled = true;
454
- await logDiagnostic(logFile, {
455
- level: "info",
456
- code: workspaceActive === false ? "workspace_not_active" : "workspace_check_unavailable",
457
- message: workspaceActive === false
458
- ? "current pi workspace is not an active registered pontia workspace; pontia reporting disabled"
459
- : "could not verify active registered pontia workspace; pontia reporting disabled",
460
- details: { client_cwd: sessionDetails.clientCwd },
461
- });
462
- activeTurn = undefined;
463
- pendingPrompt = undefined;
464
- return;
465
- }
466
- boundSessionContext = await bindSession(pontiaHome, currentEnv(), fetchImpl, sessionDetails);
467
- if (boundSessionContext && !readyReported) {
468
- readyReported = reportAccepted(await makeReporter(logFile).report(boundSessionContext, buildSessionReadyEvent(boundSessionContext)));
469
- }
470
- }
471
- if (boundSessionContext) {
472
- turnContext = {
473
- sessionId: boundSessionContext.sessionId,
474
- runtimeInstanceId: boundSessionContext.runtimeInstanceId,
475
- clientType: "pi",
476
- internalEventUrl: boundSessionContext.internalEventUrl,
477
- input: pendingPrompt,
478
- };
479
- } else {
731
+ const workspaceActive = await isActiveRegisteredWorkspace(
732
+ await registrationConnection(),
733
+ sessionDetails.clientCwd,
734
+ );
735
+ if (!workspaceActive) {
736
+ await closeControlSocket();
737
+ reportingDisabled = true;
738
+ await logDiagnostic(logFile, {
739
+ level: "info",
740
+ code: "workspace_not_active",
741
+ message:
742
+ "current pi workspace is not an active registered pontia workspace; pontia reporting disabled",
743
+ details: { client_cwd: sessionDetails.clientCwd },
744
+ });
480
745
  activeTurn = undefined;
481
746
  pendingPrompt = undefined;
482
747
  return;
483
748
  }
484
- } else {
749
+ boundSessionContext = await bindSession(
750
+ await registrationConnection(),
751
+ currentEnv(),
752
+ sessionDetails,
753
+ );
754
+ if (boundSessionContext && !readyReported) {
755
+ piContext = ctx;
756
+ controlSocket!.registered(boundSessionContext);
757
+ readyReported = reportAccepted(
758
+ await makeReporter(logFile).report(
759
+ boundSessionContext,
760
+ buildSessionReadyEvent(boundSessionContext),
761
+ ),
762
+ );
763
+ await reportModel().catch(controlError);
764
+ }
765
+ }
766
+ if (!boundSessionContext) {
485
767
  activeTurn = undefined;
486
- if (!loaded.silent && ctx?.hasUI) ctx.ui.notify(`pontia: ${loaded.reason}`, "warning");
768
+ pendingPrompt = undefined;
487
769
  return;
488
770
  }
771
+ const turnContext: TurnContext = {
772
+ sessionId: boundSessionContext.sessionId,
773
+ runtimeInstanceId: boundSessionContext.runtimeInstanceId,
774
+ clientType: "pi",
775
+ ...(submission ?? { input: pendingPrompt }),
776
+ };
489
777
 
490
778
  pendingPrompt = undefined;
491
- if (!await confirmManagedPane(true)) {
779
+ if (!(await confirmManagedPane(true))) {
492
780
  activeTurn = undefined;
493
781
  return;
494
782
  }
495
783
  const reporter = makeReporter(logFile);
496
784
  lastContextUsageJson = undefined;
497
- const started = await reporter.report(turnContext, buildTurnStartedEvent(turnContext, previousLeafId, topologyContext));
785
+ const started = await reporter.report(
786
+ turnContext,
787
+ buildTurnStartedEvent(turnContext, previousLeafId, topologyContext),
788
+ );
498
789
  const canonicalTurnId = typeof started === "boolean" ? turnContext.turnId : started.turnId;
499
790
  if (!reportAccepted(started) || !canonicalTurnId) {
500
791
  activeTurn = undefined;
@@ -505,14 +796,17 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
505
796
  });
506
797
  return;
507
798
  }
799
+ const activeContext = { ...turnContext, turnId: canonicalTurnId };
508
800
  activeTurn = {
509
- context: { ...turnContext, turnId: canonicalTurnId },
801
+ context: activeContext,
510
802
  logFile,
511
803
  reporter,
804
+ liveOutput: makeLiveOutputPublisher(activeContext),
512
805
  output: "",
513
806
  ended: false,
514
807
  };
515
808
  } catch (error) {
809
+ if (!boundSessionContext) await closeControlSocket();
516
810
  pendingPrompt = undefined;
517
811
  activeTurn = undefined;
518
812
  const logFile = currentHookLogFile();
@@ -529,66 +823,99 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
529
823
  if (!activeTurn || activeTurn.ended) return;
530
824
  await reportContextUsageFromHookEvent(event, ctx);
531
825
 
532
- const fullText = assistantTextFromMessage((event as unknown as Record<string, unknown> | undefined)?.message);
826
+ const delta = assistantDeltaFromEvent(event);
827
+ if (delta) activeTurn.liveOutput.appendText(delta);
828
+ const toolCall = completeToolCallFromMessageUpdate(event);
829
+ if (toolCall) activeTurn.liveOutput.appendToolCall(toolCall);
830
+
831
+ const fullText = assistantTextFromMessage(
832
+ (event as unknown as Record<string, unknown> | undefined)?.message,
833
+ );
533
834
  if (fullText) {
534
835
  activeTurn.output = fullText;
535
- } else {
536
- const delta = assistantDeltaFromEvent(event);
537
- if (delta) activeTurn.output += delta;
836
+ } else if (delta) {
837
+ activeTurn.output += delta;
538
838
  }
539
839
 
540
840
  if (isTranscriptBoundaryMessageUpdate(event)) await scheduleMessageRefresh("update");
541
841
  });
542
842
 
543
- pi.on("tool_execution_start", async () => {
544
- await scheduleMessageRefresh("update");
843
+ pi.on("tool_execution_start", async (event) => {
844
+ if (!event.parentToolCallId) await scheduleMessageRefresh("update");
545
845
  });
546
846
 
547
- pi.on("tool_execution_end", async () => {
548
- await scheduleMessageRefresh("update");
847
+ pi.on("tool_execution_end", async (event) => {
848
+ if (!event.parentToolCallId) await scheduleMessageRefresh("update");
549
849
  });
550
850
 
551
851
  pi.on("message_end", async (event, ctx) => {
552
852
  if (!activeTurn || activeTurn.ended) return;
553
853
  await reportContextUsageFromHookEvent(event, ctx);
554
- const fullText = assistantTextFromMessage((event as unknown as Record<string, unknown> | undefined)?.message);
854
+ const fullText = assistantTextFromMessage(
855
+ (event as unknown as Record<string, unknown> | undefined)?.message,
856
+ );
555
857
  if (fullText) activeTurn.output = fullText;
556
858
  await scheduleMessageRefresh("append");
557
859
  });
558
860
 
861
+ pi.on("agent_settled", (_event, ctx) => {
862
+ if (!queuedControlInput || !ctx.isIdle()) return;
863
+ const submission = queuedControlInput;
864
+ queuedControlInput = undefined;
865
+ sendControlInput(submission);
866
+ });
867
+
559
868
  pi.on("agent_end", async (event, ctx) => {
560
869
  if (!activeTurn || activeTurn.ended) return;
561
870
  await reportContextUsageFromHookEvent(event, ctx);
562
871
  activeTurn.ended = true;
563
872
 
564
873
  const state = activeTurn;
565
- const terminalLeafId = leafIdFromHookContext(ctx);
566
- if (agentEndWasInterrupted(event, ctx?.signal)) {
567
- await state.reporter.report(state.context, buildTurnInterruptedEvent(state.context, terminalLeafId));
568
- await reportFinalMessageRefresh(state);
569
- return;
570
- }
874
+ try {
875
+ const terminalLeafId = leafIdFromHookContext(ctx);
876
+ if (agentEndWasInterrupted(event, ctx?.signal)) {
877
+ await state.reporter.report(
878
+ state.context,
879
+ buildTurnInterruptedEvent(state.context, terminalLeafId),
880
+ );
881
+ await reportFinalMessageRefresh(state);
882
+ return;
883
+ }
571
884
 
572
- const failureMessage = errorMessageFromAgentEnd(event);
573
- if (failureMessage) {
574
- await state.reporter.report(state.context, buildTurnFailedEvent(state.context, failureMessage, terminalLeafId));
575
- await reportFinalMessageRefresh(state);
576
- return;
577
- }
885
+ const failureMessage = errorMessageFromAgentEnd(event);
886
+ if (failureMessage) {
887
+ await state.reporter.report(
888
+ state.context,
889
+ buildTurnFailedEvent(state.context, failureMessage, terminalLeafId),
890
+ );
891
+ await reportFinalMessageRefresh(state);
892
+ return;
893
+ }
578
894
 
579
- if (!state.output) {
580
- const finalText = lastAssistantTextFromMessages((event as unknown as Record<string, unknown> | undefined)?.messages);
581
- if (finalText) state.output = finalText;
582
- }
895
+ if (!state.output) {
896
+ const finalText = lastAssistantTextFromMessages(
897
+ (event as unknown as Record<string, unknown> | undefined)?.messages,
898
+ );
899
+ if (finalText) state.output = finalText;
900
+ }
583
901
 
584
- const output = state.output.trim();
585
- if (output.length > 0) {
586
- const outputResult = await state.reporter.report(state.context, buildTurnOutputEvent(state.context, output));
587
- if (!reportAccepted(outputResult)) return;
588
- }
902
+ const output = state.output.trim();
903
+ if (output.length > 0) {
904
+ const outputResult = await state.reporter.report(
905
+ state.context,
906
+ buildTurnOutputEvent(state.context, output),
907
+ );
908
+ if (!reportAccepted(outputResult)) return;
909
+ }
589
910
 
590
- await state.reporter.report(state.context, buildTurnCompletedEvent(state.context, terminalLeafId));
591
- await reportFinalMessageRefresh(state);
911
+ await state.reporter.report(
912
+ state.context,
913
+ buildTurnCompletedEvent(state.context, terminalLeafId),
914
+ );
915
+ await reportFinalMessageRefresh(state);
916
+ } finally {
917
+ await state.liveOutput.close();
918
+ }
592
919
  });
593
920
  }
594
921