@fitchmultz/pi-subagents 0.50.0 → 0.52.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/CHANGELOG.md +63 -2
- package/README.md +36 -5
- package/dist/agent/child-runtime.js +80 -0
- package/dist/agent/contracts.js +15 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +212 -192
- package/dist/agent/journal-tail.js +115 -156
- package/dist/agent/launch.js +20 -8
- package/dist/agent/legacy.js +65 -23
- package/dist/agent/notifications.js +138 -47
- package/dist/agent/observation-state.js +246 -0
- package/dist/agent/observation-view.js +258 -0
- package/dist/agent/observation.js +350 -0
- package/dist/agent/observe-card.js +243 -0
- package/dist/agent/observe-chrome.js +85 -0
- package/dist/agent/observe-client.js +285 -0
- package/dist/agent/observe-layout.js +65 -0
- package/dist/agent/observe-reading.js +84 -0
- package/dist/agent/observe-screen.js +400 -96
- package/dist/agent/observe-transcript.js +382 -0
- package/dist/agent/observe.js +250 -43
- package/dist/agent/parking.js +253 -0
- package/dist/agent/process.js +228 -28
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +229 -126
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +47 -12
- package/dist/agent/tool-schemas.js +20 -0
- package/dist/agent/typebox.js +10 -5
- package/dist/agent/usage.js +0 -115
- package/dist/agent/view.js +46 -16
- package/dist/agent/wake.js +99 -0
- package/dist/build-info.js +1 -1
- package/dist/intercom-client.d.ts +26 -0
- package/dist/intercom-client.js +45 -0
- package/dist/pi-intercom/agent-dir.d.ts +1 -0
- package/dist/pi-intercom/bridge-session.js +2 -1
- package/dist/pi-intercom/broker/broker.js +62 -43
- package/dist/pi-intercom/broker/client-last.d.ts +24 -0
- package/dist/pi-intercom/broker/client-last.js +42 -5
- package/dist/pi-intercom/broker/client-park.d.ts +9 -0
- package/dist/pi-intercom/broker/client-park.js +21 -0
- package/dist/pi-intercom/broker/client.d.ts +59 -0
- package/dist/pi-intercom/broker/client.js +55 -29
- package/dist/pi-intercom/broker/connection.d.ts +29 -0
- package/dist/pi-intercom/broker/framing.d.ts +17 -0
- package/dist/pi-intercom/broker/framing.js +10 -7
- package/dist/pi-intercom/broker/guard.js +23 -0
- package/dist/pi-intercom/broker/last.d.ts +30 -0
- package/dist/pi-intercom/broker/last.js +87 -40
- package/dist/pi-intercom/broker/parked.js +107 -0
- package/dist/pi-intercom/broker/paths.d.ts +13 -0
- package/dist/pi-intercom/broker/paths.js +35 -12
- package/dist/pi-intercom/broker/pid.js +5 -1
- package/dist/pi-intercom/broker/server-connection.js +20 -1
- package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
- package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
- package/dist/pi-intercom/broker/targets.d.ts +9 -0
- package/dist/pi-intercom/connection.js +71 -20
- package/dist/pi-intercom/identity.js +6 -5
- package/dist/pi-intercom/inbound-delivery.js +16 -60
- package/dist/pi-intercom/inbound-journal.js +83 -6
- package/dist/pi-intercom/inbound-record.js +18 -1
- package/dist/pi-intercom/inbound.js +22 -37
- package/dist/pi-intercom/index.js +41 -0
- package/dist/pi-intercom/last-message.js +4 -16
- package/dist/pi-intercom/lifecycle.js +88 -3
- package/dist/pi-intercom/outbound.js +5 -15
- package/dist/pi-intercom/reply-tracker.js +7 -6
- package/dist/pi-intercom/reply-wait.js +2 -2
- package/dist/pi-intercom/session-targets.d.ts +24 -0
- package/dist/pi-intercom/tool-registration.js +4 -1
- package/dist/pi-intercom/tool-results.js +5 -7
- package/dist/pi-intercom/tool-schemas.js +12 -26
- package/dist/pi-intercom/types.d.ts +212 -0
- package/dist/pi-intercom/types.js +54 -3
- package/dist/pi-intercom/validation.d.ts +12 -0
- package/dist/pi-intercom/validation.js +3 -0
- package/dist/shared/bounded-json.js +151 -0
- package/dist/shared/compact-cards.js +66 -0
- package/dist/shared/journal-frames.js +3 -6
- package/dist/shared/json-projection.js +267 -129
- package/dist/shared/native-journal.js +123 -56
- package/dist/shared/parking.js +12 -0
- package/dist/shared/post-exit-stdio-guard.js +120 -7
- package/dist/shared/tui-action.js +29 -0
- package/dist/shared/unknown.d.ts +9 -0
- package/docs/code-quality.md +5 -3
- package/docs/development.md +42 -3
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +228 -7
- package/docs/performance.md +118 -14
- package/docs/reference.md +168 -20
- package/docs/retired-history-maintenance.md +252 -0
- package/package.json +28 -15
- package/scripts/archive-retired-history.mjs +464 -0
- package/scripts/build.mjs +48 -28
- package/scripts/measure-size.mjs +1 -1
- package/scripts/native-package-smoke.mjs +153 -12
- package/scripts/package-smoke.mjs +80 -11
- package/scripts/prepare.mjs +2 -2
- package/scripts/real-pi-live.mjs +377 -0
- package/scripts/real-pi-processes.mjs +6 -1
- package/scripts/real-pi-smoke.mjs +6 -282
- package/scripts/run-tests.mjs +4 -2
- package/scripts/typescript.mjs +35 -0
- package/skills/pi-intercom/SKILL.md +2 -1
- package/skills/pi-subagents/SKILL.md +23 -5
- package/src/agent/child-runtime.ts +105 -0
- package/src/agent/contracts.ts +18 -34
- package/src/agent/host.ts +39 -20
- package/src/agent/index.ts +234 -207
- package/src/agent/journal-tail.ts +141 -153
- package/src/agent/launch.ts +33 -9
- package/src/agent/legacy.ts +85 -31
- package/src/agent/notifications.ts +192 -67
- package/src/agent/observation-state.ts +277 -0
- package/src/agent/observation-view.ts +267 -0
- package/src/agent/observation.ts +409 -0
- package/src/agent/observe-card.ts +304 -0
- package/src/agent/observe-chrome.ts +170 -0
- package/src/agent/observe-client.ts +318 -0
- package/src/agent/observe-layout.ts +79 -0
- package/src/agent/observe-reading.ts +97 -0
- package/src/agent/observe-screen.ts +443 -129
- package/src/agent/observe-transcript.ts +434 -0
- package/src/agent/observe.ts +304 -71
- package/src/agent/parking.ts +341 -0
- package/src/agent/process.ts +280 -37
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +227 -149
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +44 -11
- package/src/agent/tool-schemas.ts +33 -0
- package/src/agent/typebox.ts +12 -7
- package/src/agent/usage.ts +0 -130
- package/src/agent/view.ts +70 -20
- package/src/agent/wake.ts +107 -0
- package/src/intercom-client.ts +108 -0
- package/src/pi-intercom/bridge-session.ts +2 -2
- package/src/pi-intercom/broker/broker.ts +64 -53
- package/src/pi-intercom/broker/client-last.ts +65 -12
- package/src/pi-intercom/broker/client-park.ts +23 -0
- package/src/pi-intercom/broker/client.ts +63 -45
- package/src/pi-intercom/broker/framing.ts +11 -8
- package/src/pi-intercom/broker/guard.ts +33 -0
- package/src/pi-intercom/broker/last.ts +131 -43
- package/src/pi-intercom/broker/parked.ts +130 -0
- package/src/pi-intercom/broker/paths.ts +39 -12
- package/src/pi-intercom/broker/pid.ts +6 -1
- package/src/pi-intercom/broker/server-connection.ts +27 -2
- package/src/pi-intercom/connection.ts +103 -39
- package/src/pi-intercom/identity.ts +8 -5
- package/src/pi-intercom/inbound-delivery.ts +20 -66
- package/src/pi-intercom/inbound-journal.ts +103 -7
- package/src/pi-intercom/inbound-record.ts +21 -4
- package/src/pi-intercom/inbound.ts +32 -49
- package/src/pi-intercom/index.ts +44 -0
- package/src/pi-intercom/last-message.ts +11 -20
- package/src/pi-intercom/lifecycle.ts +117 -14
- package/src/pi-intercom/outbound.ts +7 -16
- package/src/pi-intercom/reply-tracker.ts +8 -6
- package/src/pi-intercom/reply-wait.ts +3 -2
- package/src/pi-intercom/runtime-types.ts +0 -1
- package/src/pi-intercom/tool-registration.ts +4 -1
- package/src/pi-intercom/tool-results.ts +10 -9
- package/src/pi-intercom/tool-schemas.ts +12 -44
- package/src/pi-intercom/types.ts +115 -4
- package/src/pi-intercom/validation.ts +4 -0
- package/src/shared/bounded-json.ts +162 -0
- package/src/shared/compact-cards.ts +118 -0
- package/src/shared/journal-frames.ts +3 -19
- package/src/shared/json-projection.ts +267 -147
- package/src/shared/native-journal.ts +157 -75
- package/src/shared/parking.ts +22 -0
- package/src/shared/post-exit-stdio-guard.ts +165 -7
- package/src/shared/tui-action.ts +39 -0
- package/tsconfig.client.json +8 -0
- package/tsconfig.json +1 -0
- package/src/shared/stream-json.d.ts +0 -10
|
@@ -4,28 +4,40 @@ import type { IntercomTransport } from "./transport.ts";
|
|
|
4
4
|
import { spawnBrokerIfNeeded } from "./broker/spawn.ts";
|
|
5
5
|
import type { IntercomConfig } from "./config.ts";
|
|
6
6
|
import type { IntercomLifecycle } from "./lifecycle.ts";
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
7
|
+
import type { Message, SessionInfo, ReceiptQuery, ReceiptResult } from "./types.ts";
|
|
8
|
+
import type { NativeReadQuery } from "./broker/last.ts";
|
|
9
|
+
import { readLastMessage, type NativeHistory } from "./last-message.ts";
|
|
9
10
|
import { asError } from "./validation.ts";
|
|
11
|
+
import { RecoveryError } from "./tool-results.ts";
|
|
10
12
|
import { formatTargetOptions, resolveSessionTarget, nativePeerId } from "./session-targets.ts";
|
|
11
13
|
|
|
12
|
-
type Lifecycle =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
>;
|
|
14
|
+
type Lifecycle = Pick<
|
|
15
|
+
Readonly<IntercomLifecycle>,
|
|
16
|
+
"currentSessionId" | "generation" | "health" | "identity" | "live" | "registration" | "status"
|
|
17
|
+
> &
|
|
18
|
+
Pick<Readonly<IntercomLifecycle>, "snapshot" | "transport">;
|
|
18
19
|
|
|
19
20
|
interface ConnectionHooks {
|
|
20
21
|
readonly restoring: () => Promise<void> | undefined;
|
|
21
22
|
readonly pendingAsks: () => number;
|
|
23
|
+
readonly receipt: (query: ReceiptQuery) => ReceiptResult;
|
|
22
24
|
readonly incoming: (ctx: ExtensionContext, from: SessionInfo, message: Message) => Promise<void>;
|
|
23
25
|
readonly peerLeft: (id: string) => void;
|
|
24
26
|
readonly disconnected: (error: Readonly<Error>) => void;
|
|
27
|
+
/** A main answers for and wakes its exited parked children. */
|
|
28
|
+
readonly parkedHistory: (peer: SessionInfo) => Promise<NativeHistory | undefined>;
|
|
29
|
+
readonly wake: (peer: string) => void;
|
|
30
|
+
}
|
|
31
|
+
interface LastRequest {
|
|
32
|
+
readonly requestId: string;
|
|
33
|
+
readonly query: NativeReadQuery;
|
|
34
|
+
readonly parked?: SessionInfo;
|
|
25
35
|
}
|
|
26
36
|
/** Owns socket registration, coalesced connects and the reconnect/startup timers. */
|
|
27
37
|
export class IntercomConnection {
|
|
28
38
|
private client: IntercomClient | null = null;
|
|
39
|
+
/** The transport whose broker retains this actor's input while it parks. */
|
|
40
|
+
private parked: IntercomClient | null = null;
|
|
29
41
|
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
30
42
|
private startupTimer: NodeJS.Timeout | null = null;
|
|
31
43
|
private connecting: Promise<IntercomTransport> | null = null;
|
|
@@ -42,6 +54,22 @@ export class IntercomConnection {
|
|
|
42
54
|
get active(): IntercomTransport | null {
|
|
43
55
|
return this.client;
|
|
44
56
|
}
|
|
57
|
+
async park(): Promise<boolean> {
|
|
58
|
+
const client = this.client;
|
|
59
|
+
this.parked = client !== null && (await client.park().catch(() => false)) ? client : null;
|
|
60
|
+
return this.parked !== null && this.parked === this.client;
|
|
61
|
+
}
|
|
62
|
+
get parking(): boolean {
|
|
63
|
+
return this.parked !== null;
|
|
64
|
+
}
|
|
65
|
+
get reconnected(): boolean {
|
|
66
|
+
return this.client !== null && this.client !== this.parked;
|
|
67
|
+
}
|
|
68
|
+
unpark(): Promise<boolean> {
|
|
69
|
+
const client = this.parked === this.client ? this.client : null;
|
|
70
|
+
this.parked = null;
|
|
71
|
+
return client?.park(false).catch(() => false) ?? Promise.resolve(false);
|
|
72
|
+
}
|
|
45
73
|
start(ctx: ExtensionContext, generation: number): void {
|
|
46
74
|
this.attempt = 0;
|
|
47
75
|
this.clearTimers();
|
|
@@ -113,44 +141,71 @@ export class IntercomConnection {
|
|
|
113
141
|
this.client.updatePresence({
|
|
114
142
|
status: this.lifecycle.status(),
|
|
115
143
|
...this.lifecycle.health(this.hooks.pendingAsks()),
|
|
144
|
+
applicationGuard: this.lifecycle.snapshot().applicationGuard ?? null,
|
|
116
145
|
});
|
|
117
146
|
}
|
|
118
147
|
}
|
|
148
|
+
private answerLast(next: IntercomClient, request: LastRequest): void {
|
|
149
|
+
const ctx = this.lifecycle.live();
|
|
150
|
+
const id = next.sessionId;
|
|
151
|
+
if (this.client !== next || !ctx || id === null) {
|
|
152
|
+
next.respondLast(request.requestId, { error: "Intercom runtime no longer active" });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const { parked, query } = request;
|
|
156
|
+
if ("messageId" in query) {
|
|
157
|
+
// The broker refuses receipt reads of an exited parked child before relaying them.
|
|
158
|
+
next.respondLast(
|
|
159
|
+
request.requestId,
|
|
160
|
+
parked ? { error: "publication_unavailable" } : this.hooks.receipt(query),
|
|
161
|
+
);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (parked) {
|
|
165
|
+
this.hooks
|
|
166
|
+
.parkedHistory(parked)
|
|
167
|
+
.then((history) =>
|
|
168
|
+
history
|
|
169
|
+
? readLastMessage(history, parked, query)
|
|
170
|
+
: { error: "Parked session history is unavailable." },
|
|
171
|
+
)
|
|
172
|
+
.catch((error: unknown) => ({ error: asError(error).message }))
|
|
173
|
+
.then((result) => {
|
|
174
|
+
next.respondLast(request.requestId, result);
|
|
175
|
+
})
|
|
176
|
+
.catch(() => {
|
|
177
|
+
/* The requester's broker timeout reports a lost transport. */
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
next.respondLast(
|
|
183
|
+
request.requestId,
|
|
184
|
+
readLastMessage(
|
|
185
|
+
ctx.sessionManager,
|
|
186
|
+
{
|
|
187
|
+
id,
|
|
188
|
+
...this.lifecycle.identity(ctx.sessionManager.getSessionId()),
|
|
189
|
+
cwd: ctx.cwd,
|
|
190
|
+
model: ctx.model?.id ?? "unknown",
|
|
191
|
+
status: this.lifecycle.status(),
|
|
192
|
+
},
|
|
193
|
+
query,
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
next.respondLast(request.requestId, { error: asError(error).message });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
119
200
|
private createClient(): IntercomClient {
|
|
120
201
|
const next = new IntercomClient({
|
|
121
202
|
sendTimeoutMs: this.config.sendTimeoutMs,
|
|
122
203
|
listTimeoutMs: this.config.listTimeoutMs,
|
|
123
204
|
});
|
|
124
205
|
this.client = next;
|
|
125
|
-
next.on(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const ctx = this.lifecycle.live();
|
|
129
|
-
const id = next.sessionId;
|
|
130
|
-
if (this.client !== next || !ctx || id === null) {
|
|
131
|
-
next.respondLast(request.requestId, { error: "Intercom runtime no longer active" });
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
next.respondLast(
|
|
136
|
-
request.requestId,
|
|
137
|
-
readLastMessage(
|
|
138
|
-
ctx.sessionManager,
|
|
139
|
-
{
|
|
140
|
-
id,
|
|
141
|
-
...this.lifecycle.identity(ctx.sessionManager.getSessionId()),
|
|
142
|
-
cwd: ctx.cwd,
|
|
143
|
-
model: ctx.model?.id ?? "unknown",
|
|
144
|
-
status: this.lifecycle.status(),
|
|
145
|
-
},
|
|
146
|
-
request.query,
|
|
147
|
-
),
|
|
148
|
-
);
|
|
149
|
-
} catch (error) {
|
|
150
|
-
next.respondLast(request.requestId, { error: asError(error).message });
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
);
|
|
206
|
+
next.on("last_request", (request: LastRequest) => {
|
|
207
|
+
this.answerLast(next, request);
|
|
208
|
+
});
|
|
154
209
|
next.on("message", (from: SessionInfo, message: Message) => {
|
|
155
210
|
const ctx = this.lifecycle.live();
|
|
156
211
|
if (this.client !== next || !ctx) {
|
|
@@ -160,6 +215,11 @@ export class IntercomConnection {
|
|
|
160
215
|
console.error("Intercom inbound delivery failed:", error);
|
|
161
216
|
});
|
|
162
217
|
});
|
|
218
|
+
next.on("wake", (peer: string) => {
|
|
219
|
+
if (this.client === next) {
|
|
220
|
+
this.hooks.wake(peer);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
163
223
|
next.on("session_left", (id: string) => {
|
|
164
224
|
if (this.client !== next) {
|
|
165
225
|
return;
|
|
@@ -172,6 +232,7 @@ export class IntercomConnection {
|
|
|
172
232
|
}
|
|
173
233
|
this.hooks.disconnected(error);
|
|
174
234
|
this.client = null;
|
|
235
|
+
this.lifecycle.transport(null);
|
|
175
236
|
this.clearReconnect();
|
|
176
237
|
this.scheduleReconnect();
|
|
177
238
|
});
|
|
@@ -197,6 +258,7 @@ export class IntercomConnection {
|
|
|
197
258
|
throw new Error("Intercom runtime no longer active");
|
|
198
259
|
}
|
|
199
260
|
this.client = next;
|
|
261
|
+
this.lifecycle.transport(next.sessionId, next.supportsGuardedDelivery);
|
|
200
262
|
this.syncIdentity(ctx.sessionManager.getSessionId());
|
|
201
263
|
this.attempt = 0;
|
|
202
264
|
return next;
|
|
@@ -279,12 +341,14 @@ export async function resolveConnectedTarget(
|
|
|
279
341
|
return resolution.target.id;
|
|
280
342
|
}
|
|
281
343
|
if (resolution.status === "ambiguous") {
|
|
282
|
-
throw new
|
|
344
|
+
throw new RecoveryError(
|
|
345
|
+
"ambiguous_target",
|
|
283
346
|
`Target "${name}" matches multiple sessions. Use one of these targets: ${formatTargetOptions(resolution.matches, sessions)}.`,
|
|
284
347
|
);
|
|
285
348
|
}
|
|
286
349
|
if (resolution.status === "prefix_too_short") {
|
|
287
|
-
throw new
|
|
350
|
+
throw new RecoveryError(
|
|
351
|
+
"ambiguous_target",
|
|
288
352
|
`Target "${name}" is too short. Use the displayed target from intercom list, such as ${formatTargetOptions(resolution.matches, sessions)}.`,
|
|
289
353
|
);
|
|
290
354
|
}
|
|
@@ -4,20 +4,23 @@ export interface IntercomLineage {
|
|
|
4
4
|
readonly rootSessionId: string;
|
|
5
5
|
readonly parentSessionId: string;
|
|
6
6
|
}
|
|
7
|
+
// Launches set both variables to the parent's exact native session ID. Absence means a main;
|
|
8
|
+
// anything else is refused, never normalized into a different identity.
|
|
7
9
|
export function readIntercomLineage(): IntercomLineage | undefined {
|
|
8
|
-
const rootSessionId = process.env.PI_INTERCOM_ROOT_SESSION_ID
|
|
9
|
-
const parentSessionId = process.env.PI_INTERCOM_PARENT_SESSION_ID
|
|
10
|
+
const rootSessionId = process.env.PI_INTERCOM_ROOT_SESSION_ID;
|
|
11
|
+
const parentSessionId = process.env.PI_INTERCOM_PARENT_SESSION_ID;
|
|
10
12
|
if (rootSessionId === undefined && parentSessionId === undefined) {
|
|
11
13
|
return;
|
|
12
14
|
}
|
|
13
15
|
if (
|
|
14
16
|
rootSessionId === undefined ||
|
|
15
|
-
parentSessionId === undefined ||
|
|
16
17
|
rootSessionId === "" ||
|
|
17
|
-
|
|
18
|
+
rootSessionId !== rootSessionId.trim() ||
|
|
18
19
|
rootSessionId !== parentSessionId
|
|
19
20
|
) {
|
|
20
|
-
throw new Error(
|
|
21
|
+
throw new Error(
|
|
22
|
+
"Child launch requires identical, unpadded root and parent native session IDs.",
|
|
23
|
+
);
|
|
21
24
|
}
|
|
22
25
|
return { rootSessionId, parentSessionId };
|
|
23
26
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import type { IntercomLifecycle } from "./lifecycle.ts";
|
|
4
3
|
import type { InboundJournal } from "./inbound-journal.ts";
|
|
@@ -9,10 +8,9 @@ import type {
|
|
|
9
8
|
PendingInboundMessage,
|
|
10
9
|
} from "./runtime-types.ts";
|
|
11
10
|
import { requestedDelivery } from "./inbound-record.ts";
|
|
12
|
-
import { isRecord } from "./validation.ts";
|
|
13
11
|
import { runCooperatively } from "../shared/cooperative.ts";
|
|
14
12
|
|
|
15
|
-
type Lifecycle = Readonly<
|
|
13
|
+
type Lifecycle = Pick<Readonly<IntercomLifecycle>, "generation" | "isIdle" | "live" | "toolCount">;
|
|
16
14
|
type Journal = Readonly<
|
|
17
15
|
Pick<
|
|
18
16
|
InboundJournal,
|
|
@@ -121,6 +119,9 @@ export class InboundDeliveryOwner {
|
|
|
121
119
|
flushDelivery: PendingInboundMessage["flushDelivery"] = "auto",
|
|
122
120
|
delayMs = 200,
|
|
123
121
|
): void {
|
|
122
|
+
if (!this.journal.owns(entry)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
124
125
|
this.journal.remember(entry, "queued", flushDelivery);
|
|
125
126
|
this.hooks.syncStatus();
|
|
126
127
|
this.schedule(delayMs);
|
|
@@ -153,10 +154,7 @@ export class InboundDeliveryOwner {
|
|
|
153
154
|
private triggerIndex(entries: readonly PendingInboundMessage[]): number {
|
|
154
155
|
const canTrigger = (entry: PendingInboundMessage) =>
|
|
155
156
|
entry.flushDelivery !== "passive" && requestedDelivery(entry.message) !== "passive";
|
|
156
|
-
|
|
157
|
-
(entry) => canTrigger(entry) && entry.message.expectsReply === true,
|
|
158
|
-
);
|
|
159
|
-
return ask === -1 ? entries.findIndex(canTrigger) : ask;
|
|
157
|
+
return entries.findLastIndex(canTrigger);
|
|
160
158
|
}
|
|
161
159
|
private *sendTriggerLast(
|
|
162
160
|
entries: readonly PendingInboundMessage[],
|
|
@@ -170,14 +168,19 @@ export class InboundDeliveryOwner {
|
|
|
170
168
|
}
|
|
171
169
|
const index = this.triggerIndex(rest);
|
|
172
170
|
const trigger = index === -1 ? undefined : rest.splice(index, 1)[0];
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
const replyTarget = entries.find((entry) => entry.message.expectsReply === true) ?? trigger;
|
|
172
|
+
if (replyTarget && this.journal.get(replyTarget.message.id) === replyTarget) {
|
|
173
|
+
this.hooks.replies.queueTurnContext({ ...replyTarget, receivedAt: Date.now() });
|
|
175
174
|
}
|
|
176
175
|
for (const entry of rest) {
|
|
177
176
|
if (!this.live(generation)) {
|
|
178
177
|
return;
|
|
179
178
|
}
|
|
180
|
-
this.send(
|
|
179
|
+
this.send(
|
|
180
|
+
entry,
|
|
181
|
+
requestedDelivery(entry.message) === "queue" ? "followUp" : "steer",
|
|
182
|
+
generation,
|
|
183
|
+
);
|
|
181
184
|
yield;
|
|
182
185
|
}
|
|
183
186
|
if (trigger) {
|
|
@@ -202,7 +205,6 @@ export class InboundDeliveryOwner {
|
|
|
202
205
|
private *sendSteers(
|
|
203
206
|
entries: readonly PendingInboundMessage[],
|
|
204
207
|
generation: number,
|
|
205
|
-
onDetach: (task: Promise<boolean>) => void,
|
|
206
208
|
): Generator<void, void> {
|
|
207
209
|
for (const entry of entries) {
|
|
208
210
|
if (!this.live(generation)) {
|
|
@@ -212,9 +214,6 @@ export class InboundDeliveryOwner {
|
|
|
212
214
|
continue;
|
|
213
215
|
}
|
|
214
216
|
this.send(entry, "steer", generation);
|
|
215
|
-
if (requestedDelivery(entry.message) === "steer") {
|
|
216
|
-
onDetach(this.detach(entry, entry.message.expectsReply !== true));
|
|
217
|
-
}
|
|
218
217
|
yield;
|
|
219
218
|
}
|
|
220
219
|
}
|
|
@@ -243,15 +242,7 @@ export class InboundDeliveryOwner {
|
|
|
243
242
|
this.schedule(500);
|
|
244
243
|
return;
|
|
245
244
|
}
|
|
246
|
-
return this.runBatch(
|
|
247
|
-
const detachments: Promise<boolean>[] = [];
|
|
248
|
-
await runCooperatively(
|
|
249
|
-
this.sendSteers(steers, generation, (task) => {
|
|
250
|
-
detachments.push(task);
|
|
251
|
-
}),
|
|
252
|
-
);
|
|
253
|
-
await Promise.all(detachments);
|
|
254
|
-
}, generation);
|
|
245
|
+
return this.runBatch(() => runCooperatively(this.sendSteers(steers, generation)), generation);
|
|
255
246
|
}
|
|
256
247
|
private messageOptions(delivery: InboundDelivery): {
|
|
257
248
|
readonly triggerTurn?: boolean;
|
|
@@ -297,57 +288,20 @@ export class InboundDeliveryOwner {
|
|
|
297
288
|
}
|
|
298
289
|
}
|
|
299
290
|
private nativeMessage(entry: InboundMessageEntry): Parameters<ExtensionAPI["sendMessage"]>[0] {
|
|
300
|
-
const sender =
|
|
301
|
-
entry.from.name === undefined || entry.from.name === ""
|
|
302
|
-
? entry.from.id.slice(0, 8)
|
|
303
|
-
: entry.from.name;
|
|
291
|
+
const sender = entry.from.name ?? entry.from.id.slice(0, 8);
|
|
304
292
|
const command = entry.replyCommand ?? "";
|
|
305
293
|
const reply = command === "" ? "" : `\n\nTo reply, use the intercom tool: ${command}`;
|
|
294
|
+
const hint = entry.message.replyHint;
|
|
295
|
+
const publication = hint
|
|
296
|
+
? `\n\nTo publish a correlated reply, use ${hint.tool} with commandId ${hint.commandId}. This is not an intercom ask.`
|
|
297
|
+
: "";
|
|
306
298
|
return {
|
|
307
299
|
customType: "intercom_message",
|
|
308
|
-
content: `**📨 From ${sender}** (Native session cwd: ${entry.from.cwd})${reply}\n\n${entry.bodyText}`,
|
|
300
|
+
content: `**📨 From ${sender}** (Native session cwd: ${entry.from.cwd})${reply}${publication}\n\n${entry.bodyText}`,
|
|
309
301
|
display: true,
|
|
310
302
|
details: { ...entry, bodyText: entry.bodyText },
|
|
311
303
|
};
|
|
312
304
|
}
|
|
313
|
-
async detach(entry: InboundMessageEntry, attention = false): Promise<boolean> {
|
|
314
|
-
if (!attention && entry.message.expectsReply !== true) {
|
|
315
|
-
return false;
|
|
316
|
-
}
|
|
317
|
-
const requestId = randomUUID();
|
|
318
|
-
return await new Promise((resolve) => {
|
|
319
|
-
let settled = false;
|
|
320
|
-
|
|
321
|
-
const finish = (accepted: boolean) => {
|
|
322
|
-
if (settled) {
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
settled = true;
|
|
326
|
-
clearTimeout(timer);
|
|
327
|
-
unsubscribe();
|
|
328
|
-
resolve(accepted);
|
|
329
|
-
};
|
|
330
|
-
const timer = setTimeout(() => finish(false), 500);
|
|
331
|
-
timer.unref();
|
|
332
|
-
const unsubscribe = this.pi.events.on("pi-intercom:detach-response", (payload: unknown) => {
|
|
333
|
-
if (isRecord(payload) && payload.requestId === requestId) {
|
|
334
|
-
finish(payload.accepted === true);
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
try {
|
|
338
|
-
this.pi.events.emit("pi-intercom:detach-request", {
|
|
339
|
-
requestId,
|
|
340
|
-
...(attention ? { reason: "attention" } : {}),
|
|
341
|
-
});
|
|
342
|
-
// Owned waits answer ordinary attention synchronously; an unanswered probe completes now.
|
|
343
|
-
if (attention) {
|
|
344
|
-
finish(false);
|
|
345
|
-
}
|
|
346
|
-
} catch {
|
|
347
|
-
finish(false);
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
305
|
async redeliver(ctx: ExtensionContext, generation: number): Promise<void> {
|
|
352
306
|
if (this.batchTask) {
|
|
353
307
|
await this.batchTask;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { isContextRequest, type IntercomLifecycle } from "./lifecycle.ts";
|
|
3
4
|
import type { ReplyTracker } from "./reply-tracker.ts";
|
|
4
5
|
import {
|
|
5
6
|
INBOUND_CHECKPOINT_TYPE,
|
|
@@ -7,7 +8,13 @@ import {
|
|
|
7
8
|
type InboundMessageEntry,
|
|
8
9
|
type PendingInboundMessage,
|
|
9
10
|
} from "./runtime-types.ts";
|
|
10
|
-
import { isInboundCheckpoint } from "./inbound-record.ts";
|
|
11
|
+
import { isInboundCheckpoint, publicationMetadata, requestedDelivery } from "./inbound-record.ts";
|
|
12
|
+
import {
|
|
13
|
+
isReceiptQuery,
|
|
14
|
+
type NativeReceipt,
|
|
15
|
+
type ReceiptQuery,
|
|
16
|
+
type ReceiptResult,
|
|
17
|
+
} from "./types.ts";
|
|
11
18
|
import { inboundIdFromCustomMessage } from "./message-format.ts";
|
|
12
19
|
import { isRecord } from "./validation.ts";
|
|
13
20
|
|
|
@@ -29,6 +36,85 @@ export class InboundJournal {
|
|
|
29
36
|
this.pi = pi;
|
|
30
37
|
this.lifecycle = lifecycle;
|
|
31
38
|
this.replies = replies;
|
|
39
|
+
pi.events.on("pi-intercom:provenance-query", (request) => {
|
|
40
|
+
if (!isContextRequest(request)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
request.respond(
|
|
44
|
+
request.version === 1 && isReceiptQuery(request)
|
|
45
|
+
? this.receipt(request)
|
|
46
|
+
: { error: "publication_unavailable" },
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
receipt(query: ReceiptQuery): ReceiptResult {
|
|
51
|
+
const ctx = this.lifecycle.live();
|
|
52
|
+
if (!ctx || ctx.sessionManager.getSessionId() !== query.rootSessionId) {
|
|
53
|
+
return { error: "publication_unavailable" };
|
|
54
|
+
}
|
|
55
|
+
let receipt: NativeReceipt | undefined;
|
|
56
|
+
for (const item of ctx.sessionManager.getEntries()) {
|
|
57
|
+
receipt = this.receiptEntry(receipt, query, item);
|
|
58
|
+
}
|
|
59
|
+
return receipt ? { receipt } : { error: "receipt_not_found" };
|
|
60
|
+
}
|
|
61
|
+
private receiptEntry(
|
|
62
|
+
receipt: NativeReceipt | undefined,
|
|
63
|
+
query: ReceiptQuery,
|
|
64
|
+
item: SessionEntry,
|
|
65
|
+
): NativeReceipt | undefined {
|
|
66
|
+
if (
|
|
67
|
+
item.type === "custom" &&
|
|
68
|
+
item.customType === INBOUND_CHECKPOINT_TYPE &&
|
|
69
|
+
isInboundCheckpoint(item.data) &&
|
|
70
|
+
item.data.sessionId === query.rootSessionId
|
|
71
|
+
) {
|
|
72
|
+
return this.receiptCheckpoint(receipt, query, item.id, item.data);
|
|
73
|
+
}
|
|
74
|
+
return receipt && inboundIdFromCustomMessage(item) === query.messageId
|
|
75
|
+
? { ...receipt, placementEntryId: item.id }
|
|
76
|
+
: receipt;
|
|
77
|
+
}
|
|
78
|
+
private receiptCheckpoint(
|
|
79
|
+
receipt: NativeReceipt | undefined,
|
|
80
|
+
query: ReceiptQuery,
|
|
81
|
+
entryId: string,
|
|
82
|
+
data: InboundCheckpoint,
|
|
83
|
+
): NativeReceipt | undefined {
|
|
84
|
+
if ("entry" in data) {
|
|
85
|
+
if (receipt || data.entry.message.id !== query.messageId) {
|
|
86
|
+
return receipt;
|
|
87
|
+
}
|
|
88
|
+
const { from, message, stage, receivedAt } = data.entry;
|
|
89
|
+
return {
|
|
90
|
+
version: 1,
|
|
91
|
+
rootSessionId: query.rootSessionId,
|
|
92
|
+
messageId: query.messageId,
|
|
93
|
+
sender: {
|
|
94
|
+
peerId: from.id,
|
|
95
|
+
rootSessionId: from.rootSessionId,
|
|
96
|
+
incarnation: from.incarnation,
|
|
97
|
+
},
|
|
98
|
+
textSha256: createHash("sha256").update(message.content.text, "utf8").digest("hex"),
|
|
99
|
+
delivery: requestedDelivery(message),
|
|
100
|
+
...publicationMetadata(message),
|
|
101
|
+
checkpointEntryId: entryId,
|
|
102
|
+
receivedAt,
|
|
103
|
+
queued: stage === "queued",
|
|
104
|
+
nativeHandoff: stage === "native",
|
|
105
|
+
placementEntryId: null,
|
|
106
|
+
discarded: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!receipt || data.messageId !== query.messageId) {
|
|
110
|
+
return receipt;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
...receipt,
|
|
114
|
+
queued: receipt.queued || data.stage === "queued",
|
|
115
|
+
nativeHandoff: receipt.nativeHandoff || data.stage === "native",
|
|
116
|
+
discarded: receipt.discarded || data.stage === "discarded",
|
|
117
|
+
};
|
|
32
118
|
}
|
|
33
119
|
peerLeft(id: string): void {
|
|
34
120
|
for (const pending of this.replies.listPending()) {
|
|
@@ -39,7 +125,7 @@ export class InboundJournal {
|
|
|
39
125
|
this.replies.expireSender(id);
|
|
40
126
|
for (const pending of this.queued()) {
|
|
41
127
|
if (pending.from.id === id && pending.message.expectsReply === true) {
|
|
42
|
-
this.
|
|
128
|
+
this.consume(pending.message.id);
|
|
43
129
|
this.checkpoint({ messageId: pending.message.id, stage: "discarded" });
|
|
44
130
|
}
|
|
45
131
|
}
|
|
@@ -57,7 +143,10 @@ export class InboundJournal {
|
|
|
57
143
|
return this.pending.get(id);
|
|
58
144
|
}
|
|
59
145
|
owns(entry: InboundMessageEntry): boolean {
|
|
60
|
-
|
|
146
|
+
const id = entry.message.id;
|
|
147
|
+
return "stage" in entry
|
|
148
|
+
? this.pending.get(id) === entry
|
|
149
|
+
: !this.pending.has(id) && !this.consumed.has(id);
|
|
61
150
|
}
|
|
62
151
|
checkpoint(update: InboundCheckpoint): void {
|
|
63
152
|
const sessionId = this.lifecycle.currentSessionId;
|
|
@@ -96,7 +185,7 @@ export class InboundJournal {
|
|
|
96
185
|
pending.from.id === entry.from.id &&
|
|
97
186
|
pending.message.threadId === entry.message.threadId
|
|
98
187
|
) {
|
|
99
|
-
this.
|
|
188
|
+
this.consume(pending.message.id);
|
|
100
189
|
this.replies.markReplied(pending.message.id);
|
|
101
190
|
}
|
|
102
191
|
}
|
|
@@ -164,7 +253,7 @@ export class InboundJournal {
|
|
|
164
253
|
return;
|
|
165
254
|
}
|
|
166
255
|
if (data.stage === "discarded") {
|
|
167
|
-
this.
|
|
256
|
+
this.consume(data.messageId);
|
|
168
257
|
this.replies.markReplied(data.messageId);
|
|
169
258
|
return;
|
|
170
259
|
}
|
|
@@ -192,6 +281,13 @@ export class InboundJournal {
|
|
|
192
281
|
if (item.customType === "intercom_sent") {
|
|
193
282
|
this.restoreSent(item.data);
|
|
194
283
|
}
|
|
284
|
+
if (
|
|
285
|
+
item.customType === "intercom_received" &&
|
|
286
|
+
isRecord(item.data) &&
|
|
287
|
+
typeof item.data.messageId === "string"
|
|
288
|
+
) {
|
|
289
|
+
this.consume(item.data.messageId);
|
|
290
|
+
}
|
|
195
291
|
}
|
|
196
292
|
private restoreSent(data: unknown): void {
|
|
197
293
|
if (
|
|
@@ -226,7 +322,7 @@ export class InboundJournal {
|
|
|
226
322
|
(entry.type === "custom_message" &&
|
|
227
323
|
["intercom_message", "subagent-human-message"].includes(entry.customType)) ||
|
|
228
324
|
(entry.type === "custom" &&
|
|
229
|
-
[INBOUND_CHECKPOINT_TYPE, "intercom_sent"].includes(entry.customType))
|
|
325
|
+
[INBOUND_CHECKPOINT_TYPE, "intercom_sent", "intercom_received"].includes(entry.customType))
|
|
230
326
|
);
|
|
231
327
|
}
|
|
232
328
|
private recentEntries(ctx: ExtensionContext, fullScan: boolean): readonly SessionEntry[] {
|
|
@@ -1,16 +1,31 @@
|
|
|
1
|
-
import { isMessage, isSessionRegistration, type Message } from "./types.ts";
|
|
1
|
+
import { isMessage, isSessionRegistration, type Message, type MessageDelivery } from "./types.ts";
|
|
2
2
|
import type {
|
|
3
3
|
InboundCheckpoint,
|
|
4
4
|
InboundMessageEntry,
|
|
5
5
|
PendingInboundMessage,
|
|
6
|
-
RequestedDelivery,
|
|
7
6
|
} from "./runtime-types.ts";
|
|
8
7
|
import { finiteNumber, isRecord } from "./validation.ts";
|
|
9
|
-
|
|
8
|
+
|
|
9
|
+
export function publicationMetadata(message: Message): Pick<Message, "guard" | "replyHint"> {
|
|
10
|
+
return {
|
|
11
|
+
guard: message.guard && {
|
|
12
|
+
rootSessionId: message.guard.rootSessionId,
|
|
13
|
+
peerId: message.guard.peerId,
|
|
14
|
+
incarnation: message.guard.incarnation,
|
|
15
|
+
applicationGuard: message.guard.applicationGuard,
|
|
16
|
+
},
|
|
17
|
+
replyHint: message.replyHint && {
|
|
18
|
+
tool: message.replyHint.tool,
|
|
19
|
+
commandId: message.replyHint.commandId,
|
|
20
|
+
commandFingerprint: message.replyHint.commandFingerprint,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function requestedDelivery(message: Message): MessageDelivery {
|
|
10
25
|
if (message.passive === true || message.delivery === "passive") {
|
|
11
26
|
return "passive";
|
|
12
27
|
}
|
|
13
|
-
return message.delivery ??
|
|
28
|
+
return message.delivery ?? "steer";
|
|
14
29
|
}
|
|
15
30
|
function isLocalJournalMessage(value: unknown): value is Message {
|
|
16
31
|
if (!isRecord(value) || !isRecord(value.content)) {
|
|
@@ -32,6 +47,8 @@ function isLocalJournalMessage(value: unknown): value is Message {
|
|
|
32
47
|
"queueMode",
|
|
33
48
|
"threadId",
|
|
34
49
|
"passive",
|
|
50
|
+
"guard",
|
|
51
|
+
"replyHint",
|
|
35
52
|
].every((key) => value[key] === undefined)
|
|
36
53
|
);
|
|
37
54
|
}
|