@pontia/pi-client-plugin 0.2.1 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pontia/pi-client-plugin",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Pi extension that connects coding-agent sessions to Pontia.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -36,14 +36,14 @@
36
36
  "typecheck": "tsc --noEmit"
37
37
  },
38
38
  "devDependencies": {
39
- "@earendil-works/pi-coding-agent": ">=0.86.1 <1",
39
+ "@earendil-works/pi-coding-agent": "^1.0.0",
40
40
  "@types/node": "^22.15.3",
41
41
  "oxfmt": "^0.70.0",
42
42
  "typescript": "^5.8.3",
43
43
  "vitest": "^3.1.2"
44
44
  },
45
45
  "peerDependencies": {
46
- "@earendil-works/pi-coding-agent": ">=0.86.1 <1"
46
+ "@earendil-works/pi-coding-agent": "*"
47
47
  },
48
48
  "pi": {
49
49
  "extensions": [
package/src/context.ts CHANGED
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
  export interface TurnContext {
4
4
  sessionId: string;
5
5
  turnId?: string;
6
- runtimeInstanceId: string;
6
+ runtimeId: string;
7
7
  input?: string;
8
8
  inboxMessageId?: string;
9
9
  clientType: "pi";
@@ -1,7 +1,7 @@
1
1
  import { createConnection, type Socket } from "node:net";
2
2
  import { isAbsolute, join } from "node:path";
3
3
 
4
- export const CONTROL_VERSION = 6;
4
+ export const CONTROL_VERSION = 7;
5
5
  export const MAX_CONTROL_FRAME_BYTES = 64 * 1024;
6
6
  // Match the former HTTP event body limit, including the RPC envelope.
7
7
  export const MAX_RPC_FRAME_BYTES = 2 * 1024 * 1024 + 1024;
@@ -9,7 +9,7 @@ const REQUEST_TIMEOUT_MS = 5_000;
9
9
 
10
10
  export interface ControlIdentity {
11
11
  sessionId: string;
12
- runtimeInstanceId: string;
12
+ runtimeId: string;
13
13
  clientSessionKey?: string;
14
14
  }
15
15
  export interface ControlInput {
@@ -392,12 +392,12 @@ export async function connectPi(
392
392
  const result = (await peer.request("runtime.attach", {
393
393
  version: CONTROL_VERSION,
394
394
  session_id: identity!.sessionId,
395
- runtime_instance_id: identity!.runtimeInstanceId,
395
+ runtime_id: identity!.runtimeId,
396
396
  client_session_key: identity!.clientSessionKey,
397
397
  })) as Record<string, unknown>;
398
398
  if (
399
399
  result?.session_id !== identity!.sessionId ||
400
- result?.runtime_instance_id !== identity!.runtimeInstanceId
400
+ result?.runtime_id !== identity!.runtimeId
401
401
  ) {
402
402
  throw new RpcError(-32009, "Pi reconnect identity mismatch");
403
403
  }
@@ -425,15 +425,12 @@ export async function connectPi(
425
425
  if (method === "turn.startFailure") {
426
426
  if (!identity?.clientSessionKey || stopped)
427
427
  throw new Error("Pi reporting identity is unavailable");
428
- const peer = await open(true);
429
- try {
430
- return await peer.request(method, {
431
- ...params,
432
- client_session_key: identity.clientSessionKey,
433
- });
434
- } finally {
435
- peer.close();
436
- }
428
+ await ready();
429
+ if (!current || stopped) throw new Error("Pi connection is unavailable");
430
+ return current.request(method, {
431
+ ...params,
432
+ client_session_key: identity.clientSessionKey,
433
+ });
437
434
  }
438
435
  await ready();
439
436
  if (!current || stopped) return Promise.reject(new Error("Pi connection is unavailable"));
package/src/events.ts CHANGED
@@ -329,7 +329,7 @@ export function buildTurnStartedEvent(
329
329
  topologyContext?: PiTopologyContext,
330
330
  ): InternalEvent {
331
331
  const data: Record<string, unknown> = {
332
- runtime_instance_id: context.runtimeInstanceId,
332
+ runtime_id: context.runtimeId,
333
333
  input_summary:
334
334
  context.input === undefined
335
335
  ? undefined
@@ -378,7 +378,7 @@ export function buildTurnInterruptedEvent(
378
378
 
379
379
  export function buildSessionReadyEvent(context: SessionContext): InternalEvent {
380
380
  const payload: Record<string, unknown> = {
381
- runtime_instance_id: context.runtimeInstanceId,
381
+ runtime_id: context.runtimeId,
382
382
  };
383
383
  if (context.clientSessionKey) payload.client_session_key = context.clientSessionKey;
384
384
  if (context.clientSessionFile) payload.client_session_file = context.clientSessionFile;
@@ -398,7 +398,7 @@ export function buildSessionExitedEvent(context: SessionContext, reason: string)
398
398
  type: "session.exited",
399
399
  data: {
400
400
  reason,
401
- runtime_instance_id: context.runtimeInstanceId,
401
+ runtime_id: context.runtimeId,
402
402
  },
403
403
  };
404
404
  }
package/src/index.ts CHANGED
@@ -53,7 +53,7 @@ import { isActiveRegisteredWorkspace } from "./workspace.js";
53
53
 
54
54
  interface ReporterLike {
55
55
  report(
56
- context: { runtimeInstanceId: string },
56
+ context: { runtimeId: string },
57
57
  event: InternalEvent,
58
58
  ): Promise<EventReportResult | boolean>;
59
59
  }
@@ -205,7 +205,7 @@ export function createPontiaPiExtension(
205
205
  const result = await makeReporter(currentHookLogFile()).report(context, {
206
206
  session_id: context.sessionId,
207
207
  type: "session.model_updated",
208
- data: { model: modelId(model), runtime_instance_id: context.runtimeInstanceId },
208
+ data: { model: modelId(model), runtime_id: context.runtimeId },
209
209
  });
210
210
  if (!reportAccepted(result)) throw new Error("Pi model observation was not accepted");
211
211
  }
@@ -361,7 +361,7 @@ export function createPontiaPiExtension(
361
361
  if (!runtimeIdentity) return undefined;
362
362
  return {
363
363
  sessionId: runtimeIdentity.sessionId,
364
- runtimeInstanceId: runtimeIdentity.runtimeInstanceId,
364
+ runtimeId: runtimeIdentity.runtimeId,
365
365
  clientType: "pi",
366
366
  };
367
367
  }
@@ -399,7 +399,7 @@ export function createPontiaPiExtension(
399
399
  const body = await connection.request("branch.resolve", {
400
400
  inbox_message_id: inboxMessageId,
401
401
  session_id: commandContext.sessionId,
402
- runtime_instance_id: commandContext.runtimeInstanceId,
402
+ runtime_id: commandContext.runtimeId,
403
403
  client_type: "pi",
404
404
  });
405
405
  const replay = asRecord(asRecord(body)?.branch_replay);
@@ -408,7 +408,7 @@ export function createPontiaPiExtension(
408
408
  if (
409
409
  optionalString(replay?.inbox_message_id) !== inboxMessageId ||
410
410
  optionalString(replay?.session_id) !== commandContext.sessionId ||
411
- optionalString(replay?.runtime_instance_id) !== commandContext.runtimeInstanceId ||
411
+ optionalString(replay?.runtime_id) !== commandContext.runtimeId ||
412
412
  optionalString(replay?.client_type) !== "pi" ||
413
413
  !replacementInput ||
414
414
  !targetEntryId
@@ -452,7 +452,7 @@ export function createPontiaPiExtension(
452
452
  const current = await currentManagedSessionContext();
453
453
  if (
454
454
  current?.sessionId !== commandContext.sessionId ||
455
- current?.runtimeInstanceId !== commandContext.runtimeInstanceId
455
+ current?.runtimeId !== commandContext.runtimeId
456
456
  ) {
457
457
  throw new Error("branch replay Runtime is no longer current");
458
458
  }
@@ -504,22 +504,16 @@ export function createPontiaPiExtension(
504
504
  }
505
505
 
506
506
  pi.on("before_agent_start", async (event) => {
507
- const eventRecord = event as unknown as Record<string, unknown>;
508
- pendingPrompt = optionalString(eventRecord.prompt);
509
- if (reportingDisabled || !(await confirmManagedPane())) {
510
- return {
511
- systemPrompt: typeof eventRecord.systemPrompt === "string" ? eventRecord.systemPrompt : "",
512
- };
513
- }
514
- const currentSystemPrompt =
515
- typeof eventRecord.systemPrompt === "string" ? eventRecord.systemPrompt : "";
507
+ pendingPrompt = event.prompt;
508
+ if (reportingDisabled || !(await confirmManagedPane())) return;
516
509
  try {
517
510
  const profilePrompt = await loadProfileSystemPrompt(
518
511
  rpcConnection,
519
512
  boundSessionContext?.sessionId,
520
513
  );
521
- if (!profilePrompt) return { systemPrompt: currentSystemPrompt };
522
- return { systemPrompt: `${currentSystemPrompt}\n\n${profilePrompt}` };
514
+ if (profilePrompt) {
515
+ event.systemPromptOptions.sections.pontia_execution_profile = profilePrompt;
516
+ }
523
517
  } catch (error) {
524
518
  await logDiagnostic(currentHookLogFile(), {
525
519
  level: "warn",
@@ -527,7 +521,6 @@ export function createPontiaPiExtension(
527
521
  message: "failed to append pontia execution profile system prompt",
528
522
  details: error instanceof Error ? error.message : String(error),
529
523
  });
530
- return { systemPrompt: currentSystemPrompt };
531
524
  }
532
525
  });
533
526
 
@@ -540,7 +533,13 @@ export function createPontiaPiExtension(
540
533
  if (reportingDisabled) return;
541
534
  const reason = (event as unknown as Record<string, unknown> | undefined)?.reason;
542
535
  if (readyReported && reason !== "fork" && reason !== "resume" && reason !== "new") return;
543
- if (reason !== "startup" && reason !== "new" && reason !== "resume" && reason !== "fork")
536
+ if (
537
+ reason !== "startup" &&
538
+ reason !== "reload" &&
539
+ reason !== "new" &&
540
+ reason !== "resume" &&
541
+ reason !== "fork"
542
+ )
544
543
  return;
545
544
  await closeControlSocket();
546
545
  const parentSessionId = boundSessionContext?.sessionId;
@@ -595,7 +594,20 @@ export function createPontiaPiExtension(
595
594
  await registrationConnection(),
596
595
  sessionDetails,
597
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
+ }
598
609
  if (
610
+ reason !== "reload" &&
599
611
  existingSession &&
600
612
  ["idle", "busy", "interrupted"].includes(existingSession.sessionState)
601
613
  ) {
@@ -624,14 +636,27 @@ export function createPontiaPiExtension(
624
636
  }
625
637
 
626
638
  context = await bindSession(await registrationConnection(), env, sessionDetails, {
627
- runtimeInstanceId:
628
- existingSession?.sessionState === "starting"
629
- ? existingSession.runtimeInstanceId
639
+ runtimeId:
640
+ reason === "reload" || existingSession?.sessionState === "starting"
641
+ ? existingSession?.runtimeId
630
642
  : undefined,
631
643
  });
632
- if (reason === "resume" || reason === "new") readyReported = false;
644
+ if (
645
+ reason === "reload" &&
646
+ (context?.sessionId !== existingSession!.sessionId ||
647
+ context.runtimeId !== existingSession!.runtimeId)
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;
633
654
  }
634
655
  if (!context || !(await confirmManagedPane(true))) {
656
+ if (reason === "reload") {
657
+ reportingDisabled = true;
658
+ deferredManualSessionDetails = undefined;
659
+ }
635
660
  await closeControlSocket();
636
661
  return;
637
662
  }
@@ -745,7 +770,7 @@ export function createPontiaPiExtension(
745
770
  }
746
771
  const turnContext: TurnContext = {
747
772
  sessionId: boundSessionContext.sessionId,
748
- runtimeInstanceId: boundSessionContext.runtimeInstanceId,
773
+ runtimeId: boundSessionContext.runtimeId,
749
774
  clientType: "pi",
750
775
  ...(submission ?? { input: pendingPrompt }),
751
776
  };
@@ -815,12 +840,12 @@ export function createPontiaPiExtension(
815
840
  if (isTranscriptBoundaryMessageUpdate(event)) await scheduleMessageRefresh("update");
816
841
  });
817
842
 
818
- pi.on("tool_execution_start", async () => {
819
- await scheduleMessageRefresh("update");
843
+ pi.on("tool_execution_start", async (event) => {
844
+ if (!event.parentToolCallId) await scheduleMessageRefresh("update");
820
845
  });
821
846
 
822
- pi.on("tool_execution_end", async () => {
823
- await scheduleMessageRefresh("update");
847
+ pi.on("tool_execution_end", async (event) => {
848
+ if (!event.parentToolCallId) await scheduleMessageRefresh("update");
824
849
  });
825
850
 
826
851
  pi.on("message_end", async (event, ctx) => {
@@ -236,7 +236,7 @@ export class LiveOutputPublisher implements LiveOutputPublisherLike {
236
236
  return {
237
237
  session_id: this.context.sessionId,
238
238
  turn_id: this.context.turnId,
239
- runtime_instance_id: this.context.runtimeInstanceId,
239
+ runtime_id: this.context.runtimeId,
240
240
  stream_id: this.streamId,
241
241
  };
242
242
  }
@@ -6,7 +6,7 @@ const execFileAsync = promisify(execFile);
6
6
 
7
7
  export interface ManagedRuntimeIdentity {
8
8
  sessionId: string;
9
- runtimeInstanceId: string;
9
+ runtimeId: string;
10
10
  }
11
11
 
12
12
  export function hasTmuxPaneEnvironment(env: EnvLike = process.env): boolean {
@@ -43,12 +43,12 @@ export async function loadPontiaManagedRuntimeIdentity(
43
43
  const socketPath = env.TMUX!.trim().split(",", 1)[0]!.trim();
44
44
  const paneId = env.TMUX_PANE!.trim();
45
45
 
46
- const [sessionId, runtimeInstanceId] = await Promise.all([
46
+ const [sessionId, runtimeId] = await Promise.all([
47
47
  paneOption(socketPath, paneId, "@pontia_session_id"),
48
- paneOption(socketPath, paneId, "@pontia_runtime_instance_id"),
48
+ paneOption(socketPath, paneId, "@pontia_runtime_id"),
49
49
  ]);
50
- if (!sessionId || !runtimeInstanceId) return undefined;
51
- return { sessionId, runtimeInstanceId };
50
+ if (!sessionId || !runtimeId) return undefined;
51
+ return { sessionId, runtimeId };
52
52
  }
53
53
 
54
54
  export async function isPontiaManagedTmuxPane(env: EnvLike = process.env): Promise<boolean> {
package/src/reporter.ts CHANGED
@@ -22,13 +22,10 @@ export class EventReporter {
22
22
  this.logFile = options.logFile;
23
23
  }
24
24
 
25
- async report(
26
- context: { runtimeInstanceId: string },
27
- event: InternalEvent,
28
- ): Promise<EventReportResult> {
25
+ async report(context: { runtimeId: string }, event: InternalEvent): Promise<EventReportResult> {
29
26
  try {
30
27
  const body = await this.connection.request("event.report", {
31
- runtime_instance_id: context.runtimeInstanceId,
28
+ runtime_id: context.runtimeId,
32
29
  event,
33
30
  });
34
31
  const record =
@@ -69,7 +66,7 @@ export class EventReporter {
69
66
  event: InternalEvent,
70
67
  reason: "event_rejected" | "transport_failed" | "missing_turn_id",
71
68
  ): Promise<void> {
72
- if (event.type !== "turn.started" || typeof event.data.runtime_instance_id !== "string") return;
69
+ if (event.type !== "turn.started" || typeof event.data.runtime_id !== "string") return;
73
70
  // A lost turn.started response is ambiguous: retrying it could create a
74
71
  // second Turn. Only this fenced, idempotent failure notification is retried.
75
72
  for (let attempt = 0; attempt < 3; attempt++) {
@@ -77,7 +74,7 @@ export class EventReporter {
77
74
  try {
78
75
  const result = (await this.connection.request("turn.startFailure", {
79
76
  session_id: event.session_id,
80
- runtime_instance_id: event.data.runtime_instance_id,
77
+ runtime_id: event.data.runtime_id,
81
78
  reason,
82
79
  })) as { accepted?: unknown } | null;
83
80
  if (result?.accepted === true) return;
@@ -42,7 +42,7 @@ export async function bindSession(
42
42
  connection: PiConnection,
43
43
  env: EnvLike,
44
44
  sessionDetails: PiSessionDetails,
45
- options: { startKind?: "fork"; parentSessionId?: string; runtimeInstanceId?: string } = {},
45
+ options: { startKind?: "fork"; parentSessionId?: string; runtimeId?: string } = {},
46
46
  ): Promise<SessionContext | undefined> {
47
47
  if (!sessionDetails.clientSessionKey) return undefined;
48
48
  const tmux = tmuxBindingFromEnv(env);
@@ -57,7 +57,7 @@ export async function bindSession(
57
57
  launch_cwd: sessionDetails.clientCwd,
58
58
  ...(options.startKind ? { start_kind: options.startKind } : {}),
59
59
  ...(options.parentSessionId ? { parent_session_id: options.parentSessionId } : {}),
60
- ...(options.runtimeInstanceId ? { runtime_instance_id: options.runtimeInstanceId } : {}),
60
+ ...(options.runtimeId ? { runtime_id: options.runtimeId } : {}),
61
61
  ...(tmux ? { tmux } : {}),
62
62
  },
63
63
  });
@@ -66,14 +66,14 @@ export async function bindSession(
66
66
  const session = asRecord(record?.session);
67
67
  const runtime = asRecord(record?.runtime);
68
68
  const sessionId = optionalString(session?.session_id);
69
- const resolvedRuntimeInstanceId = optionalString(runtime?.runtime_instance_id);
69
+ const resolvedRuntimeId = optionalString(runtime?.runtime_id);
70
70
  if (!sessionId) throw new Error("runtime binding upsert response missing session.session_id");
71
- if (!resolvedRuntimeInstanceId)
72
- throw new Error("runtime binding upsert response missing runtime.runtime_instance_id");
71
+ if (!resolvedRuntimeId)
72
+ throw new Error("runtime binding upsert response missing runtime.runtime_id");
73
73
  return {
74
74
  sessionId,
75
75
  clientType: "pi",
76
- runtimeInstanceId: resolvedRuntimeInstanceId,
76
+ runtimeId: resolvedRuntimeId,
77
77
  ...sessionDetails,
78
78
  };
79
79
  }
@@ -95,15 +95,15 @@ export async function loadExistingSessionContext(
95
95
  const sessionId = optionalString(record?.session_id);
96
96
  const sessionState = optionalString(record?.session_state);
97
97
  const clientType = optionalString(record?.client_type);
98
- const runtimeInstanceId = optionalString(record?.runtime_instance_id);
99
- if (!sessionId || !sessionState || clientType !== "pi" || !runtimeInstanceId) {
98
+ const runtimeId = optionalString(record?.runtime_id);
99
+ if (!sessionId || !sessionState || clientType !== "pi" || !runtimeId) {
100
100
  throw new Error("agent binding session context lookup returned an invalid context");
101
101
  }
102
102
  return {
103
103
  sessionId,
104
104
  sessionState,
105
105
  clientType: "pi",
106
- runtimeInstanceId,
106
+ runtimeId,
107
107
  ...sessionDetails,
108
108
  };
109
109
  }
package/src/session.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export interface SessionContext {
2
2
  sessionId: string;
3
3
  clientType: "pi";
4
- runtimeInstanceId: string;
4
+ runtimeId: string;
5
5
  clientSessionKey?: string;
6
6
  clientSessionFile?: string;
7
7
  clientSessionDir?: string;