@fitchmultz/pi-subagents 0.50.0 → 0.51.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 +51 -2
- package/README.md +36 -5
- package/dist/agent/contracts.js +6 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +207 -173
- 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 +346 -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/process.js +198 -25
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +125 -20
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +29 -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 +27 -21
- package/dist/pi-intercom/broker/client-last.d.ts +13 -0
- package/dist/pi-intercom/broker/client-last.js +17 -5
- package/dist/pi-intercom/broker/client.d.ts +60 -0
- package/dist/pi-intercom/broker/client.js +47 -16
- 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 +26 -0
- package/dist/pi-intercom/broker/last.js +57 -32
- 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 +19 -0
- 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 +10 -2
- 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 +12 -38
- package/dist/pi-intercom/index.js +3 -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/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 +2 -0
- package/docs/development.md +40 -1
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +226 -6
- package/docs/performance.md +87 -11
- package/docs/reference.md +157 -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/contracts.ts +8 -34
- package/src/agent/host.ts +19 -20
- package/src/agent/index.ts +224 -190
- 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 +405 -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/process.ts +240 -34
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +125 -27
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +27 -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 +27 -23
- package/src/pi-intercom/broker/client-last.ts +25 -12
- package/src/pi-intercom/broker/client.ts +55 -33
- 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 +86 -33
- 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 +25 -0
- package/src/pi-intercom/connection.ts +21 -10
- 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 +18 -49
- package/src/pi-intercom/index.ts +3 -0
- package/src/pi-intercom/last-message.ts +10 -19
- 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/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,21 +4,23 @@ 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 {
|
|
7
|
+
import type { Message, SessionInfo, ReceiptQuery, ReceiptResult } from "./types.ts";
|
|
8
|
+
import type { NativeReadQuery } from "./broker/last.ts";
|
|
8
9
|
import { readLastMessage } 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;
|
|
@@ -113,6 +115,7 @@ export class IntercomConnection {
|
|
|
113
115
|
this.client.updatePresence({
|
|
114
116
|
status: this.lifecycle.status(),
|
|
115
117
|
...this.lifecycle.health(this.hooks.pendingAsks()),
|
|
118
|
+
applicationGuard: this.lifecycle.snapshot().applicationGuard ?? null,
|
|
116
119
|
});
|
|
117
120
|
}
|
|
118
121
|
}
|
|
@@ -124,7 +127,7 @@ export class IntercomConnection {
|
|
|
124
127
|
this.client = next;
|
|
125
128
|
next.on(
|
|
126
129
|
"last_request",
|
|
127
|
-
(request: { readonly requestId: string; readonly query:
|
|
130
|
+
(request: { readonly requestId: string; readonly query: NativeReadQuery }) => {
|
|
128
131
|
const ctx = this.lifecycle.live();
|
|
129
132
|
const id = next.sessionId;
|
|
130
133
|
if (this.client !== next || !ctx || id === null) {
|
|
@@ -132,6 +135,10 @@ export class IntercomConnection {
|
|
|
132
135
|
return;
|
|
133
136
|
}
|
|
134
137
|
try {
|
|
138
|
+
if ("messageId" in request.query) {
|
|
139
|
+
next.respondLast(request.requestId, this.hooks.receipt(request.query));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
135
142
|
next.respondLast(
|
|
136
143
|
request.requestId,
|
|
137
144
|
readLastMessage(
|
|
@@ -172,6 +179,7 @@ export class IntercomConnection {
|
|
|
172
179
|
}
|
|
173
180
|
this.hooks.disconnected(error);
|
|
174
181
|
this.client = null;
|
|
182
|
+
this.lifecycle.transport(null);
|
|
175
183
|
this.clearReconnect();
|
|
176
184
|
this.scheduleReconnect();
|
|
177
185
|
});
|
|
@@ -197,6 +205,7 @@ export class IntercomConnection {
|
|
|
197
205
|
throw new Error("Intercom runtime no longer active");
|
|
198
206
|
}
|
|
199
207
|
this.client = next;
|
|
208
|
+
this.lifecycle.transport(next.sessionId, next.supportsGuardedDelivery);
|
|
200
209
|
this.syncIdentity(ctx.sessionManager.getSessionId());
|
|
201
210
|
this.attempt = 0;
|
|
202
211
|
return next;
|
|
@@ -279,12 +288,14 @@ export async function resolveConnectedTarget(
|
|
|
279
288
|
return resolution.target.id;
|
|
280
289
|
}
|
|
281
290
|
if (resolution.status === "ambiguous") {
|
|
282
|
-
throw new
|
|
291
|
+
throw new RecoveryError(
|
|
292
|
+
"ambiguous_target",
|
|
283
293
|
`Target "${name}" matches multiple sessions. Use one of these targets: ${formatTargetOptions(resolution.matches, sessions)}.`,
|
|
284
294
|
);
|
|
285
295
|
}
|
|
286
296
|
if (resolution.status === "prefix_too_short") {
|
|
287
|
-
throw new
|
|
297
|
+
throw new RecoveryError(
|
|
298
|
+
"ambiguous_target",
|
|
288
299
|
`Target "${name}" is too short. Use the displayed target from intercom list, such as ${formatTargetOptions(resolution.matches, sessions)}.`,
|
|
289
300
|
);
|
|
290
301
|
}
|
|
@@ -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
|
}
|
|
@@ -2,6 +2,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { IntercomConfig } from "./config.ts";
|
|
3
3
|
import type { IntercomLifecycle } from "./lifecycle.ts";
|
|
4
4
|
import type { InboundDeliveryOwner } from "./inbound-delivery.ts";
|
|
5
|
+
import type { InboundJournal } from "./inbound-journal.ts";
|
|
5
6
|
import type { ReplyTracker } from "./reply-tracker.ts";
|
|
6
7
|
import type { ReplyWait } from "./reply-wait.ts";
|
|
7
8
|
import type { IntercomConnection } from "./connection.ts";
|
|
@@ -11,17 +12,18 @@ import { requestedDelivery } from "./inbound-record.ts";
|
|
|
11
12
|
import { formatAttachments } from "./message-format.ts";
|
|
12
13
|
|
|
13
14
|
type Lifecycle = Readonly<
|
|
14
|
-
Pick<IntercomLifecycle, "generation" | "isIdle" | "live" | "markActivity">
|
|
15
|
+
Pick<IntercomLifecycle, "accepts" | "generation" | "isIdle" | "live" | "markActivity">
|
|
15
16
|
>;
|
|
16
17
|
type InboundDeliveryHandle = Readonly<
|
|
17
|
-
Pick<InboundDeliveryOwner, "batching" | "
|
|
18
|
+
Pick<InboundDeliveryOwner, "batching" | "queue" | "restoring" | "send">
|
|
18
19
|
>;
|
|
19
20
|
type ReplyWaitHandle = Readonly<Pick<ReplyWait, "receive">>;
|
|
20
|
-
type Connection = Readonly<Pick<IntercomConnection, "
|
|
21
|
-
type Replies = Readonly<Pick<ReplyTracker, "
|
|
21
|
+
type Connection = Readonly<Pick<IntercomConnection, "syncStatus">>;
|
|
22
|
+
type Replies = Readonly<Pick<ReplyTracker, "recordIncomingMessage">>;
|
|
22
23
|
|
|
23
24
|
interface InboundOwners {
|
|
24
25
|
readonly delivery: InboundDeliveryHandle;
|
|
26
|
+
readonly journal: Readonly<Pick<InboundJournal, "consume" | "owns">>;
|
|
25
27
|
readonly replies: Replies;
|
|
26
28
|
readonly wait: ReplyWaitHandle;
|
|
27
29
|
readonly connection: Connection;
|
|
@@ -71,33 +73,6 @@ export class IntercomInbound {
|
|
|
71
73
|
}
|
|
72
74
|
return true;
|
|
73
75
|
}
|
|
74
|
-
private async busyNonInteractive(
|
|
75
|
-
entry: InboundMessageEntry,
|
|
76
|
-
ctx: ExtensionContext,
|
|
77
|
-
generation: number,
|
|
78
|
-
): Promise<void> {
|
|
79
|
-
if (entry.message.expectsReply !== true) {
|
|
80
|
-
this.owners.delivery.send(entry, "steer", generation);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const active = this.owners.connection.active;
|
|
84
|
-
if (entry.message.replyTo !== undefined || active?.isConnected() !== true) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
try {
|
|
88
|
-
const result = await active.send(entry.from.id, {
|
|
89
|
-
text: "This agent is running in non-interactive mode and cannot respond to intercom messages while it is working. It will continue its current task and exit when done.",
|
|
90
|
-
replyTo: entry.message.id,
|
|
91
|
-
});
|
|
92
|
-
if (result.delivered && this.lifecycle.live(ctx, generation)) {
|
|
93
|
-
this.owners.replies.markReplied(entry.message.id);
|
|
94
|
-
this.lifecycle.markActivity();
|
|
95
|
-
this.owners.connection.syncStatus();
|
|
96
|
-
}
|
|
97
|
-
} catch {
|
|
98
|
-
/* Best-effort busy reply; the non-interactive task must continue either way. */
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
76
|
private async admit(
|
|
102
77
|
entry: InboundMessageEntry,
|
|
103
78
|
live: ExtensionContext,
|
|
@@ -108,7 +83,7 @@ export class IntercomInbound {
|
|
|
108
83
|
return;
|
|
109
84
|
}
|
|
110
85
|
if (!this.lifecycle.isIdle(active)) {
|
|
111
|
-
|
|
86
|
+
this.busy(entry, generation);
|
|
112
87
|
return;
|
|
113
88
|
}
|
|
114
89
|
if (this.lifecycle.live(live, generation)) {
|
|
@@ -119,30 +94,17 @@ export class IntercomInbound {
|
|
|
119
94
|
);
|
|
120
95
|
}
|
|
121
96
|
}
|
|
122
|
-
private
|
|
123
|
-
entry: InboundMessageEntry,
|
|
124
|
-
ctx: ExtensionContext,
|
|
125
|
-
generation: number,
|
|
126
|
-
): Promise<void> {
|
|
97
|
+
private busy(entry: InboundMessageEntry, generation: number): void {
|
|
127
98
|
const delivery = requestedDelivery(entry.message);
|
|
128
99
|
if (delivery === "steer") {
|
|
129
100
|
this.owners.delivery.send(entry, "steer", generation);
|
|
130
|
-
await this.owners.delivery.detach(entry, entry.message.expectsReply !== true);
|
|
131
101
|
return;
|
|
132
102
|
}
|
|
133
|
-
if (delivery === "queue"
|
|
103
|
+
if (delivery === "queue") {
|
|
134
104
|
this.owners.delivery.send(entry, "followUp", generation);
|
|
135
105
|
return;
|
|
136
106
|
}
|
|
137
|
-
|
|
138
|
-
this.owners.delivery.queue(entry, "passive");
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (!ctx.hasUI) {
|
|
142
|
-
await this.busyNonInteractive(entry, ctx, generation);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
this.owners.delivery.queue(entry, "auto");
|
|
107
|
+
this.owners.delivery.queue(entry, "passive");
|
|
146
108
|
}
|
|
147
109
|
async receive(ctx: ExtensionContext, from: SessionInfo, input: Message): Promise<void> {
|
|
148
110
|
const generation = this.lifecycle.generation;
|
|
@@ -154,10 +116,18 @@ export class IntercomInbound {
|
|
|
154
116
|
if (!live) {
|
|
155
117
|
return;
|
|
156
118
|
}
|
|
119
|
+
if (input.guard && !this.lifecycle.accepts(input.guard)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
157
122
|
// Peer metadata never grants user authority.
|
|
158
123
|
const message = input.human ? { ...input, human: undefined } : input;
|
|
124
|
+
const entry = this.entry(from, message);
|
|
125
|
+
if (!this.owners.journal.owns(entry)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
159
128
|
if (
|
|
160
129
|
this.owners.wait.receive(from, message, () => {
|
|
130
|
+
this.owners.journal.consume(message.id);
|
|
161
131
|
this.lifecycle.markActivity();
|
|
162
132
|
this.owners.connection.syncStatus();
|
|
163
133
|
})
|
|
@@ -167,7 +137,6 @@ export class IntercomInbound {
|
|
|
167
137
|
this.owners.replies.recordIncomingMessage(from, message);
|
|
168
138
|
this.lifecycle.markActivity();
|
|
169
139
|
this.owners.connection.syncStatus();
|
|
170
|
-
const entry = this.entry(from, message);
|
|
171
140
|
await this.admit(entry, live, generation);
|
|
172
141
|
}
|
|
173
142
|
}
|
package/src/pi-intercom/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export default function piIntercomExtension(pi: ExtensionAPI): void {
|
|
|
23
23
|
const connection = new IntercomConnection(lifecycle, config, {
|
|
24
24
|
restoring: () => delivery.restoring,
|
|
25
25
|
pendingAsks: () => replies.listPending().length,
|
|
26
|
+
receipt: (query) => journal.receipt(query),
|
|
26
27
|
incoming: (ctx, from, message) => inbound.receive(ctx, from, message),
|
|
27
28
|
peerLeft: (id) => {
|
|
28
29
|
wait.peerDisconnected(id);
|
|
@@ -38,8 +39,10 @@ export default function piIntercomExtension(pi: ExtensionAPI): void {
|
|
|
38
39
|
replies,
|
|
39
40
|
syncStatus: () => connection.syncStatus(),
|
|
40
41
|
});
|
|
42
|
+
lifecycle.registerContextEvents(() => connection.syncStatus());
|
|
41
43
|
const inbound = new IntercomInbound(lifecycle, config, {
|
|
42
44
|
delivery,
|
|
45
|
+
journal,
|
|
43
46
|
replies,
|
|
44
47
|
wait,
|
|
45
48
|
connection,
|