@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 +3 -3
- package/src/context.ts +1 -1
- package/src/control-socket.ts +10 -13
- package/src/events.ts +3 -3
- package/src/index.ts +53 -28
- package/src/live-output.ts +1 -1
- package/src/managed-runtime.ts +5 -5
- package/src/reporter.ts +4 -7
- package/src/runtime-binding.ts +9 -9
- package/src/session.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pontia/pi-client-plugin",
|
|
3
|
-
"version": "0.2.
|
|
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": "
|
|
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": "
|
|
46
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
47
47
|
},
|
|
48
48
|
"pi": {
|
|
49
49
|
"extensions": [
|
package/src/context.ts
CHANGED
package/src/control-socket.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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?.
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
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?.
|
|
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?.
|
|
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
|
-
|
|
508
|
-
|
|
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 (
|
|
522
|
-
|
|
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 (
|
|
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
|
-
|
|
628
|
-
existingSession?.sessionState === "starting"
|
|
629
|
-
? existingSession
|
|
639
|
+
runtimeId:
|
|
640
|
+
reason === "reload" || existingSession?.sessionState === "starting"
|
|
641
|
+
? existingSession?.runtimeId
|
|
630
642
|
: undefined,
|
|
631
643
|
});
|
|
632
|
-
if (
|
|
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
|
-
|
|
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) => {
|
package/src/live-output.ts
CHANGED
|
@@ -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
|
-
|
|
239
|
+
runtime_id: this.context.runtimeId,
|
|
240
240
|
stream_id: this.streamId,
|
|
241
241
|
};
|
|
242
242
|
}
|
package/src/managed-runtime.ts
CHANGED
|
@@ -6,7 +6,7 @@ const execFileAsync = promisify(execFile);
|
|
|
6
6
|
|
|
7
7
|
export interface ManagedRuntimeIdentity {
|
|
8
8
|
sessionId: string;
|
|
9
|
-
|
|
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,
|
|
46
|
+
const [sessionId, runtimeId] = await Promise.all([
|
|
47
47
|
paneOption(socketPath, paneId, "@pontia_session_id"),
|
|
48
|
-
paneOption(socketPath, paneId, "@
|
|
48
|
+
paneOption(socketPath, paneId, "@pontia_runtime_id"),
|
|
49
49
|
]);
|
|
50
|
-
if (!sessionId || !
|
|
51
|
-
return { sessionId,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
77
|
+
runtime_id: event.data.runtime_id,
|
|
81
78
|
reason,
|
|
82
79
|
})) as { accepted?: unknown } | null;
|
|
83
80
|
if (result?.accepted === true) return;
|
package/src/runtime-binding.ts
CHANGED
|
@@ -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;
|
|
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.
|
|
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
|
|
69
|
+
const resolvedRuntimeId = optionalString(runtime?.runtime_id);
|
|
70
70
|
if (!sessionId) throw new Error("runtime binding upsert response missing session.session_id");
|
|
71
|
-
if (!
|
|
72
|
-
throw new Error("runtime binding upsert response missing runtime.
|
|
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
|
-
|
|
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
|
|
99
|
-
if (!sessionId || !sessionState || clientType !== "pi" || !
|
|
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
|
-
|
|
106
|
+
runtimeId,
|
|
107
107
|
...sessionDetails,
|
|
108
108
|
};
|
|
109
109
|
}
|