@pontia/pi-client-plugin 0.5.3 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pontia/pi-client-plugin",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Pi extension that connects coding-agent sessions to Pontia.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -462,16 +462,6 @@ export async function connectPi(
462
462
  current = await open();
463
463
  return {
464
464
  async request(method, params) {
465
- if (method === "turn.startFailure") {
466
- if (!identity?.clientSessionKey || stopped)
467
- throw new Error("Pi reporting identity is unavailable");
468
- await ready();
469
- if (!current || stopped) throw new Error("Pi connection is unavailable");
470
- return current.request(method, {
471
- ...params,
472
- client_session_key: identity.clientSessionKey,
473
- });
474
- }
475
465
  await ready();
476
466
  if (!current || stopped) return Promise.reject(new Error("Pi connection is unavailable"));
477
467
  return current.request(method, params);
package/src/events.ts CHANGED
@@ -61,6 +61,7 @@ export interface PiTopologyContext {
61
61
  export interface InternalEvent {
62
62
  session_id: string;
63
63
  turn_id?: string;
64
+ report_key?: string;
64
65
  type: InternalEventType;
65
66
  data: Record<string, unknown>;
66
67
  [legacyField: string]: any;
@@ -314,6 +315,7 @@ export function contextUsageFromPiHook(
314
315
 
315
316
  export function buildTurnStartedEvent(
316
317
  context: TurnContext,
318
+ reportKey: string,
317
319
  previousLeafId: string | null = null,
318
320
  topologyContext?: PiTopologyContext,
319
321
  ): InternalEvent {
@@ -327,7 +329,12 @@ export function buildTurnStartedEvent(
327
329
  };
328
330
  if (context.inboxMessageId) data.inbox_message_id = context.inboxMessageId;
329
331
  if (topologyContext) data.topology_context = topologyContext;
330
- return { session_id: context.sessionId, type: "turn.started", data };
332
+ return {
333
+ session_id: context.sessionId,
334
+ report_key: reportKey,
335
+ type: "turn.started",
336
+ data,
337
+ };
331
338
  }
332
339
 
333
340
  export function buildTurnOutputEvent(context: ActiveTurnContext, output: string): InternalEvent {
package/src/index.ts CHANGED
@@ -740,7 +740,7 @@ export function createPontiaPiExtension(
740
740
 
741
741
  pi.on("agent_start", async (_event, ctx) => {
742
742
  const topologyContext = topologyContextFromHookContext(ctx);
743
- const previousLeafId = leafIdFromHookContext(ctx);
743
+ const reportKey = leafIdFromHookContext(ctx);
744
744
  if (reportingDisabled) return;
745
745
  try {
746
746
  const dispatch = directInput.getStore();
@@ -750,6 +750,16 @@ export function createPontiaPiExtension(
750
750
  : undefined;
751
751
  if (dispatch) dispatch.consumed = true;
752
752
  const logFile = currentHookLogFile();
753
+ if (!reportKey) {
754
+ activeTurn = undefined;
755
+ pendingPrompt = undefined;
756
+ await logDiagnostic(logFile, {
757
+ level: "error",
758
+ code: "turn_start_entry_id_missing",
759
+ message: "pi agent_start has no native session entry id; turn.started was not reported",
760
+ });
761
+ return;
762
+ }
753
763
  if (!boundSessionContext) {
754
764
  const hookSessionDetails = piSessionDetailsFromHookContext(ctx);
755
765
  const sessionDetails = hookSessionDetails.clientSessionKey
@@ -816,7 +826,7 @@ export function createPontiaPiExtension(
816
826
  lastContextUsageJson = undefined;
817
827
  const started = await reporter.report(
818
828
  turnContext,
819
- buildTurnStartedEvent(turnContext, previousLeafId, topologyContext),
829
+ buildTurnStartedEvent(turnContext, reportKey, reportKey, topologyContext),
820
830
  );
821
831
  const canonicalTurnId = typeof started === "boolean" ? turnContext.turnId : started.turnId;
822
832
  if (!reportAccepted(started) || !canonicalTurnId) {
package/src/reporter.ts CHANGED
@@ -23,72 +23,52 @@ export class EventReporter {
23
23
  }
24
24
 
25
25
  async report(context: { runtimeId: string }, event: InternalEvent): Promise<EventReportResult> {
26
- try {
27
- const body = await this.connection.request("event.report", {
28
- runtime_id: context.runtimeId,
29
- event,
30
- });
31
- const record =
32
- body && typeof body === "object" ? (body as Record<string, unknown>) : undefined;
33
- if (
34
- event.type === "turn.started" &&
35
- (record?.accepted !== true || typeof record?.turn_id !== "string" || !record.turn_id)
36
- ) {
37
- await this.reportStartFailure(event, "missing_turn_id");
38
- return { accepted: false };
39
- }
40
- if (record?.accepted !== true)
41
- throw new Error("Pi event RPC returned an invalid acknowledgement");
42
- return {
43
- accepted: true,
44
- eventId: typeof record.event_id === "string" ? record.event_id : undefined,
45
- turnId: typeof record.turn_id === "string" ? record.turn_id : undefined,
46
- };
47
- } catch (error) {
48
- await appendDiagnostic(this.logFile, {
49
- level: "error",
50
- code: "pi_event_report_failed",
51
- message: `Pi event RPC failed for ${event.type}`,
52
- details: {
53
- event_type: event.type,
54
- error: error instanceof Error ? error.message : String(error),
55
- },
56
- });
57
- await this.reportStartFailure(
58
- event,
59
- error instanceof RpcError ? "event_rejected" : "transport_failed",
60
- );
61
- return { accepted: false };
62
- }
63
- }
26
+ const attempts = event.type === "turn.started" ? 3 : 1;
27
+ let lastError: unknown;
64
28
 
65
- private async reportStartFailure(
66
- event: InternalEvent,
67
- reason: "event_rejected" | "transport_failed" | "missing_turn_id",
68
- ): Promise<void> {
69
- if (event.type !== "turn.started" || typeof event.data.runtime_id !== "string") return;
70
- // A lost turn.started response is ambiguous: retrying it could create a
71
- // second Turn. Only this fenced, idempotent failure notification is retried.
72
- for (let attempt = 0; attempt < 3; attempt++) {
29
+ for (let attempt = 0; attempt < attempts; attempt++) {
73
30
  if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
74
31
  try {
75
- const result = (await this.connection.request("turn.startFailure", {
76
- session_id: event.session_id,
77
- runtime_id: event.data.runtime_id,
78
- reason,
79
- })) as { accepted?: unknown } | null;
80
- if (result?.accepted === true) return;
32
+ const body = await this.connection.request("event.report", {
33
+ runtime_id: context.runtimeId,
34
+ event,
35
+ });
36
+ const record =
37
+ body && typeof body === "object" ? (body as Record<string, unknown>) : undefined;
38
+ if (record?.accepted !== true) {
39
+ if (record?.accepted === false) {
40
+ lastError = new Error("Pi event report was rejected");
41
+ break;
42
+ }
43
+ throw new Error("Pi event RPC returned an invalid acknowledgement");
44
+ }
45
+ if (
46
+ event.type === "turn.started" &&
47
+ (typeof record.turn_id !== "string" || !record.turn_id)
48
+ ) {
49
+ throw new Error("Pi turn.started acknowledgement has no canonical turn_id");
50
+ }
51
+ return {
52
+ accepted: true,
53
+ eventId: typeof record.event_id === "string" ? record.event_id : undefined,
54
+ turnId: typeof record.turn_id === "string" ? record.turn_id : undefined,
55
+ };
81
56
  } catch (error) {
57
+ lastError = error;
82
58
  if (error instanceof RpcError && error.code !== -32603) break;
83
- // Exhaustion is visible locally; an unreachable server cannot persist
84
- // a state transition. Never claim that this notification succeeded.
85
59
  }
86
60
  }
61
+
87
62
  await appendDiagnostic(this.logFile, {
88
63
  level: "error",
89
- code: "turn_start_failure_report_failed",
90
- message: "could not persist turn.started reporting failure; workflow state is unconfirmed",
91
- details: { session_id: event.session_id, reason },
64
+ code: "pi_event_report_failed",
65
+ message: `Pi event RPC failed for ${event.type}`,
66
+ details: {
67
+ event_type: event.type,
68
+ report_key: event.report_key,
69
+ error: lastError instanceof Error ? lastError.message : String(lastError),
70
+ },
92
71
  });
72
+ return { accepted: false };
93
73
  }
94
74
  }