@fitchmultz/pi-subagents 0.52.2 → 0.52.4
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 +20 -0
- package/dist/agent/child-runtime.js +4 -4
- package/dist/agent/host.js +13 -0
- package/dist/agent/index.js +4 -1
- package/dist/agent/observe-card.js +3 -3
- package/dist/agent/observe.js +30 -26
- package/dist/agent/parking.js +15 -1
- package/dist/agent/runner.js +4 -2
- package/dist/agent/usage.js +92 -47
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/bridge-mailbox.js +18 -7
- package/dist/pi-intercom/bridge-session.js +16 -3
- package/dist/pi-intercom/bridge-wake.js +7 -9
- package/dist/pi-intercom/inbound-journal.js +4 -3
- package/dist/pi-intercom/inbound-record.js +12 -0
- package/dist/pi-intercom/ui/session-format.js +15 -6
- package/docs/performance.md +104 -7
- package/package.json +1 -1
- package/scripts/native-package-smoke.mjs +12 -3
- package/src/agent/child-runtime.ts +4 -6
- package/src/agent/host.ts +15 -0
- package/src/agent/index.ts +4 -1
- package/src/agent/observe-card.ts +3 -3
- package/src/agent/observe.ts +39 -37
- package/src/agent/parking.ts +14 -2
- package/src/agent/runner.ts +4 -2
- package/src/agent/usage.ts +116 -60
- package/src/pi-intercom/bridge-mailbox.ts +17 -7
- package/src/pi-intercom/bridge-session.ts +14 -9
- package/src/pi-intercom/bridge-wake.ts +7 -8
- package/src/pi-intercom/inbound-journal.ts +9 -3
- package/src/pi-intercom/inbound-record.ts +12 -0
- package/src/pi-intercom/ui/session-format.ts +16 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.52.4] - 2026-10-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Classify a certificate-selected bridge session deadline as `certificate_expired`, rather than idle-lease expiry, if the timer fires just before the wall-clock deadline. The timer rechecks without renewing the idle lease; depending on the connection-expiry race, a client can still see a connection reset.
|
|
10
|
+
|
|
11
|
+
## [0.52.3] - 2026-10-29
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Build Intercom's duplicate-name set once per session list instead of once per row. Session names, duplicate labels and target resolution are unchanged.
|
|
16
|
+
- Hash parked child state in bounded chunks when answering history requests or restoring a child. The digest and native state validation are unchanged.
|
|
17
|
+
- Skip the bash-owner inventory lookup for child tool calls that are not bash commands. Bash ownership checks and child delegation restrictions are unchanged.
|
|
18
|
+
- Avoid revalidating unrelated incoming messages when looking up an Intercom receipt. Receipt contents and branch-wide history remain unchanged.
|
|
19
|
+
- Reuse the child-status widget's display between inventory updates instead of rebuilding it on every parent frame. Theme changes, activity, errors and click targets remain visible.
|
|
20
|
+
- Batch optional bridge acknowledgements in one SQLite transaction and stop reading wake notifications once the next actionable message is known. A failed acknowledgement leaves the group pending. Later non-wake cleanup can wait behind a pending wake.
|
|
21
|
+
- Stop the live child viewer from rebuilding native renderers after each streamed update. Assistant and tool cards now keep the native component's own update result and only refresh the card's outer layout. On the maintained fork, unchanged thinking or prose is no longer re-parsed while a later block streams. On both hosts, tool-argument and progress updates run native tool rendering once per update instead of twice. Visible output, theme changes, resize, selection and expansion are unchanged.
|
|
22
|
+
- Stop each child continuation from rereading its whole native journal to record usage. A resident child keeps the IDs and billing fields of records it already validated and parses only records appended after its previous LF boundary. Unchanged journals are not reread; same-size edits, shrink, inode replacement, non-v3 headers and failed reads rebuild from the start. Inherited baselines, terminal stop, required usage and attribution still apply to every run. A restarted or restored child and the parent's pending-accounting repair still read the journal once from the start.
|
|
23
|
+
- Reuse the running Pi's SDK for the first delegation, `/agents` and parked-history restore in production-only installs, instead of importing a second copy of the same SDK from disk. In five alternating pairs per host on a busy macOS machine, the parent no longer grew by 66–85 MiB during its first `delegate` call. The median time to that call's receipt fell from 1.4 s to 0.1 s on the maintained fork. Official Pi's median fell from 1.9 s to 0.1 s, but the size of that gain is inconclusive under the measured load: one run with the change took 2.3 s. Installs with their own SDK copy, a set `PI_PACKAGE_DIR` and detached children still load the selected Pi package directly.
|
|
24
|
+
|
|
5
25
|
## [0.52.2] - 2026-10-29
|
|
6
26
|
|
|
7
27
|
### Fixed
|
|
@@ -6,11 +6,11 @@ function guard(pi, directory, token) {
|
|
|
6
6
|
if (event.toolName === "delegate" || event.toolName === "agent_runs") {
|
|
7
7
|
return { block: true, reason: "Only a main session may delegate or control children." };
|
|
8
8
|
}
|
|
9
|
+
if (event.toolName !== "bash" || typeof event.input.command !== "string") {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
9
12
|
const owner = pi.getAllTools().find((tool) => tool.name === "bash")?.sourceInfo;
|
|
10
|
-
if (
|
|
11
|
-
typeof event.input.command === "string" &&
|
|
12
|
-
owner &&
|
|
13
|
-
(owner.source === "builtin" || localCwdOwner(owner))) {
|
|
13
|
+
if (owner && (owner.source === "builtin" || localCwdOwner(owner))) {
|
|
14
14
|
// Preserve the selected local owner's cwd/env and leave remote/custom tools alone.
|
|
15
15
|
const input = event.input;
|
|
16
16
|
input.command = shellCommand(event.input.command, directory, token);
|
package/dist/agent/host.js
CHANGED
|
@@ -55,7 +55,20 @@ export function packageRoot() {
|
|
|
55
55
|
}
|
|
56
56
|
return path.dirname(manifest);
|
|
57
57
|
}
|
|
58
|
+
// Pi's resolution of this extension's SDK import: the running host in production installs,
|
|
59
|
+
// possibly a package-local copy in development checkouts.
|
|
60
|
+
let resolvedSDK;
|
|
61
|
+
export function rememberSDK(sdk) {
|
|
62
|
+
resolvedSDK = sdk;
|
|
63
|
+
}
|
|
58
64
|
export async function loadSDK(root) {
|
|
65
|
+
// Reuse it only as the selected root. PI_PACKAGE_DIR replaces getPackageDir()'s own
|
|
66
|
+
// location, so only an unset override proves that identity.
|
|
67
|
+
if (resolvedSDK !== undefined &&
|
|
68
|
+
(process.env.PI_PACKAGE_DIR ?? "") === "" &&
|
|
69
|
+
fs.realpathSync(resolvedSDK.getPackageDir()) === fs.realpathSync(root)) {
|
|
70
|
+
return resolvedSDK;
|
|
71
|
+
}
|
|
59
72
|
// Selected host SDK, not a second package-local runtime hidden behind Jiti aliases.
|
|
60
73
|
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
61
74
|
const sdk = await import(__rewriteRelativeImportExtension(pathToFileURL(path.join(root, "dist/index.js")).href));
|
package/dist/agent/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import * as loadedSDK from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { actor, alive, ensureActor, ownIdentity, parkFailure, requestStop, signalProcess, stopChildren, } from "./process.js";
|
|
5
|
-
import { isChild, errorText } from "./host.js";
|
|
6
|
+
import { isChild, errorText, rememberSDK } from "./host.js";
|
|
6
7
|
import { serveParked } from "./parking.js";
|
|
7
8
|
import { resultSchema, attachmentSchema, admissionSchema, commandSchema, } from "./contracts.js";
|
|
8
9
|
import { delegateSchema, runsSchema } from "./tool-schemas.js";
|
|
@@ -438,5 +439,7 @@ class AgentRuntime {
|
|
|
438
439
|
}
|
|
439
440
|
}
|
|
440
441
|
export default function agents(pi) {
|
|
442
|
+
// Delegation, /agents and parked history then avoid importing a second SDK graph.
|
|
443
|
+
rememberSDK(loadedSDK);
|
|
441
444
|
new AgentRuntime(pi).register();
|
|
442
445
|
}
|
|
@@ -137,7 +137,7 @@ export class Card {
|
|
|
137
137
|
if (this.arguments !== value.args) {
|
|
138
138
|
this.tool.updateArgs(projected(value.args) ?? {});
|
|
139
139
|
this.arguments = value.args;
|
|
140
|
-
this.
|
|
140
|
+
this.cache = undefined;
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
updateTool(value, options, preparing) {
|
|
@@ -156,7 +156,7 @@ export class Card {
|
|
|
156
156
|
this.phase = phase;
|
|
157
157
|
this.value = source.result;
|
|
158
158
|
this.ended = ended;
|
|
159
|
-
this.
|
|
159
|
+
this.cache = undefined;
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
preview(value, isError, partial) {
|
|
@@ -188,7 +188,7 @@ export class Card {
|
|
|
188
188
|
this.status?.setText(message.ended ? "Awaiting journal publication" : "In progress");
|
|
189
189
|
this.blocks = message.blocks.slice();
|
|
190
190
|
this.ended = message.ended;
|
|
191
|
-
this.
|
|
191
|
+
this.cache = undefined;
|
|
192
192
|
}
|
|
193
193
|
invalidate() {
|
|
194
194
|
this.cache = undefined;
|
package/dist/agent/observe.js
CHANGED
|
@@ -176,6 +176,7 @@ class Observer {
|
|
|
176
176
|
const scanning = this.update()
|
|
177
177
|
.catch((error) => {
|
|
178
178
|
this.error = plainText(errorText(error));
|
|
179
|
+
this.widget();
|
|
179
180
|
})
|
|
180
181
|
.finally(() => {
|
|
181
182
|
if (this.scanning !== scanning) {
|
|
@@ -267,21 +268,22 @@ class Observer {
|
|
|
267
268
|
this.widgetSignature = "";
|
|
268
269
|
return;
|
|
269
270
|
}
|
|
270
|
-
const
|
|
271
|
+
const visible = rows.slice(0, 4);
|
|
272
|
+
const more = rows.length - visible.length;
|
|
273
|
+
const snapshot = { status: this.status(), rows: visible, more, error: this.error };
|
|
274
|
+
const signature = JSON.stringify(snapshot);
|
|
271
275
|
if (signature === this.widgetSignature) {
|
|
272
276
|
return;
|
|
273
277
|
}
|
|
274
278
|
this.widgetSignature = signature;
|
|
275
279
|
if (this.widgetVisible) {
|
|
276
|
-
this.widgetRender?.();
|
|
280
|
+
this.widgetRender?.(snapshot);
|
|
277
281
|
return;
|
|
278
282
|
}
|
|
279
283
|
this.widgetVisible = true;
|
|
280
284
|
ctx.ui.setWidget(STATUS_KEY, (tui, theme) => {
|
|
281
|
-
this.widgetRender = () => {
|
|
282
|
-
tui.requestRender();
|
|
283
|
-
};
|
|
284
285
|
const content = new Container();
|
|
286
|
+
let shown = snapshot;
|
|
285
287
|
const open = (id) => {
|
|
286
288
|
if (ctx !== this.ctx) {
|
|
287
289
|
return;
|
|
@@ -290,29 +292,31 @@ class Observer {
|
|
|
290
292
|
ctx.ui.notify(errorText(error), "error");
|
|
291
293
|
});
|
|
292
294
|
};
|
|
295
|
+
const build = () => {
|
|
296
|
+
content.clear();
|
|
297
|
+
content.addChild(actionText(theme.fg("accent", shown.status), () => open("")));
|
|
298
|
+
for (const row of shown.rows) {
|
|
299
|
+
const glyph = row.live ? "●" : "○";
|
|
300
|
+
const color = row.live ? "accent" : "warning";
|
|
301
|
+
content.addChild(actionText(`${theme.fg(color, glyph)} ${theme.bold(row.name)} · ${row.activity ?? row.status}`, () => open(row.id)));
|
|
302
|
+
}
|
|
303
|
+
if (shown.more > 0) {
|
|
304
|
+
content.addChild(actionText(theme.fg("dim", `+${shown.more} more · /agents`), () => open("")));
|
|
305
|
+
}
|
|
306
|
+
if (shown.error.length > 0) {
|
|
307
|
+
content.addChild(new TruncatedText(theme.fg("warning", shown.error), 0, 0));
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
build();
|
|
311
|
+
this.widgetRender = (next) => {
|
|
312
|
+
shown = next;
|
|
313
|
+
build();
|
|
314
|
+
tui.requestRender();
|
|
315
|
+
};
|
|
293
316
|
return {
|
|
294
|
-
render: (width) =>
|
|
295
|
-
const strip = this.strip();
|
|
296
|
-
const visible = strip.slice(0, 4);
|
|
297
|
-
content.clear();
|
|
298
|
-
content.addChild(actionText(theme.fg("accent", this.status()), () => open("")));
|
|
299
|
-
for (const row of visible) {
|
|
300
|
-
const glyph = row.live ? "●" : "○";
|
|
301
|
-
const color = row.live ? "accent" : "warning";
|
|
302
|
-
content.addChild(actionText(`${theme.fg(color, glyph)} ${theme.bold(row.name)} · ${row.activity ?? row.status}`, () => open(row.id)));
|
|
303
|
-
}
|
|
304
|
-
if (strip.length > visible.length) {
|
|
305
|
-
content.addChild(actionText(theme.fg("dim", `+${strip.length - visible.length} more · /agents`), () => open("")));
|
|
306
|
-
}
|
|
307
|
-
if (this.error.length > 0) {
|
|
308
|
-
content.addChild(new TruncatedText(theme.fg("warning", this.error), 0, 0));
|
|
309
|
-
}
|
|
310
|
-
return content.render(width);
|
|
311
|
-
},
|
|
317
|
+
render: (width) => content.render(width),
|
|
312
318
|
handleMouse: (event) => content.handleMouse(event),
|
|
313
|
-
invalidate:
|
|
314
|
-
// Render reads current rows, expansion and theme on every frame.
|
|
315
|
-
},
|
|
319
|
+
invalidate: build,
|
|
316
320
|
};
|
|
317
321
|
}, { placement: "aboveEditor" });
|
|
318
322
|
}
|
package/dist/agent/parking.js
CHANGED
|
@@ -36,7 +36,21 @@ async function cut(pi, session, { quiet, errors, signal }) {
|
|
|
36
36
|
return undefined;
|
|
37
37
|
}
|
|
38
38
|
const PARK_AFTER_MS = 5 * 60_000;
|
|
39
|
-
|
|
39
|
+
/** SHA-256 of every state byte through one reused chunk, like the native state reader. */
|
|
40
|
+
function digest(file) {
|
|
41
|
+
const hash = createHash("sha256");
|
|
42
|
+
const fd = fs.openSync(file, "r");
|
|
43
|
+
try {
|
|
44
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
45
|
+
for (let count = fs.readSync(fd, chunk); count > 0; count = fs.readSync(fd, chunk)) {
|
|
46
|
+
hash.update(chunk.subarray(0, count));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
fs.closeSync(fd);
|
|
51
|
+
}
|
|
52
|
+
return hash.digest("hex");
|
|
53
|
+
}
|
|
40
54
|
/** Parked authority names this session and exactly the saved generation's state bytes. */
|
|
41
55
|
function exact(restore, parked, state, launch) {
|
|
42
56
|
if (parked.sessionId !== launch.sessionId ||
|
package/dist/agent/runner.js
CHANGED
|
@@ -5,7 +5,7 @@ import { launchAt, missing, read, runDirectory, write } from "./storage.js";
|
|
|
5
5
|
import { ownIdentity, ParentCheck, shells, signalProcess } from "./process.js";
|
|
6
6
|
import { errorText, loadSDK } from "./host.js";
|
|
7
7
|
import { Parking } from "./parking.js";
|
|
8
|
-
import {
|
|
8
|
+
import { UsageIndex } from "./usage.js";
|
|
9
9
|
import { initializeCwd } from "./cwd.js";
|
|
10
10
|
import { childRuntime } from "./child-runtime.js";
|
|
11
11
|
import { ActorObservation } from "./observation.js";
|
|
@@ -47,6 +47,8 @@ class Actor {
|
|
|
47
47
|
started = new Set();
|
|
48
48
|
// Actor-local validation metadata only; never retain command prompts or receipt authority.
|
|
49
49
|
validated = new Map();
|
|
50
|
+
// Every run of this conversation shares one native journal; later runs read only its suffix.
|
|
51
|
+
accounting = new UsageIndex();
|
|
50
52
|
session;
|
|
51
53
|
observation;
|
|
52
54
|
context;
|
|
@@ -97,7 +99,7 @@ class Actor {
|
|
|
97
99
|
}
|
|
98
100
|
usage(launch, baseline, terminal) {
|
|
99
101
|
try {
|
|
100
|
-
return { contributions:
|
|
102
|
+
return { contributions: this.accounting.read(launch, baseline, terminal) };
|
|
101
103
|
}
|
|
102
104
|
catch (error) {
|
|
103
105
|
return {
|
package/dist/agent/usage.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
-
import { journalStamp } from "../shared/native-journal.js";
|
|
3
|
+
import { journalStamp, nativeAppend } from "../shared/native-journal.js";
|
|
4
4
|
import { nativeProjection } from "../shared/journal-projections.js";
|
|
5
5
|
import { scanJournal } from "../shared/journal-frames.js";
|
|
6
6
|
import { isRecord } from "../shared/unknown.js";
|
|
7
7
|
import { Assert } from "./typebox.js";
|
|
8
8
|
import { contributionsSchema, resultSchema, } from "./contracts.js";
|
|
9
9
|
import { read, runDirectory, write } from "./storage.js";
|
|
10
|
+
/** Billing evidence of one native entry. Missing required usage is an error only outside a baseline. */
|
|
10
11
|
function billing(entry) {
|
|
11
12
|
if (entry.checkpoint === true) {
|
|
12
13
|
return;
|
|
@@ -16,11 +17,11 @@ function billing(entry) {
|
|
|
16
17
|
(entry.type === "message" && !["assistant", "toolResult"].some((role) => role === message.role))) {
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
throw new Error("Required native usage is unavailable.");
|
|
20
|
+
if (message.usage === undefined) {
|
|
21
|
+
return entry.type === "usage" || message.role === "assistant" ? "missing" : undefined;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
const { role, provider, model, responseModel, usage } = message;
|
|
24
|
+
return { role, provider, model, responseModel, usage };
|
|
24
25
|
}
|
|
25
26
|
function attribution(message, launch) {
|
|
26
27
|
if (message.role === "toolResult") {
|
|
@@ -34,56 +35,100 @@ function attribution(message, launch) {
|
|
|
34
35
|
model: typeof model === "string" ? model : launch.model,
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
function identify(header, launch) {
|
|
39
|
+
if (header.type !== "session" || header.id !== launch.sessionId) {
|
|
40
|
+
throw new Error("Foreign native usage journal identity.");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** An unchanged or native v3 appended journal keeps its prefix; any other change rebuilds it. */
|
|
44
|
+
function reusable(cached, stat) {
|
|
45
|
+
return cached !== undefined &&
|
|
46
|
+
(journalStamp(cached.stat) === journalStamp(stat) ||
|
|
47
|
+
nativeAppend(cached.stat, stat, cached.header))
|
|
48
|
+
? cached
|
|
49
|
+
: { stat, end: 0, entries: [] };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Child usage of one physical native journal. A retained index parses only records after its last
|
|
53
|
+
* accepted LF boundary. Baselines, required usage and attribution apply to each read.
|
|
54
|
+
*/
|
|
55
|
+
export class UsageIndex {
|
|
56
|
+
prefix;
|
|
57
|
+
read(launch, baseline, terminal) {
|
|
58
|
+
const values = [];
|
|
59
|
+
const reachedTerminal = new Error("Native terminal boundary reached.");
|
|
60
|
+
const charge = (entry) => {
|
|
61
|
+
const billed = baseline.has(entry.id) ? undefined : entry.billing;
|
|
62
|
+
if (billed === "missing") {
|
|
63
|
+
throw new Error("Required native usage is unavailable.");
|
|
64
|
+
}
|
|
65
|
+
if (billed !== undefined) {
|
|
66
|
+
values.push({
|
|
67
|
+
id: `${launch.sessionId}:${entry.id}`,
|
|
68
|
+
...attribution(billed, launch),
|
|
69
|
+
usage: billed.usage,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (entry.id === terminal) {
|
|
73
|
+
throw reachedTerminal;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const cached = this.prefix;
|
|
77
|
+
// Only a completed read is retained.
|
|
78
|
+
this.prefix = undefined;
|
|
79
|
+
const fd = fs.openSync(launch.sessionFile, "r");
|
|
44
80
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (typeof value.id !== "string" || typeof value.type !== "string") {
|
|
54
|
-
throw new Error("Missing native usage entry identity.");
|
|
81
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
82
|
+
const prefix = reusable(cached, stat);
|
|
83
|
+
let ended = false;
|
|
84
|
+
try {
|
|
85
|
+
if (prefix.header !== undefined) {
|
|
86
|
+
identify(prefix.header, launch);
|
|
55
87
|
}
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
values.push({
|
|
59
|
-
id: `${launch.sessionId}:${value.id}`,
|
|
60
|
-
...attribution(message, launch),
|
|
61
|
-
usage: message.usage,
|
|
62
|
-
});
|
|
88
|
+
for (const entry of prefix.entries) {
|
|
89
|
+
charge(entry);
|
|
63
90
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
91
|
+
scanJournal(fd, nativeProjection, ({ value, end }) => {
|
|
92
|
+
if (prefix.header === undefined) {
|
|
93
|
+
identify(value, launch);
|
|
94
|
+
prefix.header = value;
|
|
95
|
+
prefix.end = end;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (typeof value.id !== "string" || typeof value.type !== "string") {
|
|
99
|
+
throw new Error("Missing native usage entry identity.");
|
|
100
|
+
}
|
|
101
|
+
const entry = { id: value.id, billing: billing(value) };
|
|
102
|
+
prefix.entries.push(entry);
|
|
103
|
+
prefix.end = end;
|
|
104
|
+
charge(entry);
|
|
105
|
+
}, { policy: "live", requireNewline: true, start: prefix.end });
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error !== reachedTerminal) {
|
|
109
|
+
throw error;
|
|
67
110
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw error;
|
|
111
|
+
ended = true;
|
|
112
|
+
}
|
|
113
|
+
if (journalStamp(fs.fstatSync(fd, { bigint: true })) !== journalStamp(stat)) {
|
|
114
|
+
throw new Error("Native usage journal changed while reading.");
|
|
73
115
|
}
|
|
116
|
+
if (prefix.header === undefined || (terminal !== undefined && !ended)) {
|
|
117
|
+
throw new Error("Native terminal or session identity is not LF-published.");
|
|
118
|
+
}
|
|
119
|
+
Assert(contributionsSchema, values);
|
|
120
|
+
prefix.stat = stat;
|
|
121
|
+
this.prefix = prefix;
|
|
122
|
+
return values;
|
|
74
123
|
}
|
|
75
|
-
|
|
76
|
-
|
|
124
|
+
finally {
|
|
125
|
+
fs.closeSync(fd);
|
|
77
126
|
}
|
|
78
127
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
throw new Error("Native terminal or session identity is not LF-published.");
|
|
84
|
-
}
|
|
85
|
-
Assert(contributionsSchema, values);
|
|
86
|
-
return values;
|
|
128
|
+
}
|
|
129
|
+
/** A fresh read for callers without an owner lifetime. */
|
|
130
|
+
export function childUsage(launch, baseline, terminal) {
|
|
131
|
+
return new UsageIndex().read(launch, baseline, terminal);
|
|
87
132
|
}
|
|
88
133
|
export function readCanonicalResult(base, launch) {
|
|
89
134
|
const file = path.join(runDirectory(base, launch.id), "result.json");
|
package/dist/build-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const EXTENSION_BUILD = Object.freeze({"version":"0.52.
|
|
1
|
+
export const EXTENSION_BUILD = Object.freeze({"version":"0.52.4","sha256":"d8c708fa06701cc0d5f8770cc729ba6fa204bdc8f76c5f06f5aa9cc3e37b3cc9"});
|
|
@@ -146,14 +146,25 @@ export class Mailbox {
|
|
|
146
146
|
}
|
|
147
147
|
ack(ids) {
|
|
148
148
|
const acked = [];
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
this.stored(() => {
|
|
150
|
+
// One FULL commit per ACK group; a rollback leaves every listed payload and intent pending.
|
|
151
|
+
this.db?.exec("BEGIN IMMEDIATE");
|
|
152
|
+
try {
|
|
153
|
+
for (const id of ids) {
|
|
154
|
+
const entry = this.get(id);
|
|
155
|
+
if (entry) {
|
|
156
|
+
this.notificationAccepted(id);
|
|
157
|
+
this.remove(id);
|
|
158
|
+
acked.push(id);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.db?.exec("COMMIT");
|
|
155
162
|
}
|
|
156
|
-
|
|
163
|
+
catch (error) {
|
|
164
|
+
this.db?.exec("ROLLBACK");
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
157
168
|
return { acked };
|
|
158
169
|
}
|
|
159
170
|
page(after, limit) {
|
|
@@ -63,9 +63,22 @@ export class Session {
|
|
|
63
63
|
}
|
|
64
64
|
touch() {
|
|
65
65
|
clearTimeout(this.lease);
|
|
66
|
-
this.
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
this.armLease(IDLE_TIMEOUT);
|
|
67
|
+
}
|
|
68
|
+
armLease(idle) {
|
|
69
|
+
const due = Math.max(1, Math.min(idle, this.expires - Date.now()));
|
|
70
|
+
this.lease = setTimeout(() => {
|
|
71
|
+
if (this.expires <= Date.now()) {
|
|
72
|
+
this.close(new Fault(403, "certificate_expired", "Client certificate expired."));
|
|
73
|
+
}
|
|
74
|
+
else if (due < idle) {
|
|
75
|
+
// Node can fire before Date.now() reaches the certificate deadline; keep the idle budget.
|
|
76
|
+
this.armLease(idle - due);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this.close(new Fault(410, "lease_expired", "Bridge session idle lease expired."));
|
|
80
|
+
}
|
|
81
|
+
}, due);
|
|
69
82
|
}
|
|
70
83
|
begin() {
|
|
71
84
|
this.guard();
|
|
@@ -110,22 +110,20 @@ export class WakeDeliveries {
|
|
|
110
110
|
}
|
|
111
111
|
pending(delivery) {
|
|
112
112
|
let after = 0;
|
|
113
|
-
let next;
|
|
114
113
|
let page;
|
|
115
114
|
do {
|
|
116
|
-
|
|
115
|
+
// Decode only the retired prefix and the oldest actionable entry, not the whole backlog.
|
|
116
|
+
page = delivery.mailbox.pendingNotifications(after, 1);
|
|
117
117
|
for (const entry of page.messages) {
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
delivery.mailbox.notificationAccepted(entry.message.id);
|
|
121
|
-
}
|
|
122
|
-
else if (!next) {
|
|
123
|
-
next = entry;
|
|
118
|
+
if (actionable(entry)) {
|
|
119
|
+
return entry;
|
|
124
120
|
}
|
|
121
|
+
// Retire only notification intent. Payload/reply obligations require actual ACK/reply.
|
|
122
|
+
delivery.mailbox.notificationAccepted(entry.message.id);
|
|
125
123
|
}
|
|
126
124
|
after = Number(page.nextCursor);
|
|
127
125
|
} while (page.hasMore);
|
|
128
|
-
return
|
|
126
|
+
return undefined;
|
|
129
127
|
}
|
|
130
128
|
groups() {
|
|
131
129
|
const groups = new Map();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isContextRequest } from "./lifecycle.js";
|
|
3
3
|
import { INBOUND_CHECKPOINT_TYPE, } from "./runtime-types.js";
|
|
4
|
-
import { isInboundCheckpoint, publicationMetadata, requestedDelivery } from "./inbound-record.js";
|
|
4
|
+
import { checkpointMessageId, isInboundCheckpoint, publicationMetadata, requestedDelivery, } from "./inbound-record.js";
|
|
5
5
|
import { isReceiptQuery, } from "./types.js";
|
|
6
6
|
import { inboundIdFromCustomMessage } from "./message-format.js";
|
|
7
7
|
import { isRecord } from "./validation.js";
|
|
@@ -40,6 +40,7 @@ export class InboundJournal {
|
|
|
40
40
|
receiptEntry(receipt, query, item) {
|
|
41
41
|
if (item.type === "custom" &&
|
|
42
42
|
item.customType === INBOUND_CHECKPOINT_TYPE &&
|
|
43
|
+
checkpointMessageId(item.data) === query.messageId &&
|
|
43
44
|
isInboundCheckpoint(item.data) &&
|
|
44
45
|
item.data.sessionId === query.rootSessionId) {
|
|
45
46
|
return this.receiptCheckpoint(receipt, query, item.id, item.data);
|
|
@@ -50,7 +51,7 @@ export class InboundJournal {
|
|
|
50
51
|
}
|
|
51
52
|
receiptCheckpoint(receipt, query, entryId, data) {
|
|
52
53
|
if ("entry" in data) {
|
|
53
|
-
if (receipt
|
|
54
|
+
if (receipt) {
|
|
54
55
|
return receipt;
|
|
55
56
|
}
|
|
56
57
|
const { from, message, stage, receivedAt } = data.entry;
|
|
@@ -74,7 +75,7 @@ export class InboundJournal {
|
|
|
74
75
|
discarded: false,
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
|
-
if (!receipt
|
|
78
|
+
if (!receipt) {
|
|
78
79
|
return receipt;
|
|
79
80
|
}
|
|
80
81
|
return {
|
|
@@ -82,3 +82,15 @@ export function isInboundCheckpoint(value) {
|
|
|
82
82
|
value.stage === "discarded" ||
|
|
83
83
|
value.stage === "reply-retired"));
|
|
84
84
|
}
|
|
85
|
+
/** Unvalidated: lets a receipt skip checkpoints for other messages before full validation. */
|
|
86
|
+
export function checkpointMessageId(value) {
|
|
87
|
+
if (!isRecord(value)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if ("entry" in value) {
|
|
91
|
+
return isRecord(value.entry) && isRecord(value.entry.message)
|
|
92
|
+
? value.entry.message.id
|
|
93
|
+
: undefined;
|
|
94
|
+
}
|
|
95
|
+
return value.messageId;
|
|
96
|
+
}
|
|
@@ -9,10 +9,19 @@ function localForkStartCommand() {
|
|
|
9
9
|
return `pi --name worker --extension ${shellQuote(path.join(PACKAGE_ROOT, "src", "index.ts"))}`;
|
|
10
10
|
}
|
|
11
11
|
function duplicateNames(sessions) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const duplicates = new Set();
|
|
14
|
+
for (const session of sessions) {
|
|
15
|
+
const name = session.name?.toLowerCase();
|
|
16
|
+
if (name === undefined || name === "") {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (seen.has(name)) {
|
|
20
|
+
duplicates.add(name);
|
|
21
|
+
}
|
|
22
|
+
seen.add(name);
|
|
23
|
+
}
|
|
24
|
+
return duplicates;
|
|
16
25
|
}
|
|
17
26
|
export function formatDuration(seconds) {
|
|
18
27
|
if (seconds < 60) {
|
|
@@ -74,7 +83,7 @@ function row(session, options) {
|
|
|
74
83
|
const name = session.name === undefined || session.name === "" ? "Unnamed session" : session.name;
|
|
75
84
|
const duplicate = session.name !== undefined &&
|
|
76
85
|
session.name !== "" &&
|
|
77
|
-
options.duplicates(session.name.toLowerCase());
|
|
86
|
+
options.duplicates.has(session.name.toLowerCase());
|
|
78
87
|
let location;
|
|
79
88
|
if (options.self) {
|
|
80
89
|
location = "self";
|
|
@@ -111,7 +120,7 @@ export function formatSessionListSections(allSessions, id, scope = "all") {
|
|
|
111
120
|
const others = scoped.filter((session) => session.id !== id);
|
|
112
121
|
const options = {
|
|
113
122
|
currentCwd: current.cwd,
|
|
114
|
-
duplicates:
|
|
123
|
+
duplicates: duplicateNames(allSessions),
|
|
115
124
|
allSessions,
|
|
116
125
|
now: Date.now(),
|
|
117
126
|
};
|