@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
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import { NativeJournal, type NativeRecord } from "../shared/native-journal.ts";
|
|
3
|
+
import { nativeProjection } from "../shared/journal-projections.ts";
|
|
4
|
+
import type { Projection } from "../shared/json-projection.ts";
|
|
5
|
+
import { isRecord, recordAt, type UnknownRecord } from "../shared/unknown.ts";
|
|
2
6
|
|
|
3
|
-
const READ_BYTES = 65536;
|
|
4
|
-
const LINE_BYTES = 32768;
|
|
5
|
-
const KEEP_LINES = 80;
|
|
6
|
-
|
|
7
|
-
function record(value: unknown): value is Record<string, unknown> {
|
|
8
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
-
}
|
|
10
7
|
export function plainText(value: string, length = 512): string {
|
|
11
|
-
// Journal content is untrusted terminal input, never terminal control sequences.
|
|
12
8
|
return Array.from(value.slice(0, length))
|
|
13
9
|
.map((char) => {
|
|
14
10
|
const code = char.codePointAt(0) ?? 0;
|
|
@@ -16,169 +12,161 @@ export function plainText(value: string, length = 512): string {
|
|
|
16
12
|
})
|
|
17
13
|
.join("");
|
|
18
14
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
const PAGE_CHARS = 32 * 1024;
|
|
16
|
+
const callFields = new Set(["type", "id", "name"]);
|
|
17
|
+
const textFields = new Set(["text", "thinking"]);
|
|
18
|
+
const metadata: Projection = (keys, root) => {
|
|
19
|
+
if (
|
|
20
|
+
keys[0] === "message" &&
|
|
21
|
+
keys[1] === "content" &&
|
|
22
|
+
keys.length > 3 &&
|
|
23
|
+
!callFields.has(String(keys[3]))
|
|
24
|
+
) {
|
|
25
|
+
return false;
|
|
22
26
|
}
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
return nativeProjection(keys, root);
|
|
28
|
+
};
|
|
29
|
+
function contentIndex(keys: readonly (string | number)[]): number | undefined {
|
|
30
|
+
if (keys[0] !== "message" || keys[1] !== "content") {
|
|
31
|
+
return undefined;
|
|
25
32
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
if (keys.length === 2) {
|
|
34
|
+
return -1;
|
|
35
|
+
}
|
|
36
|
+
return typeof keys[2] === "number" && textFields.has(String(keys[3])) ? keys[2] : undefined;
|
|
37
|
+
}
|
|
38
|
+
class TextPage {
|
|
39
|
+
private chars = 0;
|
|
40
|
+
private carry?: { index: number; text: string };
|
|
41
|
+
private readonly text = new Map<number, string>();
|
|
42
|
+
private readonly start: number;
|
|
43
|
+
constructor(page: number) {
|
|
44
|
+
this.start = page * PAGE_CHARS;
|
|
45
|
+
}
|
|
46
|
+
add(index: number, chunk: string): void {
|
|
47
|
+
const previous = this.carry;
|
|
48
|
+
this.carry = undefined;
|
|
49
|
+
if (previous && previous.index !== index) {
|
|
50
|
+
this.take(previous.index, previous.text);
|
|
32
51
|
}
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
const text = previous?.index === index ? previous.text + chunk : chunk;
|
|
53
|
+
const last = text.charCodeAt(text.length - 1);
|
|
54
|
+
const pending = last >= 0xd800 && last <= 0xdbff;
|
|
55
|
+
this.take(index, pending ? text.slice(0, -1) : text);
|
|
56
|
+
if (pending) {
|
|
57
|
+
this.carry = { index, text: text.slice(-1) };
|
|
35
58
|
}
|
|
36
|
-
return block.type === "thinking"
|
|
37
|
-
? []
|
|
38
|
-
: [`[${typeof block.type === "string" ? plainText(block.type, 40) : "non-text"} omitted]`];
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
function activity(value: unknown): string[] {
|
|
42
|
-
if (!record(value) || value.type !== "message" || !record(value.message)) {
|
|
43
|
-
return [];
|
|
44
59
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
private take(index: number, text: string): void {
|
|
61
|
+
const points = Array.from(text);
|
|
62
|
+
const from = Math.max(0, this.start - this.chars);
|
|
63
|
+
const to = Math.min(points.length, this.start + PAGE_CHARS - this.chars);
|
|
64
|
+
if (to > from) {
|
|
65
|
+
this.text.set(index, (this.text.get(index) ?? "") + points.slice(from, to).join(""));
|
|
50
66
|
}
|
|
51
|
-
|
|
67
|
+
this.chars += points.length;
|
|
52
68
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
finish(value: UnknownRecord, page: number): ConversationPage {
|
|
70
|
+
if (this.carry) {
|
|
71
|
+
this.take(this.carry.index, this.carry.text);
|
|
72
|
+
}
|
|
73
|
+
const message = { ...recordAt(value, "message") };
|
|
74
|
+
if (typeof message.content === "string") {
|
|
75
|
+
message.content = this.text.get(-1) ?? "";
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(message.content)) {
|
|
78
|
+
for (const [index, block] of (message.content as unknown[]).entries()) {
|
|
79
|
+
if (isRecord(block) && typeof block.type === "string" && textFields.has(block.type)) {
|
|
80
|
+
block[block.type] = this.text.get(index) ?? "";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { message, page, pages: Math.max(1, Math.ceil(this.chars / PAGE_CHARS)) };
|
|
57
85
|
}
|
|
58
|
-
|
|
86
|
+
}
|
|
87
|
+
export interface ConversationPage {
|
|
88
|
+
message: UnknownRecord;
|
|
89
|
+
page: number;
|
|
90
|
+
pages: number;
|
|
59
91
|
}
|
|
60
92
|
|
|
61
|
-
/**
|
|
62
|
-
export class
|
|
63
|
-
readonly
|
|
64
|
-
|
|
65
|
-
private offset = 0;
|
|
66
|
-
private partial: Buffer = Buffer.alloc(0);
|
|
67
|
-
private dropping = false;
|
|
68
|
-
private identity = "";
|
|
69
|
-
private stamp = 0;
|
|
70
|
-
private readonly file: string;
|
|
71
|
-
private readonly sessionId: string;
|
|
93
|
+
/** Native indexing owns ancestry and publication; only requested message pages are hydrated. */
|
|
94
|
+
export class ConversationJournal {
|
|
95
|
+
readonly native: NativeJournal;
|
|
96
|
+
private branch?: { length: number; leaf: string | null | undefined; entries: NativeRecord[] };
|
|
72
97
|
constructor(file: string, sessionId: string) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
private reset(size: number): void {
|
|
77
|
-
this.lines.length = 0;
|
|
78
|
-
this.offset = 0;
|
|
79
|
-
this.partial = Buffer.alloc(0);
|
|
80
|
-
this.dropping = false;
|
|
81
|
-
if (size > READ_BYTES) {
|
|
82
|
-
this.skip(size);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
private skip(size: number): void {
|
|
86
|
-
this.offset = size - READ_BYTES;
|
|
87
|
-
this.partial = Buffer.alloc(0);
|
|
88
|
-
this.dropping = true;
|
|
89
|
-
this.lines.push("[Earlier native activity omitted; full journal retained.]");
|
|
90
|
-
}
|
|
91
|
-
private header(fd: number): void {
|
|
92
|
-
const head = Buffer.alloc(4096);
|
|
93
|
-
const count = fs.readSync(fd, head, 0, head.length, 0);
|
|
94
|
-
const lf = head.indexOf(10, 0);
|
|
95
|
-
if (lf < 0 || lf >= count) {
|
|
96
|
-
throw new Error("Native journal header is incomplete or too large.");
|
|
98
|
+
const stat = fs.lstatSync(file);
|
|
99
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
100
|
+
throw new Error("Child journal is not a regular file.");
|
|
97
101
|
}
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
this.native = new NativeJournal(file, "live", true, {
|
|
103
|
+
readBytes: 256 * 1024,
|
|
104
|
+
select: metadata,
|
|
105
|
+
});
|
|
106
|
+
if (this.native.records[0]?.value.id !== sessionId) {
|
|
100
107
|
throw new Error("Native journal identity does not match this child.");
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.lines.push("[Malformed published native entry omitted.]");
|
|
110
|
+
entries(requested?: string | null): NativeRecord[] {
|
|
111
|
+
const leaf =
|
|
112
|
+
typeof requested === "string" && !this.native.byId.has(requested) ? undefined : requested;
|
|
113
|
+
if (this.branch?.length !== this.native.records.length || this.branch.leaf !== leaf) {
|
|
114
|
+
this.branch = {
|
|
115
|
+
length: this.native.records.length,
|
|
116
|
+
leaf,
|
|
117
|
+
entries: this.native.branch(leaf).filter((entry) => entry.value.type === "message"),
|
|
118
|
+
};
|
|
113
119
|
}
|
|
120
|
+
return this.branch.entries;
|
|
114
121
|
}
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
122
|
+
async call(id: string, result: string, signal?: AbortSignal): Promise<UnknownRecord | undefined> {
|
|
123
|
+
const contains = (entry: NativeRecord) => {
|
|
124
|
+
const content = recordAt(entry.value, "message")?.content;
|
|
125
|
+
return (
|
|
126
|
+
Array.isArray(content) &&
|
|
127
|
+
content.some(
|
|
128
|
+
(part: unknown) => isRecord(part) && part.type === "toolCall" && part.id === id,
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
const entries = this.branch?.entries ?? [];
|
|
133
|
+
const entry = entries
|
|
134
|
+
.slice(
|
|
135
|
+
0,
|
|
136
|
+
entries.findIndex((item) => item.value.id === result),
|
|
137
|
+
)
|
|
138
|
+
.findLast(contains);
|
|
139
|
+
const content = entry && (await this.page(entry, 0, { signal })).message.content;
|
|
140
|
+
return Array.isArray(content)
|
|
141
|
+
? content.find(
|
|
142
|
+
(part: unknown): part is UnknownRecord =>
|
|
143
|
+
isRecord(part) && part.type === "toolCall" && part.id === id,
|
|
144
|
+
)
|
|
145
|
+
: undefined;
|
|
137
146
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
async page(
|
|
148
|
+
entry: NativeRecord,
|
|
149
|
+
page = 0,
|
|
150
|
+
options: { signal?: AbortSignal; hideThinking?: boolean } = {},
|
|
151
|
+
): Promise<ConversationPage> {
|
|
152
|
+
const text = new TextPage(page);
|
|
153
|
+
const value = await this.native.project(
|
|
154
|
+
entry,
|
|
155
|
+
(keys, root) => {
|
|
156
|
+
if (contentIndex(keys) !== undefined && keys.length > 2) {
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
return nativeProjection(keys, root);
|
|
160
|
+
},
|
|
161
|
+
(keys, chunk) => {
|
|
162
|
+
const index = contentIndex(keys);
|
|
163
|
+
if (index === undefined || (options.hideThinking === true && keys[3] === "thinking")) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
text.add(index, chunk);
|
|
167
|
+
},
|
|
168
|
+
options.signal,
|
|
143
169
|
);
|
|
144
|
-
|
|
145
|
-
private observe(fd: number, stat: fs.Stats): void {
|
|
146
|
-
const identity = `${stat.dev}:${stat.ino}`;
|
|
147
|
-
if (this.changed(stat, identity)) {
|
|
148
|
-
this.reset(stat.size);
|
|
149
|
-
} else if (stat.size - this.offset > READ_BYTES) {
|
|
150
|
-
this.skip(stat.size);
|
|
151
|
-
}
|
|
152
|
-
this.identity = identity;
|
|
153
|
-
this.stamp = stat.mtimeMs;
|
|
154
|
-
const chunk = Buffer.alloc(Math.min(READ_BYTES, stat.size - this.offset));
|
|
155
|
-
const count = fs.readSync(fd, chunk, 0, chunk.length, this.offset);
|
|
156
|
-
this.offset += count;
|
|
157
|
-
this.notice =
|
|
158
|
-
this.offset > READ_BYTES ? "Recent journal tail only; full journal retained." : "";
|
|
159
|
-
this.consume(chunk.subarray(0, count));
|
|
160
|
-
if (this.lines.length === 0 && this.notice.length === 0) {
|
|
161
|
-
this.notice = "Waiting for published native activity.";
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
poll(): void {
|
|
165
|
-
let fd: number | undefined;
|
|
166
|
-
try {
|
|
167
|
-
fd = fs.openSync(this.file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
168
|
-
const stat = fs.fstatSync(fd);
|
|
169
|
-
if (!stat.isFile()) {
|
|
170
|
-
throw new Error("Native journal is not a regular file.");
|
|
171
|
-
}
|
|
172
|
-
this.header(fd);
|
|
173
|
-
this.observe(fd, stat);
|
|
174
|
-
} catch (error) {
|
|
175
|
-
this.lines.length = 0;
|
|
176
|
-
this.identity = "";
|
|
177
|
-
this.notice = error instanceof Error ? plainText(error.message) : "Journal unavailable.";
|
|
178
|
-
} finally {
|
|
179
|
-
if (fd !== undefined) {
|
|
180
|
-
fs.closeSync(fd);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
170
|
+
return text.finish(value, page);
|
|
183
171
|
}
|
|
184
172
|
}
|
package/src/agent/launch.ts
CHANGED
|
@@ -3,16 +3,22 @@ import * as path from "node:path";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
6
|
-
import type { Launch
|
|
6
|
+
import type { Launch } from "./contracts.ts";
|
|
7
|
+
import type { DelegateInput } from "./tool-schemas.ts";
|
|
7
8
|
import { loadSDK, packageRoot, runtimeEntry } from "./host.ts";
|
|
8
9
|
import { runDirectory, write } from "./storage.ts";
|
|
9
10
|
|
|
10
|
-
function selectedModel(
|
|
11
|
+
function selectedModel(
|
|
12
|
+
input: DelegateInput,
|
|
13
|
+
context: ExtensionContext,
|
|
14
|
+
thinking: ThinkingLevel,
|
|
15
|
+
sdk: Awaited<ReturnType<typeof loadSDK>>,
|
|
16
|
+
) {
|
|
11
17
|
if (input.model === undefined) {
|
|
12
18
|
if (!context.model) {
|
|
13
19
|
throw new Error("No selected model; no fallback was used.");
|
|
14
20
|
}
|
|
15
|
-
return context.model;
|
|
21
|
+
return { model: context.model, thinking };
|
|
16
22
|
}
|
|
17
23
|
const slash = input.model.indexOf("/");
|
|
18
24
|
if (slash < 1) {
|
|
@@ -22,10 +28,28 @@ function selectedModel(input: DelegateInput, context: ExtensionContext) {
|
|
|
22
28
|
input.model.slice(0, slash),
|
|
23
29
|
input.model.slice(slash + 1),
|
|
24
30
|
);
|
|
25
|
-
if (
|
|
31
|
+
if (model) {
|
|
32
|
+
return { model, thinking };
|
|
33
|
+
}
|
|
34
|
+
if (typeof sdk.resolveModelReference !== "function") {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Model ${input.model} is unavailable; no fallback was used. Check the provider/model ID. This host supports literal IDs only. If you intended a thinking suffix, update Pi for native thinking-suffix support.`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const resolved = sdk.resolveModelReference(
|
|
40
|
+
{
|
|
41
|
+
text: input.model,
|
|
42
|
+
provider: input.model.slice(0, slash),
|
|
43
|
+
thinkingSuffix: "strict",
|
|
44
|
+
fuzzy: false,
|
|
45
|
+
},
|
|
46
|
+
context.modelRegistry.getAll(),
|
|
47
|
+
() => true,
|
|
48
|
+
);
|
|
49
|
+
if (resolved.status !== "resolved") {
|
|
26
50
|
throw new Error(`Model ${input.model} is unavailable; no fallback was used.`);
|
|
27
51
|
}
|
|
28
|
-
return model;
|
|
52
|
+
return { model: resolved.model, thinking: resolved.thinkingLevel ?? thinking };
|
|
29
53
|
}
|
|
30
54
|
function targetTrust(
|
|
31
55
|
sdk: Awaited<ReturnType<typeof loadSDK>>,
|
|
@@ -63,7 +87,7 @@ export async function prepareLaunch(options: {
|
|
|
63
87
|
if (!fs.statSync(cwd).isDirectory()) {
|
|
64
88
|
throw new Error("Delegation cwd is not a directory.");
|
|
65
89
|
}
|
|
66
|
-
const
|
|
90
|
+
const selection = selectedModel(input, context, thinking, sdk);
|
|
67
91
|
const trusted = targetTrust(sdk, cwd, context, agentDir);
|
|
68
92
|
const id = randomUUID();
|
|
69
93
|
const directory = runDirectory(base, id);
|
|
@@ -98,9 +122,9 @@ export async function prepareLaunch(options: {
|
|
|
98
122
|
sessionId: manager.getSessionId(),
|
|
99
123
|
prompt: input.prompt,
|
|
100
124
|
context: input.context ?? "fresh",
|
|
101
|
-
provider: model.provider,
|
|
102
|
-
model: model.id,
|
|
103
|
-
thinking,
|
|
125
|
+
provider: selection.model.provider,
|
|
126
|
+
model: selection.model.id,
|
|
127
|
+
thinking: selection.thinking,
|
|
104
128
|
trusted,
|
|
105
129
|
name: input.name,
|
|
106
130
|
createdAt: Date.now(),
|
package/src/agent/legacy.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { journalStamp } from "../shared/native-journal.ts";
|
|
5
5
|
import { receiptId } from "./receipts.ts";
|
|
6
6
|
import { resolveTempRootDir } from "../shared/types.ts";
|
|
7
7
|
import { isRecord } from "../shared/unknown.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { release, isHeld, type Completion } from "./notifications.ts";
|
|
9
9
|
import { missing } from "./storage.ts";
|
|
10
|
+
import { errorText } from "./host.ts";
|
|
10
11
|
|
|
11
12
|
interface LegacyInput {
|
|
12
13
|
file: string;
|
|
@@ -17,29 +18,53 @@ interface LegacyInput {
|
|
|
17
18
|
owner: string;
|
|
18
19
|
single: boolean;
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
+
interface IgnoredInputs {
|
|
22
|
+
readonly owner: string;
|
|
23
|
+
readonly directory: string;
|
|
24
|
+
readonly ignored: Map<string, string>;
|
|
25
|
+
}
|
|
26
|
+
interface LegacyScan extends IgnoredInputs {
|
|
27
|
+
readonly known: ReadonlyMap<string, string> | undefined;
|
|
28
|
+
}
|
|
29
|
+
// Retain only foreign classifications, never their saved output, for each live native manager.
|
|
30
|
+
const ignoredInputs = new WeakMap<ExtensionContext["sessionManager"], IgnoredInputs>();
|
|
31
|
+
function fileStamp(file: string): string | undefined {
|
|
21
32
|
const stat = fs.lstatSync(file, { bigint: true });
|
|
22
|
-
|
|
33
|
+
return stat.isFile() && !stat.isSymbolicLink() ? journalStamp(stat) : undefined;
|
|
34
|
+
}
|
|
35
|
+
function legacyInput(file: string, scan: LegacyScan): LegacyInput | undefined {
|
|
36
|
+
const stamp = fileStamp(file);
|
|
37
|
+
if (stamp === undefined) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (scan.known?.get(file) === stamp) {
|
|
41
|
+
scan.ignored.set(file, stamp);
|
|
23
42
|
return;
|
|
24
43
|
}
|
|
25
44
|
const raw = fs.readFileSync(file, "utf8");
|
|
26
45
|
const data: unknown = JSON.parse(raw);
|
|
27
|
-
if (!isRecord(data) || data.sessionId !== owner) {
|
|
46
|
+
if (!isRecord(data) || data.sessionId !== scan.owner) {
|
|
47
|
+
if (fileStamp(file) === stamp) {
|
|
48
|
+
scan.ignored.set(file, stamp);
|
|
49
|
+
}
|
|
28
50
|
return;
|
|
29
51
|
}
|
|
52
|
+
return ownedInput({ file, raw, stamp, owner: scan.owner }, data);
|
|
53
|
+
}
|
|
54
|
+
function ownedInput(
|
|
55
|
+
source: Pick<LegacyInput, "file" | "raw" | "stamp" | "owner">,
|
|
56
|
+
data: Record<string, unknown>,
|
|
57
|
+
): LegacyInput | undefined {
|
|
30
58
|
const id = typeof data.runId === "string" ? data.runId : data.id;
|
|
31
|
-
if (typeof id !== "string" || path.basename(file, ".json") !== id) {
|
|
59
|
+
if (typeof id !== "string" || path.basename(source.file, ".json") !== id) {
|
|
32
60
|
return;
|
|
33
61
|
}
|
|
34
62
|
const legacy = `legacy:${id}:${typeof data.timestamp === "number" ? data.timestamp : "unknown"}`;
|
|
35
63
|
const completionId = typeof data.completionId === "string" ? data.completionId : legacy;
|
|
36
64
|
return {
|
|
37
|
-
|
|
38
|
-
raw,
|
|
39
|
-
stamp: journalStamp(stat),
|
|
65
|
+
...source,
|
|
40
66
|
id,
|
|
41
67
|
key: `completion:${completionId}`,
|
|
42
|
-
owner,
|
|
43
68
|
single: data.mode === "single",
|
|
44
69
|
};
|
|
45
70
|
}
|
|
@@ -104,7 +129,7 @@ function published(ctx: ExtensionContext, input: LegacyInput): boolean {
|
|
|
104
129
|
undefined
|
|
105
130
|
);
|
|
106
131
|
}
|
|
107
|
-
function
|
|
132
|
+
function notification(ctx: ExtensionContext, input: LegacyInput): Completion | undefined {
|
|
108
133
|
const identity = `legacy:${input.id}:${input.key}`;
|
|
109
134
|
if (published(ctx, input)) {
|
|
110
135
|
release(input.owner, identity);
|
|
@@ -120,26 +145,54 @@ function deliver(pi: ExtensionAPI, ctx: ExtensionContext, input: LegacyInput): v
|
|
|
120
145
|
if (isHeld(input.owner, identity)) {
|
|
121
146
|
return;
|
|
122
147
|
}
|
|
123
|
-
|
|
148
|
+
return {
|
|
149
|
+
owner: input.owner,
|
|
150
|
+
id: identity,
|
|
151
|
+
message: {
|
|
152
|
+
customType: "subagent-notify",
|
|
153
|
+
display: true,
|
|
154
|
+
// Full saved bytes, not a replacement summary. No owner/run record is invented.
|
|
155
|
+
content: input.raw,
|
|
156
|
+
details: { completion: { runId: input.id, key: input.key, ownerSessionId: input.owner } },
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function legacyNotice(
|
|
161
|
+
ctx: ExtensionContext,
|
|
162
|
+
file: string,
|
|
163
|
+
scan: LegacyScan,
|
|
164
|
+
): Completion | undefined {
|
|
124
165
|
try {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
customType: "subagent-notify",
|
|
128
|
-
display: true,
|
|
129
|
-
// Full saved bytes, not a replacement summary. No owner/run record is invented.
|
|
130
|
-
content: input.raw,
|
|
131
|
-
details: { completion: { runId: input.id, key: input.key, ownerSessionId: input.owner } },
|
|
132
|
-
},
|
|
133
|
-
{ triggerTurn: true, deliverAs: "followUp" },
|
|
134
|
-
);
|
|
166
|
+
const input = legacyInput(file, scan);
|
|
167
|
+
return input ? notification(ctx, input) : undefined;
|
|
135
168
|
} catch (error) {
|
|
136
|
-
|
|
137
|
-
|
|
169
|
+
console.error(`Skipped legacy completion ${file}: ${errorText(error)}`);
|
|
170
|
+
return;
|
|
138
171
|
}
|
|
139
172
|
}
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const
|
|
173
|
+
function startScan(ctx: ExtensionContext, directory: string): LegacyScan {
|
|
174
|
+
const manager = ctx.sessionManager;
|
|
175
|
+
const owner = manager.getSessionId();
|
|
176
|
+
const prior = ignoredInputs.get(manager);
|
|
177
|
+
const scan: LegacyScan = {
|
|
178
|
+
directory,
|
|
179
|
+
owner,
|
|
180
|
+
known: prior?.directory === directory && prior.owner === owner ? prior.ignored : undefined,
|
|
181
|
+
ignored: new Map(),
|
|
182
|
+
};
|
|
183
|
+
// Replace even on a failed directory scan; vanished/unsafe inputs keep no cached decision.
|
|
184
|
+
ignoredInputs.set(manager, { directory, owner, ignored: scan.ignored });
|
|
185
|
+
return scan;
|
|
186
|
+
}
|
|
187
|
+
/** Retired releases created this directory at session start, before any of their runners. */
|
|
188
|
+
export function legacyDirectory(): string {
|
|
189
|
+
return path.join(resolveTempRootDir(), "async-subagent-results");
|
|
190
|
+
}
|
|
191
|
+
export function legacyNotifications(ctx: ExtensionContext): Completion[] {
|
|
192
|
+
const notices: Completion[] = [];
|
|
193
|
+
const directory = legacyDirectory();
|
|
194
|
+
const root = path.dirname(directory);
|
|
195
|
+
const scan = startScan(ctx, directory);
|
|
143
196
|
let names: string[];
|
|
144
197
|
try {
|
|
145
198
|
for (const candidate of [root, directory]) {
|
|
@@ -151,7 +204,7 @@ export function notifyLegacy(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
|
151
204
|
names = fs.readdirSync(directory);
|
|
152
205
|
} catch (error) {
|
|
153
206
|
if (missing(error)) {
|
|
154
|
-
return;
|
|
207
|
+
return notices;
|
|
155
208
|
}
|
|
156
209
|
throw error;
|
|
157
210
|
}
|
|
@@ -159,9 +212,10 @@ export function notifyLegacy(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
|
159
212
|
if (!name.endsWith(".json")) {
|
|
160
213
|
continue;
|
|
161
214
|
}
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
164
|
-
|
|
215
|
+
const notice = legacyNotice(ctx, path.join(directory, name), scan);
|
|
216
|
+
if (notice) {
|
|
217
|
+
notices.push(notice);
|
|
165
218
|
}
|
|
166
219
|
}
|
|
220
|
+
return notices;
|
|
167
221
|
}
|