@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,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { JournalFrames, type JournalPolicy, type JournalRecord } from "./journal-frames.ts";
|
|
3
|
+
import type { Projection, StringChunk } from "./json-projection.ts";
|
|
3
4
|
import { nativeProjection } from "./journal-projections.ts";
|
|
4
|
-
import { isRecord,
|
|
5
|
+
import { isRecord, type UnknownRecord } from "./unknown.ts";
|
|
5
6
|
|
|
6
7
|
/** A selected journal entry is not a complete SDK SessionEntry or Message. */
|
|
7
8
|
export interface NativePreview extends UnknownRecord {
|
|
@@ -33,77 +34,169 @@ function preview(
|
|
|
33
34
|
};
|
|
34
35
|
return { value, start: record.start, end: record.end };
|
|
35
36
|
}
|
|
36
|
-
function configurationModel(value: NativePreview): string | undefined {
|
|
37
|
-
const message = recordAt(value, "message");
|
|
38
|
-
const provider = value.type === "model_change" ? value.provider : message?.provider;
|
|
39
|
-
const model = value.type === "model_change" ? value.modelId : message?.model;
|
|
40
|
-
return typeof provider === "string" && typeof model === "string"
|
|
41
|
-
? `${provider}/${model}`
|
|
42
|
-
: undefined;
|
|
43
|
-
}
|
|
44
37
|
export class NativeJournal {
|
|
45
38
|
readonly records: NativeRecord[] = [];
|
|
46
39
|
readonly byId = new Map<string, NativeRecord>();
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
stamp: string;
|
|
41
|
+
end: number;
|
|
49
42
|
readonly file: string;
|
|
50
|
-
|
|
43
|
+
private readonly identity: string;
|
|
44
|
+
private offset = 0;
|
|
45
|
+
private readonly frames: JournalFrames;
|
|
46
|
+
constructor(
|
|
47
|
+
file: string,
|
|
48
|
+
policy: JournalPolicy = "inspect",
|
|
49
|
+
requireNewline = policy === "live",
|
|
50
|
+
options: { readBytes?: number; select?: Projection } = {},
|
|
51
|
+
) {
|
|
51
52
|
this.file = file;
|
|
52
|
-
|
|
53
|
+
let parent: string | null = null;
|
|
54
|
+
this.frames = new JournalFrames(
|
|
55
|
+
options.select ?? nativeProjection,
|
|
56
|
+
(raw) => {
|
|
57
|
+
const record = preview(raw, parent, this.records[0]?.value.version === 1);
|
|
58
|
+
if (!record) {
|
|
59
|
+
if (policy !== "inspect") {
|
|
60
|
+
throw new SyntaxError(`Native record at byte ${raw.start} requires a type`);
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (record.value.type !== "session") {
|
|
65
|
+
parent = record.value.id;
|
|
66
|
+
}
|
|
67
|
+
this.records.push(record);
|
|
68
|
+
this.byId.set(record.value.id, record);
|
|
69
|
+
},
|
|
70
|
+
{ policy, requireNewline },
|
|
71
|
+
);
|
|
72
|
+
const fd = fs.openSync(
|
|
73
|
+
file,
|
|
74
|
+
fs.constants.O_RDONLY | (policy === "live" ? fs.constants.O_NOFOLLOW : 0),
|
|
75
|
+
);
|
|
53
76
|
try {
|
|
54
77
|
const stat = fs.fstatSync(fd, { bigint: true });
|
|
55
78
|
this.stamp = journalStamp(stat);
|
|
79
|
+
this.identity = `${stat.dev}:${stat.ino}`;
|
|
56
80
|
this.end = Number(stat.size);
|
|
57
|
-
|
|
58
|
-
|
|
81
|
+
this.index(fd, Math.min(this.end, options.readBytes ?? Infinity));
|
|
82
|
+
this.checkInitial(stat, fs.fstatSync(fd, { bigint: true }), policy);
|
|
83
|
+
} finally {
|
|
84
|
+
fs.closeSync(fd);
|
|
85
|
+
}
|
|
86
|
+
if (this.records[0]?.value.type !== "session") {
|
|
87
|
+
throw new Error("Not a readable native Pi session.");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
private checkInitial(before: fs.BigIntStats, after: fs.BigIntStats, policy: JournalPolicy): void {
|
|
91
|
+
if (
|
|
92
|
+
journalStamp(after) !== this.stamp &&
|
|
93
|
+
(policy !== "live" ||
|
|
94
|
+
after.size < before.size ||
|
|
95
|
+
`${after.dev}:${after.ino}` !== this.identity)
|
|
96
|
+
) {
|
|
97
|
+
throw new Error("Journal changed while indexing; refresh history.");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
private index(fd: number, end: number): void {
|
|
101
|
+
const buffer = Buffer.allocUnsafe(65536);
|
|
102
|
+
while (this.offset < end) {
|
|
103
|
+
const count = fs.readSync(
|
|
59
104
|
fd,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (policy !== "inspect") {
|
|
65
|
-
throw new SyntaxError(`Native record at byte ${raw.start} requires a type`);
|
|
66
|
-
}
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (record.value.type !== "session") {
|
|
70
|
-
parent = record.value.id;
|
|
71
|
-
}
|
|
72
|
-
this.records.push(record);
|
|
73
|
-
this.byId.set(record.value.id, record);
|
|
74
|
-
},
|
|
75
|
-
{ policy, requireNewline, end: this.end },
|
|
105
|
+
buffer,
|
|
106
|
+
0,
|
|
107
|
+
Math.min(buffer.length, end - this.offset),
|
|
108
|
+
this.offset,
|
|
76
109
|
);
|
|
77
|
-
if (
|
|
78
|
-
throw new Error("Journal
|
|
110
|
+
if (count === 0) {
|
|
111
|
+
throw new Error("Journal truncated while reading");
|
|
112
|
+
}
|
|
113
|
+
this.frames.write(buffer.subarray(0, count));
|
|
114
|
+
this.offset += count;
|
|
115
|
+
}
|
|
116
|
+
this.frames.finish();
|
|
117
|
+
}
|
|
118
|
+
get indexed(): boolean {
|
|
119
|
+
return this.offset === this.end;
|
|
120
|
+
}
|
|
121
|
+
/** Incremental, LF-published indexing. Append work is budgeted independently of paint. */
|
|
122
|
+
refresh(bytes = 256 * 1024): boolean {
|
|
123
|
+
const fd = fs.openSync(this.file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
124
|
+
try {
|
|
125
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
126
|
+
if (`${stat.dev}:${stat.ino}` !== this.identity || Number(stat.size) < this.offset) {
|
|
127
|
+
throw new Error("Native journal was replaced or truncated; reopen this conversation.");
|
|
128
|
+
}
|
|
129
|
+
if (journalStamp(stat) === this.stamp && this.indexed) {
|
|
130
|
+
return false;
|
|
79
131
|
}
|
|
132
|
+
const previous = this.records.length;
|
|
133
|
+
this.end = Number(stat.size);
|
|
134
|
+
this.index(fd, Math.min(this.end, this.offset + bytes));
|
|
135
|
+
this.stamp = journalStamp(stat);
|
|
136
|
+
return previous !== this.records.length;
|
|
80
137
|
} finally {
|
|
81
138
|
fs.closeSync(fd);
|
|
82
139
|
}
|
|
83
|
-
|
|
84
|
-
|
|
140
|
+
}
|
|
141
|
+
/** Project one immutable, published record, including bounded slices of very large strings. */
|
|
142
|
+
async project(
|
|
143
|
+
record: NativeRecord,
|
|
144
|
+
select: Projection,
|
|
145
|
+
stringChunk?: StringChunk,
|
|
146
|
+
signal?: AbortSignal,
|
|
147
|
+
): Promise<UnknownRecord> {
|
|
148
|
+
const fd = fs.openSync(this.file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
149
|
+
let stream: fs.ReadStream | undefined;
|
|
150
|
+
try {
|
|
151
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
152
|
+
if (`${stat.dev}:${stat.ino}` !== this.identity || Number(stat.size) < record.end) {
|
|
153
|
+
throw new Error("Native journal was replaced or truncated.");
|
|
154
|
+
}
|
|
155
|
+
let value: UnknownRecord | undefined;
|
|
156
|
+
const frames = new JournalFrames(
|
|
157
|
+
select,
|
|
158
|
+
(entry) => {
|
|
159
|
+
value = entry.value;
|
|
160
|
+
},
|
|
161
|
+
{ policy: "live", start: record.start, stringChunk },
|
|
162
|
+
);
|
|
163
|
+
stream = fs.createReadStream(this.file, {
|
|
164
|
+
fd,
|
|
165
|
+
start: record.start,
|
|
166
|
+
end: record.end - 1,
|
|
167
|
+
highWaterMark: 65536,
|
|
168
|
+
signal,
|
|
169
|
+
});
|
|
170
|
+
for await (const chunk of stream) {
|
|
171
|
+
if (!Buffer.isBuffer(chunk)) {
|
|
172
|
+
throw new TypeError("Native journal reads require bytes.");
|
|
173
|
+
}
|
|
174
|
+
frames.write(chunk);
|
|
175
|
+
}
|
|
176
|
+
frames.finish();
|
|
177
|
+
if (!value) {
|
|
178
|
+
throw new Error("Native entry is not published.");
|
|
179
|
+
}
|
|
180
|
+
return value;
|
|
181
|
+
} finally {
|
|
182
|
+
if (stream) {
|
|
183
|
+
stream.destroy();
|
|
184
|
+
} else {
|
|
185
|
+
fs.closeSync(fd);
|
|
186
|
+
}
|
|
85
187
|
}
|
|
86
188
|
}
|
|
87
|
-
branch(leaf?: string | null
|
|
189
|
+
branch(leaf?: string | null): NativeRecord[] {
|
|
88
190
|
if (leaf === null) {
|
|
89
191
|
return [];
|
|
90
192
|
}
|
|
91
|
-
const eligible =
|
|
92
|
-
endedAt === undefined
|
|
93
|
-
? this.records
|
|
94
|
-
: this.records.filter(
|
|
95
|
-
(record) =>
|
|
96
|
-
record.value.type === "session" ||
|
|
97
|
-
Date.parse(record.value.timestamp ?? "") <= endedAt,
|
|
98
|
-
);
|
|
99
193
|
let current =
|
|
100
194
|
leaf !== undefined && leaf !== ""
|
|
101
195
|
? this.byId.get(leaf)
|
|
102
|
-
:
|
|
196
|
+
: this.records.findLast((record) => record.value.type !== "session");
|
|
103
197
|
const path: NativeRecord[] = [];
|
|
104
198
|
const seen = new Set<string>();
|
|
105
|
-
|
|
106
|
-
while (current && current.value.type !== "session" && allowed.has(current.value.id)) {
|
|
199
|
+
while (current && current.value.type !== "session") {
|
|
107
200
|
if (seen.has(current.value.id)) {
|
|
108
201
|
throw new Error("Cyclic journal ancestry");
|
|
109
202
|
}
|
|
@@ -139,33 +232,22 @@ export class NativeJournal {
|
|
|
139
232
|
fs.closeSync(fd);
|
|
140
233
|
}
|
|
141
234
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
(value.type === "message" && recordAt(value, "message")?.role === "assistant")
|
|
159
|
-
) {
|
|
160
|
-
model = value;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (model) {
|
|
164
|
-
result.model = configurationModel(model);
|
|
165
|
-
result.modelRecordedAt = Date.parse(model.timestamp ?? "");
|
|
166
|
-
}
|
|
167
|
-
return result;
|
|
168
|
-
}
|
|
235
|
+
}
|
|
236
|
+
/** Native v3 writers append after attachment; loading older formats can rewrite in place.
|
|
237
|
+
* Outside prefix edits followed by same-inode growth are not an append-only operation.
|
|
238
|
+
*/
|
|
239
|
+
export function nativeAppend(
|
|
240
|
+
before: fs.BigIntStats,
|
|
241
|
+
after: fs.BigIntStats,
|
|
242
|
+
header: UnknownRecord | undefined,
|
|
243
|
+
): boolean {
|
|
244
|
+
return (
|
|
245
|
+
header?.type === "session" &&
|
|
246
|
+
header.version === 3 &&
|
|
247
|
+
before.dev === after.dev &&
|
|
248
|
+
before.ino === after.ino &&
|
|
249
|
+
after.size > before.size
|
|
250
|
+
);
|
|
169
251
|
}
|
|
170
252
|
export function journalStamp(stat: fs.BigIntStats): string {
|
|
171
253
|
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NativeHistory } from "../pi-intercom/last-message.ts";
|
|
2
|
+
import { isRecord } from "./unknown.ts";
|
|
3
|
+
|
|
4
|
+
/** A child actor asks its intercom to fence broker input before parking, or to release it. */
|
|
5
|
+
export const PARK = "pi-subagents:park";
|
|
6
|
+
export interface ParkRequest {
|
|
7
|
+
readonly park: boolean;
|
|
8
|
+
readonly reply: (parked: Promise<boolean>) => void;
|
|
9
|
+
}
|
|
10
|
+
/** A main intercom wakes or reads an exited parked child through the agent runtime. */
|
|
11
|
+
export const WAKE = "pi-subagents:wake";
|
|
12
|
+
export const PARKED_HISTORY = "pi-subagents:parked-history";
|
|
13
|
+
export interface HistoryRequest {
|
|
14
|
+
readonly peer: string;
|
|
15
|
+
readonly reply: (history: Promise<NativeHistory | undefined>) => void;
|
|
16
|
+
}
|
|
17
|
+
export function isParkRequest(value: unknown): value is ParkRequest {
|
|
18
|
+
return isRecord(value) && typeof value.park === "boolean" && typeof value.reply === "function";
|
|
19
|
+
}
|
|
20
|
+
export function isHistoryRequest(value: unknown): value is HistoryRequest {
|
|
21
|
+
return isRecord(value) && typeof value.peer === "string" && typeof value.reply === "function";
|
|
22
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { execFileSync, type ChildProcess } from "node:child_process";
|
|
2
|
-
import {
|
|
1
|
+
import { execFile, execFileSync, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { readFileSync, readlinkSync } from "node:fs";
|
|
3
|
+
import { isLinuxProcessReplaced, readLinuxProcess } from "../pi-intercom/broker/pid.ts";
|
|
3
4
|
import { hasErrorCode } from "./unknown.ts";
|
|
4
5
|
|
|
6
|
+
const BIRTH_FIELDS = ["lstart=", "pgid="];
|
|
7
|
+
|
|
5
8
|
interface AgentProcessExit {
|
|
6
9
|
readonly pid?: number;
|
|
7
10
|
readonly code: number | null;
|
|
@@ -20,6 +23,31 @@ interface ChildWithKill {
|
|
|
20
23
|
readonly kill: (signal?: NodeJS.Signals | number) => boolean;
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
interface ProcessClock {
|
|
27
|
+
readonly boot: string;
|
|
28
|
+
readonly namespace: string;
|
|
29
|
+
readonly at: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Capture only a real, cross-process clock context; unavailable context proves nothing. */
|
|
33
|
+
export function readProcessClock(): ProcessClock | undefined {
|
|
34
|
+
if (process.platform !== "android") {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const boot = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
39
|
+
const namespace = readlinkSync("/proc/self/ns/time");
|
|
40
|
+
const at = process.hrtime.bigint();
|
|
41
|
+
return at > 0n &&
|
|
42
|
+
/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(boot) &&
|
|
43
|
+
/^time:\[\d+\]$/.test(namespace)
|
|
44
|
+
? { boot, namespace, at: at.toString() }
|
|
45
|
+
: undefined;
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
23
51
|
/** Capture the detached group leader's birth identity; Pi may replace its command-line title. */
|
|
24
52
|
export function readChildProcessIdentity(pid: number): string | undefined {
|
|
25
53
|
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
@@ -28,21 +56,151 @@ export function readChildProcessIdentity(pid: number): string | undefined {
|
|
|
28
56
|
if (process.platform === "linux") {
|
|
29
57
|
return readLinuxProcess(pid)?.identity;
|
|
30
58
|
}
|
|
59
|
+
return groupLeader(pid, readPsFields(pid, BIRTH_FIELDS));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function groupLeader(pid: number, birth: string | undefined): string | undefined {
|
|
63
|
+
return Number(birth?.split(" ").at(-1)) === pid ? birth : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Always one PID: Darwin ps given several PIDs scans every process at many times the cost.
|
|
67
|
+
function psArguments(pid: number, fields: readonly string[]): string[] {
|
|
68
|
+
return ["-p", String(pid), ...fields.flatMap((field) => ["-o", field])];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function psOptions(zone: string | undefined) {
|
|
72
|
+
const env = { ...process.env, LC_ALL: "C", ...(zone === undefined ? {} : { TZ: zone }) };
|
|
73
|
+
return { encoding: "utf8" as const, timeout: 1000, env };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readPsFields(pid: number, fields: readonly string[], zone?: string): string | undefined {
|
|
31
77
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
timeout: 1000,
|
|
78
|
+
return execFileSync("ps", psArguments(pid, fields), {
|
|
79
|
+
...psOptions(zone),
|
|
35
80
|
stdio: ["ignore", "pipe", "ignore"],
|
|
36
|
-
env: { ...process.env, LC_ALL: "C" },
|
|
37
81
|
})
|
|
38
82
|
.trim()
|
|
39
83
|
.replace(/\s+/g, " ");
|
|
40
|
-
return Number(identity.split(" ").at(-1)) === pid ? identity : undefined;
|
|
41
84
|
} catch {
|
|
42
85
|
return undefined;
|
|
43
86
|
}
|
|
44
87
|
}
|
|
45
88
|
|
|
89
|
+
/** Nonblocking `readPsFields`; a failed run likewise observes nothing. */
|
|
90
|
+
function observePsFields(
|
|
91
|
+
pid: number,
|
|
92
|
+
fields: readonly string[],
|
|
93
|
+
zone?: string,
|
|
94
|
+
): Promise<string | undefined> {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
execFile("ps", psArguments(pid, fields), psOptions(zone), (error, stdout) => {
|
|
97
|
+
resolve(error ? undefined : stdout.trim().replace(/\s+/g, " "));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// C-locale ps lstart (whole-second birth, in the reader's zone), then the process group.
|
|
103
|
+
const PS_IDENTITY = /^[A-Z][a-z]{2} ([A-Z][a-z]{2}) (\d{1,2}) (\d{2}):(\d{2}):(\d{2}) (\d{4}) \d+$/;
|
|
104
|
+
const MONTHS = "JanFebMarAprMayJunJulAugSepOctNovDec";
|
|
105
|
+
|
|
106
|
+
/** Parses a birth observed with TZ=UTC0. */
|
|
107
|
+
function utcBirth(birth: string | undefined): number {
|
|
108
|
+
const [, month = "", day, hours, minutes, seconds, year] = PS_IDENTITY.exec(birth ?? "") ?? [];
|
|
109
|
+
const index = MONTHS.indexOf(month);
|
|
110
|
+
// Unreadable or malformed observations yield NaN, which never proves a later birth.
|
|
111
|
+
return month.length === 3 && index % 3 === 0
|
|
112
|
+
? Date.UTC(
|
|
113
|
+
Number(year),
|
|
114
|
+
index / 3,
|
|
115
|
+
Number(day),
|
|
116
|
+
Number(hours),
|
|
117
|
+
Number(minutes),
|
|
118
|
+
Number(seconds),
|
|
119
|
+
)
|
|
120
|
+
: Number.NaN;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sameClock(left: ProcessClock, right: ProcessClock): boolean {
|
|
124
|
+
return left.boot === right.boot && left.namespace === right.namespace;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Android's elapsed age bounds birth in the saved monotonic clock, never wall time. */
|
|
128
|
+
function clockBound(clock: ProcessClock | undefined) {
|
|
129
|
+
const before = readProcessClock();
|
|
130
|
+
return clock !== undefined && before !== undefined && sameClock(clock, before)
|
|
131
|
+
? { clock, before }
|
|
132
|
+
: undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `elapsed` is observed after the bound's `before` read; the closing clock read follows both. */
|
|
136
|
+
function bornAfter(
|
|
137
|
+
{ clock, before }: { clock: ProcessClock; before: ProcessClock },
|
|
138
|
+
elapsed = "",
|
|
139
|
+
): boolean {
|
|
140
|
+
const after = readProcessClock();
|
|
141
|
+
if (after === undefined || !sameClock(before, after) || !/^\d+$/.test(elapsed)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
// Include displayed-second and native tick uncertainty. BOOTTIME age includes suspend,
|
|
145
|
+
// unlike hrtime's MONOTONIC clock, so suspend only moves this lower bound earlier.
|
|
146
|
+
const birth = BigInt(before.at) - (BigInt(elapsed) + 2n) * 1_000_000_000n;
|
|
147
|
+
return BigInt(after.at) >= BigInt(before.at) && birth > BigInt(clock.at);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* True only when a live PID provably names a different process than an owner-published record.
|
|
152
|
+
* Darwin identities are unzoned local times, so they are never compared across zones; only a
|
|
153
|
+
* UTC-observed birth strictly after the record's publication proves replacement. Android uses
|
|
154
|
+
* a conservative elapsed-age bound against its recorded boot-qualified monotonic clock.
|
|
155
|
+
*/
|
|
156
|
+
export function isChildProcessReplaced(
|
|
157
|
+
pid: number,
|
|
158
|
+
recorded: string,
|
|
159
|
+
published?: number,
|
|
160
|
+
clock?: ProcessClock,
|
|
161
|
+
): boolean {
|
|
162
|
+
if (process.platform === "linux") {
|
|
163
|
+
return isLinuxProcessReplaced(pid, recorded);
|
|
164
|
+
}
|
|
165
|
+
if (!PS_IDENTITY.test(recorded)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (process.platform === "android") {
|
|
169
|
+
const bound = clockBound(clock);
|
|
170
|
+
return bound !== undefined && bornAfter(bound, readPsFields(pid, ["etimes="]));
|
|
171
|
+
}
|
|
172
|
+
return published !== undefined && utcBirth(readPsFields(pid, BIRTH_FIELDS, "UTC0")) > published;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Nonblocking `readChildProcessIdentity(pid) !== recorded && isChildProcessReplaced(...)` for
|
|
177
|
+
* routine liveness. Signals still need the fresh synchronous proof.
|
|
178
|
+
*/
|
|
179
|
+
export async function observeChildProcessReplaced(
|
|
180
|
+
pid: number,
|
|
181
|
+
recorded: string,
|
|
182
|
+
published?: number,
|
|
183
|
+
clock?: ProcessClock,
|
|
184
|
+
): Promise<boolean> {
|
|
185
|
+
if (process.platform === "linux") {
|
|
186
|
+
return readChildProcessIdentity(pid) !== recorded && isLinuxProcessReplaced(pid, recorded);
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
!PS_IDENTITY.test(recorded) ||
|
|
190
|
+
groupLeader(pid, await observePsFields(pid, BIRTH_FIELDS)) === recorded
|
|
191
|
+
) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (process.platform === "android") {
|
|
195
|
+
const bound = clockBound(clock);
|
|
196
|
+
return bound !== undefined && bornAfter(bound, await observePsFields(pid, ["etimes="]));
|
|
197
|
+
}
|
|
198
|
+
if (published === undefined) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return utcBirth(await observePsFields(pid, BIRTH_FIELDS, "UTC0")) > published;
|
|
202
|
+
}
|
|
203
|
+
|
|
46
204
|
function trySignalChild(child: ChildWithKill, signal: NodeJS.Signals): boolean {
|
|
47
205
|
try {
|
|
48
206
|
return child.kill(signal);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MouseRegion,
|
|
3
|
+
TruncatedText,
|
|
4
|
+
truncateToWidth,
|
|
5
|
+
visibleWidth,
|
|
6
|
+
type Component,
|
|
7
|
+
} from "@earendil-works/pi-tui";
|
|
8
|
+
|
|
9
|
+
export function actionText(text: string, action: (column: number) => void): Component {
|
|
10
|
+
const component = new TruncatedText(text, 0, 0);
|
|
11
|
+
return new MouseRegion(component, (event) => {
|
|
12
|
+
if (
|
|
13
|
+
(event.type !== "press" && event.type !== "click") ||
|
|
14
|
+
event.button !== "left" ||
|
|
15
|
+
event.x >= visibleWidth(truncateToWidth(text, event.width))
|
|
16
|
+
) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (event.type === "click") {
|
|
20
|
+
action(event.x);
|
|
21
|
+
}
|
|
22
|
+
return { handled: true, render: event.type === "click" };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Each visible hint dispatches the same action as its corresponding keyboard binding. */
|
|
27
|
+
export function actionHints(hints: readonly { text: string; action: () => void }[]): Component {
|
|
28
|
+
return actionText(hints.map((hint) => hint.text).join(" · "), (column) => {
|
|
29
|
+
let start = 0;
|
|
30
|
+
for (const hint of hints) {
|
|
31
|
+
const end = start + visibleWidth(hint.text);
|
|
32
|
+
if (column >= start && column < end) {
|
|
33
|
+
hint.action();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
start = end + 3;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { Many, none } from "stream-chain/defs.js";
|
|
2
|
-
import type { ParserOptions, Token } from "stream-json/core/parser.js";
|
|
3
|
-
|
|
4
|
-
// stream-json 3.7.0 exports the synchronous tokenizer used by its default
|
|
5
|
-
// generator adapter, but omits this export from parser.d.ts.
|
|
6
|
-
declare module "stream-json/core/parser.js" {
|
|
7
|
-
export function jsonParser(
|
|
8
|
-
options?: Readonly<ParserOptions>,
|
|
9
|
-
): (chunk: string | typeof none) => Many<Token> | typeof none;
|
|
10
|
-
}
|