@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
package/dist/agent/usage.js
CHANGED
|
@@ -1,127 +1,12 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { journalStamp } from "../shared/native-journal.js";
|
|
4
|
-
import { isDeepStrictEqual } from "node:util";
|
|
5
4
|
import { nativeProjection } from "../shared/journal-projections.js";
|
|
6
5
|
import { scanJournal } from "../shared/journal-frames.js";
|
|
7
6
|
import { isRecord } from "../shared/unknown.js";
|
|
8
7
|
import { Assert } from "./typebox.js";
|
|
9
8
|
import { contributionsSchema, resultSchema, } from "./contracts.js";
|
|
10
9
|
import { read, runDirectory, write } from "./storage.js";
|
|
11
|
-
export function sumUsage(values) {
|
|
12
|
-
const total = {
|
|
13
|
-
input: 0,
|
|
14
|
-
output: 0,
|
|
15
|
-
cacheRead: 0,
|
|
16
|
-
cacheWrite: 0,
|
|
17
|
-
totalTokens: 0,
|
|
18
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
19
|
-
};
|
|
20
|
-
for (const { usage } of values) {
|
|
21
|
-
for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"]) {
|
|
22
|
-
total[key] += usage[key];
|
|
23
|
-
}
|
|
24
|
-
for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
|
|
25
|
-
total.cost[key] += usage.cost[key];
|
|
26
|
-
}
|
|
27
|
-
for (const key of ["reasoning", "cacheWrite1h"]) {
|
|
28
|
-
if (usage[key] !== undefined) {
|
|
29
|
-
total[key] = (total[key] ?? 0) + usage[key];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return total;
|
|
34
|
-
}
|
|
35
|
-
function intents(details) {
|
|
36
|
-
if (!isRecord(details) || details.agentUsage === undefined) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const values = details.agentUsage;
|
|
40
|
-
Assert(contributionsSchema, values);
|
|
41
|
-
return values;
|
|
42
|
-
}
|
|
43
|
-
function publishedContributions(entry) {
|
|
44
|
-
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
const message = entry.message;
|
|
48
|
-
if (message.role !== "toolResult" ||
|
|
49
|
-
(message.toolName !== "delegate" && message.toolName !== "agent_runs")) {
|
|
50
|
-
return [];
|
|
51
|
-
}
|
|
52
|
-
const values = intents(message.details);
|
|
53
|
-
if (values === undefined || !isDeepStrictEqual(sumUsage(values), message.usage)) {
|
|
54
|
-
return [];
|
|
55
|
-
}
|
|
56
|
-
return values;
|
|
57
|
-
}
|
|
58
|
-
const receiptProjection = (keys) => {
|
|
59
|
-
if (keys.length === 0 || keys[0] === "type") {
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
if (keys[0] !== "message") {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
if (keys.length === 1 || ["role", "toolName", "usage"].includes(String(keys[1]))) {
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
return keys[1] === "details" && (keys.length === 2 || keys[2] === "agentUsage");
|
|
69
|
-
};
|
|
70
|
-
function priorUsage(ctx) {
|
|
71
|
-
const values = new Map();
|
|
72
|
-
const file = ctx.sessionManager.getSessionFile();
|
|
73
|
-
if (file !== undefined) {
|
|
74
|
-
scanJournal(file, receiptProjection, ({ value: entry }) => {
|
|
75
|
-
for (const value of publishedContributions(entry)) {
|
|
76
|
-
values.set(value.id, value);
|
|
77
|
-
}
|
|
78
|
-
}, { policy: "live", requireNewline: true });
|
|
79
|
-
}
|
|
80
|
-
for (const entry of ctx.sessionManager.getEntries()) {
|
|
81
|
-
for (const value of publishedContributions(entry)) {
|
|
82
|
-
values.set(value.id, value);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return values;
|
|
86
|
-
}
|
|
87
|
-
function uncharged(values, prior) {
|
|
88
|
-
const pending = [];
|
|
89
|
-
for (const value of values) {
|
|
90
|
-
const earlier = prior.get(value.id);
|
|
91
|
-
if (earlier && !isDeepStrictEqual(earlier, value)) {
|
|
92
|
-
throw new Error(`Conflicting native usage contribution ${value.id}`);
|
|
93
|
-
}
|
|
94
|
-
if (!earlier) {
|
|
95
|
-
pending.push(value);
|
|
96
|
-
prior.set(value.id, value);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return pending;
|
|
100
|
-
}
|
|
101
|
-
export function registerUsage(pi) {
|
|
102
|
-
pi.on("message_end", (event, ctx) => {
|
|
103
|
-
const message = event.message;
|
|
104
|
-
if (message.role !== "toolResult" ||
|
|
105
|
-
(message.toolName !== "delegate" && message.toolName !== "agent_runs")) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
const values = intents(message.details);
|
|
109
|
-
if (values === undefined || !isRecord(message.details)) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const pending = uncharged(values, priorUsage(ctx));
|
|
113
|
-
const { usage: _usage, ...rest } = message;
|
|
114
|
-
// Finalized native tool messages are serialized even for concurrent tools.
|
|
115
|
-
// Preparing or discarding a result cannot reserve a charge.
|
|
116
|
-
return {
|
|
117
|
-
message: {
|
|
118
|
-
...rest,
|
|
119
|
-
details: { ...message.details, agentUsage: pending },
|
|
120
|
-
...(pending.length > 0 ? { usage: sumUsage(pending) } : {}),
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
10
|
function billing(entry) {
|
|
126
11
|
if (entry.checkpoint === true) {
|
|
127
12
|
return;
|
package/dist/agent/view.js
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { readOutputPage } from "../shared/output-page.js";
|
|
4
|
-
import { actor, alive } from "./process.js";
|
|
4
|
+
import { actor, alive, observeAlive } from "./process.js";
|
|
5
5
|
import { admissionSchema, commandSchema, exitSchema, successorSchema, } from "./contracts.js";
|
|
6
6
|
import { conversation, read, runDirectory } from "./storage.js";
|
|
7
7
|
import { published } from "./notifications.js";
|
|
8
8
|
import { readCanonicalResult, readResult } from "./usage.js";
|
|
9
9
|
import { Type } from "./typebox.js";
|
|
10
10
|
const commandMetadataSchema = Type.Pick(commandSchema, ["id", "runId", "action", "createdAt"]);
|
|
11
|
-
|
|
11
|
+
function toolResult(value, text) {
|
|
12
12
|
return { content: [{ type: "text", text }], details: value };
|
|
13
13
|
}
|
|
14
|
-
function
|
|
14
|
+
function receiptResult(result) {
|
|
15
|
+
return result === undefined
|
|
16
|
+
? undefined
|
|
17
|
+
: {
|
|
18
|
+
id: result.id,
|
|
19
|
+
owner: result.owner,
|
|
20
|
+
status: result.status,
|
|
21
|
+
endedAt: result.endedAt,
|
|
22
|
+
terminalEntryId: result.terminalEntryId,
|
|
23
|
+
error: result.error,
|
|
24
|
+
accountingError: result.accountingError,
|
|
25
|
+
accountingPending: result.accountingPending,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** `observed` holds batched liveness by actor token; any other actor gets a fresh check. */
|
|
29
|
+
function processView(base, launch, observed) {
|
|
15
30
|
const identity = actor(base, launch);
|
|
16
31
|
const exit = identity
|
|
17
32
|
? read(path.join(conversation(base, launch), `exit-${identity.token}.json`), exitSchema)
|
|
18
33
|
: undefined;
|
|
19
34
|
return {
|
|
20
|
-
live: identity !== undefined && alive(identity),
|
|
35
|
+
live: identity !== undefined && (observed?.get(identity.token) ?? alive(identity)),
|
|
21
36
|
exit: exit ?? null,
|
|
22
37
|
exitKnown: exit !== undefined,
|
|
23
38
|
};
|
|
@@ -60,7 +75,7 @@ function guidanceView(base, launches) {
|
|
|
60
75
|
}
|
|
61
76
|
function metadata(base, launch, result, options) {
|
|
62
77
|
const directory = runDirectory(base, launch.id);
|
|
63
|
-
const process = processView(base, launch);
|
|
78
|
+
const process = processView(base, launch, options.observed);
|
|
64
79
|
const outputFile = path.join(directory, "output.txt");
|
|
65
80
|
const admission = read(path.join(conversation(base, launch), "admissions", `${launch.id}.json`), admissionSchema);
|
|
66
81
|
const status = result?.status ??
|
|
@@ -89,12 +104,18 @@ function metadata(base, launch, result, options) {
|
|
|
89
104
|
guidance: options.guidance,
|
|
90
105
|
};
|
|
91
106
|
}
|
|
92
|
-
|
|
107
|
+
async function summaries(base, launches, ctx) {
|
|
93
108
|
const guidance = guidanceView(base, launches);
|
|
109
|
+
const identities = [
|
|
110
|
+
...new Map(launches.map((launch) => [launch.rootId, actor(base, launch)])).values(),
|
|
111
|
+
].filter((identity) => identity !== undefined);
|
|
112
|
+
const verdicts = await observeAlive(identities);
|
|
113
|
+
const observed = new Map(identities.map((identity, index) => [identity.token, verdicts[index]]));
|
|
94
114
|
return launches.map((launch) => {
|
|
95
115
|
const view = metadata(base, launch, readCanonicalResult(base, launch), {
|
|
96
116
|
ctx,
|
|
97
117
|
guidance: guidance.get(launch.id) ?? [],
|
|
118
|
+
observed,
|
|
98
119
|
});
|
|
99
120
|
return {
|
|
100
121
|
...view,
|
|
@@ -109,7 +130,7 @@ export function summaries(base, launches, ctx) {
|
|
|
109
130
|
};
|
|
110
131
|
});
|
|
111
132
|
}
|
|
112
|
-
|
|
133
|
+
function projection(base, launch, ctx, input = {}) {
|
|
113
134
|
const view = metadata(base, launch, readResult(base, launch), {
|
|
114
135
|
ctx,
|
|
115
136
|
guidance: guidanceView(base, [launch]).get(launch.id) ?? [],
|
|
@@ -119,6 +140,22 @@ export function projection(base, launch, ctx, input = {}) {
|
|
|
119
140
|
output: outputView(view.outputFile, input),
|
|
120
141
|
};
|
|
121
142
|
}
|
|
143
|
+
export function renderReceipt(base, launch, ctx, receipt) {
|
|
144
|
+
const details = { ...projection(base, launch, ctx), ...receipt };
|
|
145
|
+
const model = { ...details, result: receiptResult(details.result) };
|
|
146
|
+
return toolResult(details, JSON.stringify(model, null, 2));
|
|
147
|
+
}
|
|
148
|
+
export async function renderInventory(base, launches, ctx, input) {
|
|
149
|
+
const offset = input.offset ?? 0;
|
|
150
|
+
const runs = await summaries(base, launches.slice(offset, offset + 50), ctx);
|
|
151
|
+
const details = {
|
|
152
|
+
runs,
|
|
153
|
+
offset,
|
|
154
|
+
nextOffset: offset + runs.length < launches.length ? offset + runs.length : undefined,
|
|
155
|
+
total: launches.length,
|
|
156
|
+
};
|
|
157
|
+
return toolResult(details, JSON.stringify(details, null, 2));
|
|
158
|
+
}
|
|
122
159
|
function receiptText(details) {
|
|
123
160
|
return details.nativeReceipt !== null
|
|
124
161
|
? `Native completion receipt: published (${details.nativeReceipt}); publication does not prove model consumption.`
|
|
@@ -148,15 +185,8 @@ function resultText(details) {
|
|
|
148
185
|
`Native journal: ${details.sessionFile}`,
|
|
149
186
|
].join("\n");
|
|
150
187
|
}
|
|
151
|
-
export function
|
|
152
|
-
const
|
|
153
|
-
const details = {
|
|
154
|
-
...view,
|
|
155
|
-
receivedResult: input.receivedResult === true,
|
|
156
|
-
...(input.receivedResult === true && view.result?.contributions !== undefined
|
|
157
|
-
? { agentUsage: view.result.contributions }
|
|
158
|
-
: {}),
|
|
159
|
-
};
|
|
188
|
+
export function renderInspection(base, launch, ctx, input = {}) {
|
|
189
|
+
const details = projection(base, launch, ctx, input);
|
|
160
190
|
return {
|
|
161
191
|
...toolResult(details, resultText(details)),
|
|
162
192
|
isError: details.result?.status === "failed",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { journalStamp } from "../shared/native-journal.js";
|
|
3
|
+
import { missing } from "./storage.js";
|
|
4
|
+
/** Identity and change stamp of a path; "" when absent. */
|
|
5
|
+
export function pathStamp(file) {
|
|
6
|
+
try {
|
|
7
|
+
const stat = fs.statSync(file, { bigint: true, throwIfNoEntry: false });
|
|
8
|
+
return stat === undefined ? "" : journalStamp(stat);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return "unreadable";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Native change hints for one directory identity; owners still reconcile canonical files. */
|
|
15
|
+
export class DirectoryWatch {
|
|
16
|
+
watcher;
|
|
17
|
+
armed = "";
|
|
18
|
+
checked = "";
|
|
19
|
+
failed = false;
|
|
20
|
+
directory;
|
|
21
|
+
relevant;
|
|
22
|
+
wake;
|
|
23
|
+
constructor(directory, relevant, wake) {
|
|
24
|
+
this.directory = directory;
|
|
25
|
+
this.relevant = relevant;
|
|
26
|
+
this.wake = wake;
|
|
27
|
+
}
|
|
28
|
+
ensure() {
|
|
29
|
+
const stat = fs.statSync(this.directory, { bigint: true, throwIfNoEntry: false });
|
|
30
|
+
const identity = stat?.isDirectory() === true ? `${stat.dev}:${stat.ino}` : "";
|
|
31
|
+
const checked = this.checked;
|
|
32
|
+
this.checked = stat === undefined ? "" : `${identity}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
33
|
+
if (this.watcher !== undefined && identity === this.armed) {
|
|
34
|
+
return checked === this.checked ? "watching" : "changed";
|
|
35
|
+
}
|
|
36
|
+
// A replaced directory leaves an inode watch silent on Linux.
|
|
37
|
+
this.close();
|
|
38
|
+
return identity === "" || this.failed ? "unwatched" : this.arm(identity);
|
|
39
|
+
}
|
|
40
|
+
arm(identity) {
|
|
41
|
+
try {
|
|
42
|
+
const watcher = fs.watch(this.directory, { persistent: false }, (_event, file) => {
|
|
43
|
+
if (watcher === this.watcher && this.relevant(file)) {
|
|
44
|
+
this.wake();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
watcher.on("error", () => {
|
|
48
|
+
if (watcher === this.watcher) {
|
|
49
|
+
this.failed = true;
|
|
50
|
+
this.close();
|
|
51
|
+
this.wake();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
this.watcher = watcher;
|
|
55
|
+
this.armed = identity;
|
|
56
|
+
return "armed";
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Unsupported platforms and watch limits keep their owner's polling fallback.
|
|
60
|
+
this.failed = !missing(error);
|
|
61
|
+
return "unwatched";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
close() {
|
|
65
|
+
this.watcher?.close();
|
|
66
|
+
this.watcher = undefined;
|
|
67
|
+
this.armed = "";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** A coalesced wake for one owner loop; raised until that loop takes it. */
|
|
71
|
+
export class Wake {
|
|
72
|
+
raised = true;
|
|
73
|
+
resolve;
|
|
74
|
+
raise = () => {
|
|
75
|
+
this.raised = true;
|
|
76
|
+
this.resolve?.();
|
|
77
|
+
};
|
|
78
|
+
take() {
|
|
79
|
+
const raised = this.raised;
|
|
80
|
+
this.raised = false;
|
|
81
|
+
return raised;
|
|
82
|
+
}
|
|
83
|
+
/** Returns at once for a wake raised since the last take. */
|
|
84
|
+
async sleep(ms) {
|
|
85
|
+
if (this.raised) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
89
|
+
const timer = setTimeout(() => {
|
|
90
|
+
resolve("elapsed");
|
|
91
|
+
}, ms);
|
|
92
|
+
this.resolve = () => {
|
|
93
|
+
resolve("woken");
|
|
94
|
+
};
|
|
95
|
+
await promise;
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
this.resolve = undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
package/dist/build-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const EXTENSION_BUILD = Object.freeze({"version":"0.
|
|
1
|
+
export const EXTENSION_BUILD = Object.freeze({"version":"0.52.0","sha256":"a9faf29451051826907d9343b432c9232529ccaa68c75d681dbcb464702fc25f"});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type IntercomClientOptions as BrokerOptions } from "./pi-intercom/broker/client.ts";
|
|
2
|
+
import { type DeliveryGuard, type LastMessageQuery, type LastMessageResult, type ReceiptQuery, type ReceiptResult, type SendOptions, type SendResult, type SessionInfo } from "./pi-intercom/types.ts";
|
|
3
|
+
export type { ContextTarget, DeliveryGuard, LastMessageQuery, LastMessageResult, MessageDelivery, NativeReceipt, PublicationReplyHint, ReceiptQuery, ReceiptResult, SendResult, SessionInfo, } from "./pi-intercom/types.ts";
|
|
4
|
+
export type GuardedSendOptions = Omit<SendOptions, "messageId" | "guard" | "expectsReply" | "replyTo" | "human" | "topic" | "passive" | "delivery"> & {
|
|
5
|
+
readonly messageId: string;
|
|
6
|
+
readonly guard: DeliveryGuard;
|
|
7
|
+
readonly expectsReply: false;
|
|
8
|
+
readonly delivery: "queue" | "steer" | "passive";
|
|
9
|
+
};
|
|
10
|
+
export interface IntercomClientOptions extends BrokerOptions {
|
|
11
|
+
readonly spawn: false;
|
|
12
|
+
readonly session: Omit<SessionInfo, "id" | "guardedDelivery" | "applicationGuard" | "topics" | "subscriptions">;
|
|
13
|
+
readonly requestedId?: string;
|
|
14
|
+
readonly onError?: (error: Readonly<Error>) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface IntercomClient {
|
|
17
|
+
readonly sessionId: string | null;
|
|
18
|
+
readonly isConnected: () => boolean;
|
|
19
|
+
readonly listSessions: () => Promise<readonly SessionInfo[]>;
|
|
20
|
+
readonly send: (to: string, options: GuardedSendOptions) => Promise<SendResult>;
|
|
21
|
+
readonly last: (to: string, query?: LastMessageQuery) => Promise<LastMessageResult>;
|
|
22
|
+
readonly receipt: (to: string, query: ReceiptQuery) => Promise<ReceiptResult>;
|
|
23
|
+
readonly disconnect: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/** Connect to an existing native broker. No Pi runtime, broker spawn or automatic retry. */
|
|
26
|
+
export declare function connectIntercom(options: IntercomClientOptions): Promise<IntercomClient>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { IntercomClient as BrokerClient, } from "./pi-intercom/broker/client.js";
|
|
2
|
+
import { isDeliveryGuard, } from "./pi-intercom/types.js";
|
|
3
|
+
import { isRecord } from "./shared/unknown.js";
|
|
4
|
+
function requireNoSpawn(value) {
|
|
5
|
+
if (value !== false) {
|
|
6
|
+
throw new Error("The supported Intercom client requires spawn:false.");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function guardedSend(to, input) {
|
|
10
|
+
return (isRecord(input) &&
|
|
11
|
+
isDeliveryGuard(input.guard) &&
|
|
12
|
+
to === input.guard.peerId &&
|
|
13
|
+
typeof input.messageId === "string" &&
|
|
14
|
+
input.expectsReply === false &&
|
|
15
|
+
input.delivery !== undefined);
|
|
16
|
+
}
|
|
17
|
+
/** Connect to an existing native broker. No Pi runtime, broker spawn or automatic retry. */
|
|
18
|
+
export async function connectIntercom(options) {
|
|
19
|
+
requireNoSpawn(options.spawn);
|
|
20
|
+
const client = new BrokerClient(options);
|
|
21
|
+
client.on("error", (error) => options.onError?.(error));
|
|
22
|
+
try {
|
|
23
|
+
await client.connect(options.session, options.requestedId);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
await client.disconnect();
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
get sessionId() {
|
|
31
|
+
return client.sessionId;
|
|
32
|
+
},
|
|
33
|
+
isConnected: () => client.isConnected(),
|
|
34
|
+
listSessions: () => client.listSessions(),
|
|
35
|
+
async send(to, input) {
|
|
36
|
+
if (!guardedSend(to, input)) {
|
|
37
|
+
throw new Error("An exact guarded nonblocking send with an original message ID is required.");
|
|
38
|
+
}
|
|
39
|
+
return await client.send(to, input);
|
|
40
|
+
},
|
|
41
|
+
last: (to, query) => client.last(to, query),
|
|
42
|
+
receipt: (to, query) => client.receipt(to, query),
|
|
43
|
+
disconnect: () => client.disconnect(),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getPiAgentDir(): string;
|
|
@@ -4,6 +4,7 @@ import { IntercomClient } from "./broker/client.js";
|
|
|
4
4
|
import { resolveSessionProjectId } from "./session-targets.js";
|
|
5
5
|
import { Fault, cancelled, log, wait } from "./bridge-protocol.js";
|
|
6
6
|
import { Mailbox } from "./bridge-mailbox.js";
|
|
7
|
+
import { hasErrorCode } from "../shared/unknown.js";
|
|
7
8
|
import { resolveBridgeTarget, bridgeSessionList } from "./bridge-target.js";
|
|
8
9
|
const MAX_REQUESTS = 8;
|
|
9
10
|
const IDLE_TIMEOUT = 5 * 60 * 1000;
|
|
@@ -222,7 +223,7 @@ export class Session {
|
|
|
222
223
|
}
|
|
223
224
|
catch (error) {
|
|
224
225
|
cancelled(signal);
|
|
225
|
-
if (error
|
|
226
|
+
if (hasErrorCode(error, "list_timeout")) {
|
|
226
227
|
throw new Fault(504, "list_timeout", "Broker session list deadline exceeded.");
|
|
227
228
|
}
|
|
228
229
|
throw new Fault(503, "broker_unavailable", "Broker session list failed.");
|
|
@@ -7,9 +7,11 @@ import { getBrokerRuntimeDir, prepareBrokerSocketPath } from "./paths.js";
|
|
|
7
7
|
import { brokerPidRecord } from "./pid.js";
|
|
8
8
|
import { assertBrokerCanStart } from "./spawn.js";
|
|
9
9
|
import { LastMessageRelay } from "./last.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { canSeeSession, findSessions } from "./targets.js";
|
|
11
|
+
import { ParkedSessions, reachable } from "./parked.js";
|
|
12
|
+
import { acceptBrokerConnection, registeredSessionInfo } from "./server-connection.js";
|
|
13
|
+
import { guardedDeliveryError } from "./guard.js";
|
|
14
|
+
import { compactTopicMessage, normalizeMessage, normalizeSessionInfo, } from "../types.js";
|
|
13
15
|
import { errorMessage, isRecord } from "../../shared/unknown.js";
|
|
14
16
|
import { messageSender, snapshotFrames, TopicSnapshot, publicationMessage, deliveryAcknowledgement, shouldReplaceDelivery, } from "./snapshots.js";
|
|
15
17
|
const PID_PATH = join(getBrokerRuntimeDir(), "broker.pid");
|
|
@@ -17,6 +19,7 @@ const REPLACE_DELIVERY_DELAY_MS = 1500;
|
|
|
17
19
|
class IntercomBroker {
|
|
18
20
|
sessions = new Map();
|
|
19
21
|
lastMessages = new LastMessageRelay(this.sessions);
|
|
22
|
+
parking = new ParkedSessions(this.sessions, this.deliverMessage.bind(this));
|
|
20
23
|
pendingReplaceDeliveries = new Map();
|
|
21
24
|
server;
|
|
22
25
|
shutdownTimer = null;
|
|
@@ -55,7 +58,7 @@ class IntercomBroker {
|
|
|
55
58
|
}
|
|
56
59
|
this.shutdownTimer = setTimeout(() => {
|
|
57
60
|
this.shutdownTimer = null;
|
|
58
|
-
if (this.
|
|
61
|
+
if (this.parking.exited) {
|
|
59
62
|
console.log("No sessions connected, shutting down");
|
|
60
63
|
this.shutdown();
|
|
61
64
|
}
|
|
@@ -83,7 +86,7 @@ class IntercomBroker {
|
|
|
83
86
|
}
|
|
84
87
|
switch (msg.type) {
|
|
85
88
|
case "unregister":
|
|
86
|
-
this.removeSession(currentId);
|
|
89
|
+
this.removeSession(currentId, socket);
|
|
87
90
|
setId(null);
|
|
88
91
|
break;
|
|
89
92
|
case "list":
|
|
@@ -93,7 +96,8 @@ class IntercomBroker {
|
|
|
93
96
|
this.send(socket, msg, currentId);
|
|
94
97
|
break;
|
|
95
98
|
case "last_request":
|
|
96
|
-
|
|
99
|
+
// Reads never spawn a parked child.
|
|
100
|
+
this.lastMessages.request(currentId, msg, (id) => this.parking.reader(id));
|
|
97
101
|
break;
|
|
98
102
|
case "last_response":
|
|
99
103
|
this.lastMessages.respond(currentId, msg);
|
|
@@ -101,28 +105,29 @@ class IntercomBroker {
|
|
|
101
105
|
case "presence":
|
|
102
106
|
this.presence(msg, currentId);
|
|
103
107
|
break;
|
|
108
|
+
case "park":
|
|
109
|
+
case "unpark":
|
|
110
|
+
this.parking.park(socket, currentId, msg.type === "park");
|
|
111
|
+
break;
|
|
104
112
|
default:
|
|
105
113
|
throw new Error(`Unknown client message type: ${msg.type}`);
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
register(socket, msg, setId) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
assertMainParent(msg.session, this.sessions);
|
|
113
|
-
const requestedId = typeof msg.requestedId === "string" && /^[a-zA-Z0-9_-]{8,80}$/.test(msg.requestedId)
|
|
114
|
-
? msg.requestedId
|
|
115
|
-
: undefined;
|
|
116
|
-
const id = requestedId !== undefined && !this.sessions.has(requestedId) ? requestedId : randomUUID();
|
|
117
|
-
const info = { ...msg.session, id, lastSeen: msg.session.lastSeen ?? Date.now() };
|
|
117
|
+
const registered = registeredSessionInfo(msg, this.sessions);
|
|
118
|
+
const { id, resumed } = this.parking.claim(msg.requestedId, registered);
|
|
119
|
+
const info = { ...registered, id };
|
|
118
120
|
snapshotFrames(randomUUID(), [info], true);
|
|
119
121
|
const topicFrames = msg.topicFrames === true;
|
|
120
122
|
this.sessions.set(id, {
|
|
121
123
|
socket,
|
|
122
124
|
info: messageSender(info),
|
|
123
|
-
topics
|
|
125
|
+
// A woken generation registers no topics; the parked identity's owner keeps them.
|
|
126
|
+
topics: resumed?.topics ?? new TopicSnapshot(info),
|
|
124
127
|
topicFrames,
|
|
125
128
|
lastMessages: msg.lastMessages === true,
|
|
129
|
+
parking: msg.parking === true,
|
|
130
|
+
receiptReads: msg.receiptReads === 1,
|
|
126
131
|
});
|
|
127
132
|
setId(id);
|
|
128
133
|
if (this.shutdownTimer) {
|
|
@@ -133,18 +138,24 @@ class IntercomBroker {
|
|
|
133
138
|
type: "registered",
|
|
134
139
|
sessionId: id,
|
|
135
140
|
lastMessages: true,
|
|
141
|
+
parking: msg.parking === true,
|
|
142
|
+
receiptReads: 1,
|
|
143
|
+
...(msg.guardedDelivery === 1 ? { guardedDelivery: 1 } : {}),
|
|
136
144
|
...(topicFrames ? { topicsSupported: true, topicFrames: true } : {}),
|
|
137
145
|
});
|
|
146
|
+
this.parking.registered(id, resumed);
|
|
138
147
|
}
|
|
139
|
-
removeSession(sessionId) {
|
|
148
|
+
removeSession(sessionId, socket) {
|
|
140
149
|
const session = this.sessions.get(sessionId);
|
|
141
|
-
if (
|
|
150
|
+
if (session?.socket !== socket) {
|
|
142
151
|
return;
|
|
143
152
|
}
|
|
144
|
-
this.sessions.delete(sessionId);
|
|
145
153
|
this.lastMessages.disconnected(sessionId);
|
|
146
|
-
this.
|
|
147
|
-
|
|
154
|
+
if (!this.parking.closed(session)) {
|
|
155
|
+
this.sessions.delete(sessionId);
|
|
156
|
+
this.clearPendingReplaceDeliveries(sessionId);
|
|
157
|
+
this.broadcast({ type: "session_left", sessionId }, session.info);
|
|
158
|
+
}
|
|
148
159
|
this.scheduleShutdownCheck();
|
|
149
160
|
}
|
|
150
161
|
planPublications(fromId, from, publication) {
|
|
@@ -158,7 +169,7 @@ class IntercomBroker {
|
|
|
158
169
|
continue;
|
|
159
170
|
}
|
|
160
171
|
const message = publicationMessage(publication, subscription.awaitRelease === true);
|
|
161
|
-
const error = this.validateDeliveryPayload(from, message, target
|
|
172
|
+
const error = this.validateDeliveryPayload(from, message, target);
|
|
162
173
|
if (error !== null) {
|
|
163
174
|
throw new Error(error);
|
|
164
175
|
}
|
|
@@ -240,6 +251,15 @@ class IntercomBroker {
|
|
|
240
251
|
const targets = findSessions(this.sessions, msg.to, currentId);
|
|
241
252
|
const target = targets[0];
|
|
242
253
|
if (targets.length === 1) {
|
|
254
|
+
const guardError = guardedDeliveryError(this.sessions, msg.to, message);
|
|
255
|
+
if (guardError !== null) {
|
|
256
|
+
writeMessage(socket, {
|
|
257
|
+
type: "delivery_failed",
|
|
258
|
+
messageId: message.id,
|
|
259
|
+
reason: guardError,
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
243
263
|
const fromSession = this.sessions.get(currentId);
|
|
244
264
|
if (!fromSession) {
|
|
245
265
|
writeMessage(socket, {
|
|
@@ -280,11 +300,15 @@ class IntercomBroker {
|
|
|
280
300
|
"lastIntercomActivity",
|
|
281
301
|
"subscriptions",
|
|
282
302
|
"topics",
|
|
303
|
+
"applicationGuard",
|
|
283
304
|
]) {
|
|
284
305
|
if (msg[key] !== undefined) {
|
|
285
306
|
update[key] = msg[key];
|
|
286
307
|
}
|
|
287
308
|
}
|
|
309
|
+
if (msg.applicationGuard === null) {
|
|
310
|
+
delete update.applicationGuard;
|
|
311
|
+
}
|
|
288
312
|
const nextInfo = normalizeSessionInfo(update);
|
|
289
313
|
if (!nextInfo) {
|
|
290
314
|
throw new Error("Invalid presence update");
|
|
@@ -297,22 +321,16 @@ class IntercomBroker {
|
|
|
297
321
|
}
|
|
298
322
|
sendToSession(fromId, toId, from, message) {
|
|
299
323
|
const target = this.sessions.get(toId);
|
|
300
|
-
if (
|
|
301
|
-
return {
|
|
302
|
-
id: message.id,
|
|
303
|
-
accepted: false,
|
|
304
|
-
delivered: false,
|
|
305
|
-
reason: "Recipient disconnected before delivery",
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
if (shouldReplaceDelivery(message, target.info)) {
|
|
324
|
+
if (target && target.parked === undefined && shouldReplaceDelivery(message, target.info)) {
|
|
309
325
|
return this.queueReplaceDelivery(fromId, toId, from, message);
|
|
310
326
|
}
|
|
311
327
|
const reason = this.deliverMessage(toId, from, message);
|
|
328
|
+
const queued = reason === null && target?.parked !== undefined;
|
|
312
329
|
return {
|
|
313
330
|
id: message.id,
|
|
314
331
|
accepted: reason === null,
|
|
315
|
-
delivered: reason === null,
|
|
332
|
+
delivered: reason === null && !queued,
|
|
333
|
+
...(queued ? { queued } : {}),
|
|
316
334
|
...(reason !== null ? { reason } : {}),
|
|
317
335
|
};
|
|
318
336
|
}
|
|
@@ -333,7 +351,7 @@ class IntercomBroker {
|
|
|
333
351
|
return `${fromId}\0${toId}\0${threadId}`;
|
|
334
352
|
}
|
|
335
353
|
queueReplaceDelivery(fromId, toId, from, message) {
|
|
336
|
-
const validationError = this.validateDeliveryPayload(from, message, this.sessions.get(toId)
|
|
354
|
+
const validationError = this.validateDeliveryPayload(from, message, this.sessions.get(toId));
|
|
337
355
|
if (validationError !== null) {
|
|
338
356
|
return { id: message.id, accepted: false, delivered: false, reason: validationError };
|
|
339
357
|
}
|
|
@@ -365,24 +383,25 @@ class IntercomBroker {
|
|
|
365
383
|
}
|
|
366
384
|
}
|
|
367
385
|
}
|
|
368
|
-
|
|
386
|
+
/** One admission rule; retained input is checked as the uncompacted envelope any later
|
|
387
|
+
* transport generation accepts. */
|
|
388
|
+
validateDeliveryPayload(from, message, target) {
|
|
389
|
+
const compact = target?.parked === undefined && target?.topicFrames === true;
|
|
369
390
|
return (validateIntercomMessageSize({
|
|
370
391
|
type: "message",
|
|
371
392
|
from: messageSender(from),
|
|
372
|
-
message:
|
|
393
|
+
message: compact ? compactTopicMessage(message) : message,
|
|
373
394
|
})?.message ?? null);
|
|
374
395
|
}
|
|
375
396
|
deliverMessage(toId, from, message) {
|
|
376
397
|
const target = this.sessions.get(toId);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return "Recipient disconnected before delivery";
|
|
398
|
+
const refused = guardedDeliveryError(this.sessions, toId, message) ??
|
|
399
|
+
this.validateDeliveryPayload(from, message, target);
|
|
400
|
+
if (refused !== null || this.parking.retain(target, from, message)) {
|
|
401
|
+
return refused;
|
|
382
402
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return validationError;
|
|
403
|
+
if (!reachable(target)) {
|
|
404
|
+
return "Recipient disconnected before delivery";
|
|
386
405
|
}
|
|
387
406
|
this.touchActivity(toId, true);
|
|
388
407
|
try {
|