@fitchmultz/pi-subagents 0.50.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -2
- package/README.md +36 -5
- package/dist/agent/contracts.js +6 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +207 -173
- package/dist/agent/journal-tail.js +115 -156
- package/dist/agent/launch.js +20 -8
- package/dist/agent/legacy.js +65 -23
- package/dist/agent/notifications.js +138 -47
- package/dist/agent/observation-state.js +246 -0
- package/dist/agent/observation-view.js +258 -0
- package/dist/agent/observation.js +346 -0
- package/dist/agent/observe-card.js +243 -0
- package/dist/agent/observe-chrome.js +85 -0
- package/dist/agent/observe-client.js +285 -0
- package/dist/agent/observe-layout.js +65 -0
- package/dist/agent/observe-reading.js +84 -0
- package/dist/agent/observe-screen.js +400 -96
- package/dist/agent/observe-transcript.js +382 -0
- package/dist/agent/observe.js +250 -43
- package/dist/agent/process.js +198 -25
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +125 -20
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +29 -12
- package/dist/agent/tool-schemas.js +20 -0
- package/dist/agent/typebox.js +10 -5
- package/dist/agent/usage.js +0 -115
- package/dist/agent/view.js +46 -16
- package/dist/agent/wake.js +99 -0
- package/dist/build-info.js +1 -1
- package/dist/intercom-client.d.ts +26 -0
- package/dist/intercom-client.js +45 -0
- package/dist/pi-intercom/agent-dir.d.ts +1 -0
- package/dist/pi-intercom/bridge-session.js +2 -1
- package/dist/pi-intercom/broker/broker.js +27 -21
- package/dist/pi-intercom/broker/client-last.d.ts +13 -0
- package/dist/pi-intercom/broker/client-last.js +17 -5
- package/dist/pi-intercom/broker/client.d.ts +60 -0
- package/dist/pi-intercom/broker/client.js +47 -16
- package/dist/pi-intercom/broker/connection.d.ts +29 -0
- package/dist/pi-intercom/broker/framing.d.ts +17 -0
- package/dist/pi-intercom/broker/framing.js +10 -7
- package/dist/pi-intercom/broker/guard.js +23 -0
- package/dist/pi-intercom/broker/last.d.ts +26 -0
- package/dist/pi-intercom/broker/last.js +57 -32
- package/dist/pi-intercom/broker/paths.d.ts +13 -0
- package/dist/pi-intercom/broker/paths.js +35 -12
- package/dist/pi-intercom/broker/pid.js +5 -1
- package/dist/pi-intercom/broker/server-connection.js +19 -0
- package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
- package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
- package/dist/pi-intercom/broker/targets.d.ts +9 -0
- package/dist/pi-intercom/connection.js +10 -2
- package/dist/pi-intercom/identity.js +6 -5
- package/dist/pi-intercom/inbound-delivery.js +16 -60
- package/dist/pi-intercom/inbound-journal.js +83 -6
- package/dist/pi-intercom/inbound-record.js +18 -1
- package/dist/pi-intercom/inbound.js +12 -38
- package/dist/pi-intercom/index.js +3 -0
- package/dist/pi-intercom/last-message.js +4 -16
- package/dist/pi-intercom/lifecycle.js +88 -3
- package/dist/pi-intercom/outbound.js +5 -15
- package/dist/pi-intercom/reply-tracker.js +7 -6
- package/dist/pi-intercom/reply-wait.js +2 -2
- package/dist/pi-intercom/session-targets.d.ts +24 -0
- package/dist/pi-intercom/tool-registration.js +4 -1
- package/dist/pi-intercom/tool-results.js +5 -7
- package/dist/pi-intercom/tool-schemas.js +12 -26
- package/dist/pi-intercom/types.d.ts +212 -0
- package/dist/pi-intercom/types.js +54 -3
- package/dist/pi-intercom/validation.d.ts +12 -0
- package/dist/pi-intercom/validation.js +3 -0
- package/dist/shared/bounded-json.js +151 -0
- package/dist/shared/compact-cards.js +66 -0
- package/dist/shared/journal-frames.js +3 -6
- package/dist/shared/json-projection.js +267 -129
- package/dist/shared/native-journal.js +123 -56
- package/dist/shared/post-exit-stdio-guard.js +120 -7
- package/dist/shared/tui-action.js +29 -0
- package/dist/shared/unknown.d.ts +9 -0
- package/docs/code-quality.md +2 -0
- package/docs/development.md +40 -1
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +226 -6
- package/docs/performance.md +87 -11
- package/docs/reference.md +157 -20
- package/docs/retired-history-maintenance.md +252 -0
- package/package.json +28 -15
- package/scripts/archive-retired-history.mjs +464 -0
- package/scripts/build.mjs +48 -28
- package/scripts/measure-size.mjs +1 -1
- package/scripts/native-package-smoke.mjs +153 -12
- package/scripts/package-smoke.mjs +80 -11
- package/scripts/prepare.mjs +2 -2
- package/scripts/real-pi-live.mjs +377 -0
- package/scripts/real-pi-processes.mjs +6 -1
- package/scripts/real-pi-smoke.mjs +6 -282
- package/scripts/run-tests.mjs +4 -2
- package/scripts/typescript.mjs +35 -0
- package/skills/pi-intercom/SKILL.md +2 -1
- package/skills/pi-subagents/SKILL.md +23 -5
- package/src/agent/contracts.ts +8 -34
- package/src/agent/host.ts +19 -20
- package/src/agent/index.ts +224 -190
- package/src/agent/journal-tail.ts +141 -153
- package/src/agent/launch.ts +33 -9
- package/src/agent/legacy.ts +85 -31
- package/src/agent/notifications.ts +192 -67
- package/src/agent/observation-state.ts +277 -0
- package/src/agent/observation-view.ts +267 -0
- package/src/agent/observation.ts +405 -0
- package/src/agent/observe-card.ts +304 -0
- package/src/agent/observe-chrome.ts +170 -0
- package/src/agent/observe-client.ts +318 -0
- package/src/agent/observe-layout.ts +79 -0
- package/src/agent/observe-reading.ts +97 -0
- package/src/agent/observe-screen.ts +443 -129
- package/src/agent/observe-transcript.ts +434 -0
- package/src/agent/observe.ts +304 -71
- package/src/agent/process.ts +240 -34
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +125 -27
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +27 -11
- package/src/agent/tool-schemas.ts +33 -0
- package/src/agent/typebox.ts +12 -7
- package/src/agent/usage.ts +0 -130
- package/src/agent/view.ts +70 -20
- package/src/agent/wake.ts +107 -0
- package/src/intercom-client.ts +108 -0
- package/src/pi-intercom/bridge-session.ts +2 -2
- package/src/pi-intercom/broker/broker.ts +27 -23
- package/src/pi-intercom/broker/client-last.ts +25 -12
- package/src/pi-intercom/broker/client.ts +55 -33
- package/src/pi-intercom/broker/framing.ts +11 -8
- package/src/pi-intercom/broker/guard.ts +33 -0
- package/src/pi-intercom/broker/last.ts +86 -33
- package/src/pi-intercom/broker/paths.ts +39 -12
- package/src/pi-intercom/broker/pid.ts +6 -1
- package/src/pi-intercom/broker/server-connection.ts +25 -0
- package/src/pi-intercom/connection.ts +21 -10
- package/src/pi-intercom/identity.ts +8 -5
- package/src/pi-intercom/inbound-delivery.ts +20 -66
- package/src/pi-intercom/inbound-journal.ts +103 -7
- package/src/pi-intercom/inbound-record.ts +21 -4
- package/src/pi-intercom/inbound.ts +18 -49
- package/src/pi-intercom/index.ts +3 -0
- package/src/pi-intercom/last-message.ts +10 -19
- package/src/pi-intercom/lifecycle.ts +117 -14
- package/src/pi-intercom/outbound.ts +7 -16
- package/src/pi-intercom/reply-tracker.ts +8 -6
- package/src/pi-intercom/reply-wait.ts +3 -2
- package/src/pi-intercom/runtime-types.ts +0 -1
- package/src/pi-intercom/tool-registration.ts +4 -1
- package/src/pi-intercom/tool-results.ts +10 -9
- package/src/pi-intercom/tool-schemas.ts +12 -44
- package/src/pi-intercom/types.ts +115 -4
- package/src/pi-intercom/validation.ts +4 -0
- package/src/shared/bounded-json.ts +162 -0
- package/src/shared/compact-cards.ts +118 -0
- package/src/shared/journal-frames.ts +3 -19
- package/src/shared/json-projection.ts +267 -147
- package/src/shared/native-journal.ts +157 -75
- package/src/shared/post-exit-stdio-guard.ts +165 -7
- package/src/shared/tui-action.ts +39 -0
- package/tsconfig.client.json +8 -0
- package/tsconfig.json +1 -0
- package/src/shared/stream-json.d.ts +0 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { JournalFrames } from "./journal-frames.js";
|
|
3
3
|
import { nativeProjection } from "./journal-projections.js";
|
|
4
|
-
import { isRecord
|
|
4
|
+
import { isRecord } from "./unknown.js";
|
|
5
5
|
function preview(record, parent, legacy) {
|
|
6
6
|
const entry = record.value;
|
|
7
7
|
if (typeof entry.type !== "string") {
|
|
@@ -18,45 +18,40 @@ function preview(record, parent, legacy) {
|
|
|
18
18
|
};
|
|
19
19
|
return { value, start: record.start, end: record.end };
|
|
20
20
|
}
|
|
21
|
-
function configurationModel(value) {
|
|
22
|
-
const message = recordAt(value, "message");
|
|
23
|
-
const provider = value.type === "model_change" ? value.provider : message?.provider;
|
|
24
|
-
const model = value.type === "model_change" ? value.modelId : message?.model;
|
|
25
|
-
return typeof provider === "string" && typeof model === "string"
|
|
26
|
-
? `${provider}/${model}`
|
|
27
|
-
: undefined;
|
|
28
|
-
}
|
|
29
21
|
export class NativeJournal {
|
|
30
22
|
records = [];
|
|
31
23
|
byId = new Map();
|
|
32
24
|
stamp;
|
|
33
25
|
end;
|
|
34
26
|
file;
|
|
35
|
-
|
|
27
|
+
identity;
|
|
28
|
+
offset = 0;
|
|
29
|
+
frames;
|
|
30
|
+
constructor(file, policy = "inspect", requireNewline = policy === "live", options = {}) {
|
|
36
31
|
this.file = file;
|
|
37
|
-
|
|
32
|
+
let parent = null;
|
|
33
|
+
this.frames = new JournalFrames(options.select ?? nativeProjection, (raw) => {
|
|
34
|
+
const record = preview(raw, parent, this.records[0]?.value.version === 1);
|
|
35
|
+
if (!record) {
|
|
36
|
+
if (policy !== "inspect") {
|
|
37
|
+
throw new SyntaxError(`Native record at byte ${raw.start} requires a type`);
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (record.value.type !== "session") {
|
|
42
|
+
parent = record.value.id;
|
|
43
|
+
}
|
|
44
|
+
this.records.push(record);
|
|
45
|
+
this.byId.set(record.value.id, record);
|
|
46
|
+
}, { policy, requireNewline });
|
|
47
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | (policy === "live" ? fs.constants.O_NOFOLLOW : 0));
|
|
38
48
|
try {
|
|
39
49
|
const stat = fs.fstatSync(fd, { bigint: true });
|
|
40
50
|
this.stamp = journalStamp(stat);
|
|
51
|
+
this.identity = `${stat.dev}:${stat.ino}`;
|
|
41
52
|
this.end = Number(stat.size);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const record = preview(raw, parent, this.records[0]?.value.version === 1);
|
|
45
|
-
if (!record) {
|
|
46
|
-
if (policy !== "inspect") {
|
|
47
|
-
throw new SyntaxError(`Native record at byte ${raw.start} requires a type`);
|
|
48
|
-
}
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (record.value.type !== "session") {
|
|
52
|
-
parent = record.value.id;
|
|
53
|
-
}
|
|
54
|
-
this.records.push(record);
|
|
55
|
-
this.byId.set(record.value.id, record);
|
|
56
|
-
}, { policy, requireNewline, end: this.end });
|
|
57
|
-
if (journalStamp(fs.fstatSync(fd, { bigint: true })) !== this.stamp) {
|
|
58
|
-
throw new Error("Journal changed while indexing; refresh history.");
|
|
59
|
-
}
|
|
53
|
+
this.index(fd, Math.min(this.end, options.readBytes ?? Infinity));
|
|
54
|
+
this.checkInitial(stat, fs.fstatSync(fd, { bigint: true }), policy);
|
|
60
55
|
}
|
|
61
56
|
finally {
|
|
62
57
|
fs.closeSync(fd);
|
|
@@ -65,21 +60,101 @@ export class NativeJournal {
|
|
|
65
60
|
throw new Error("Not a readable native Pi session.");
|
|
66
61
|
}
|
|
67
62
|
}
|
|
68
|
-
|
|
63
|
+
checkInitial(before, after, policy) {
|
|
64
|
+
if (journalStamp(after) !== this.stamp &&
|
|
65
|
+
(policy !== "live" ||
|
|
66
|
+
after.size < before.size ||
|
|
67
|
+
`${after.dev}:${after.ino}` !== this.identity)) {
|
|
68
|
+
throw new Error("Journal changed while indexing; refresh history.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
index(fd, end) {
|
|
72
|
+
const buffer = Buffer.allocUnsafe(65536);
|
|
73
|
+
while (this.offset < end) {
|
|
74
|
+
const count = fs.readSync(fd, buffer, 0, Math.min(buffer.length, end - this.offset), this.offset);
|
|
75
|
+
if (count === 0) {
|
|
76
|
+
throw new Error("Journal truncated while reading");
|
|
77
|
+
}
|
|
78
|
+
this.frames.write(buffer.subarray(0, count));
|
|
79
|
+
this.offset += count;
|
|
80
|
+
}
|
|
81
|
+
this.frames.finish();
|
|
82
|
+
}
|
|
83
|
+
get indexed() {
|
|
84
|
+
return this.offset === this.end;
|
|
85
|
+
}
|
|
86
|
+
/** Incremental, LF-published indexing. Append work is budgeted independently of paint. */
|
|
87
|
+
refresh(bytes = 256 * 1024) {
|
|
88
|
+
const fd = fs.openSync(this.file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
89
|
+
try {
|
|
90
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
91
|
+
if (`${stat.dev}:${stat.ino}` !== this.identity || Number(stat.size) < this.offset) {
|
|
92
|
+
throw new Error("Native journal was replaced or truncated; reopen this conversation.");
|
|
93
|
+
}
|
|
94
|
+
if (journalStamp(stat) === this.stamp && this.indexed) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const previous = this.records.length;
|
|
98
|
+
this.end = Number(stat.size);
|
|
99
|
+
this.index(fd, Math.min(this.end, this.offset + bytes));
|
|
100
|
+
this.stamp = journalStamp(stat);
|
|
101
|
+
return previous !== this.records.length;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
fs.closeSync(fd);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Project one immutable, published record, including bounded slices of very large strings. */
|
|
108
|
+
async project(record, select, stringChunk, signal) {
|
|
109
|
+
const fd = fs.openSync(this.file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
110
|
+
let stream;
|
|
111
|
+
try {
|
|
112
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
113
|
+
if (`${stat.dev}:${stat.ino}` !== this.identity || Number(stat.size) < record.end) {
|
|
114
|
+
throw new Error("Native journal was replaced or truncated.");
|
|
115
|
+
}
|
|
116
|
+
let value;
|
|
117
|
+
const frames = new JournalFrames(select, (entry) => {
|
|
118
|
+
value = entry.value;
|
|
119
|
+
}, { policy: "live", start: record.start, stringChunk });
|
|
120
|
+
stream = fs.createReadStream(this.file, {
|
|
121
|
+
fd,
|
|
122
|
+
start: record.start,
|
|
123
|
+
end: record.end - 1,
|
|
124
|
+
highWaterMark: 65536,
|
|
125
|
+
signal,
|
|
126
|
+
});
|
|
127
|
+
for await (const chunk of stream) {
|
|
128
|
+
if (!Buffer.isBuffer(chunk)) {
|
|
129
|
+
throw new TypeError("Native journal reads require bytes.");
|
|
130
|
+
}
|
|
131
|
+
frames.write(chunk);
|
|
132
|
+
}
|
|
133
|
+
frames.finish();
|
|
134
|
+
if (!value) {
|
|
135
|
+
throw new Error("Native entry is not published.");
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
if (stream) {
|
|
141
|
+
stream.destroy();
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
fs.closeSync(fd);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
branch(leaf) {
|
|
69
149
|
if (leaf === null) {
|
|
70
150
|
return [];
|
|
71
151
|
}
|
|
72
|
-
const eligible = endedAt === undefined
|
|
73
|
-
? this.records
|
|
74
|
-
: this.records.filter((record) => record.value.type === "session" ||
|
|
75
|
-
Date.parse(record.value.timestamp ?? "") <= endedAt);
|
|
76
152
|
let current = leaf !== undefined && leaf !== ""
|
|
77
153
|
? this.byId.get(leaf)
|
|
78
|
-
:
|
|
154
|
+
: this.records.findLast((record) => record.value.type !== "session");
|
|
79
155
|
const path = [];
|
|
80
156
|
const seen = new Set();
|
|
81
|
-
|
|
82
|
-
while (current && current.value.type !== "session" && allowed.has(current.value.id)) {
|
|
157
|
+
while (current && current.value.type !== "session") {
|
|
83
158
|
if (seen.has(current.value.id)) {
|
|
84
159
|
throw new Error("Cyclic journal ancestry");
|
|
85
160
|
}
|
|
@@ -116,24 +191,16 @@ export class NativeJournal {
|
|
|
116
191
|
fs.closeSync(fd);
|
|
117
192
|
}
|
|
118
193
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (model) {
|
|
132
|
-
result.model = configurationModel(model);
|
|
133
|
-
result.modelRecordedAt = Date.parse(model.timestamp ?? "");
|
|
134
|
-
}
|
|
135
|
-
return result;
|
|
136
|
-
}
|
|
194
|
+
}
|
|
195
|
+
/** Native v3 writers append after attachment; loading older formats can rewrite in place.
|
|
196
|
+
* Outside prefix edits followed by same-inode growth are not an append-only operation.
|
|
197
|
+
*/
|
|
198
|
+
export function nativeAppend(before, after, header) {
|
|
199
|
+
return (header?.type === "session" &&
|
|
200
|
+
header.version === 3 &&
|
|
201
|
+
before.dev === after.dev &&
|
|
202
|
+
before.ino === after.ino &&
|
|
203
|
+
after.size > before.size);
|
|
137
204
|
}
|
|
138
205
|
export function journalStamp(stat) {
|
|
139
206
|
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
1
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
+
import { readFileSync, readlinkSync } from "node:fs";
|
|
3
|
+
import { isLinuxProcessReplaced, readLinuxProcess } from "../pi-intercom/broker/pid.js";
|
|
3
4
|
import { hasErrorCode } from "./unknown.js";
|
|
5
|
+
const BIRTH_FIELDS = ["lstart=", "pgid="];
|
|
6
|
+
/** Capture only a real, cross-process clock context; unavailable context proves nothing. */
|
|
7
|
+
export function readProcessClock() {
|
|
8
|
+
if (process.platform !== "android") {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const boot = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
13
|
+
const namespace = readlinkSync("/proc/self/ns/time");
|
|
14
|
+
const at = process.hrtime.bigint();
|
|
15
|
+
return at > 0n &&
|
|
16
|
+
/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(boot) &&
|
|
17
|
+
/^time:\[\d+\]$/.test(namespace)
|
|
18
|
+
? { boot, namespace, at: at.toString() }
|
|
19
|
+
: undefined;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
4
25
|
/** Capture the detached group leader's birth identity; Pi may replace its command-line title. */
|
|
5
26
|
export function readChildProcessIdentity(pid) {
|
|
6
27
|
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
@@ -9,21 +30,113 @@ export function readChildProcessIdentity(pid) {
|
|
|
9
30
|
if (process.platform === "linux") {
|
|
10
31
|
return readLinuxProcess(pid)?.identity;
|
|
11
32
|
}
|
|
33
|
+
return groupLeader(pid, readPsFields(pid, BIRTH_FIELDS));
|
|
34
|
+
}
|
|
35
|
+
function groupLeader(pid, birth) {
|
|
36
|
+
return Number(birth?.split(" ").at(-1)) === pid ? birth : undefined;
|
|
37
|
+
}
|
|
38
|
+
// Always one PID: Darwin ps given several PIDs scans every process at many times the cost.
|
|
39
|
+
function psArguments(pid, fields) {
|
|
40
|
+
return ["-p", String(pid), ...fields.flatMap((field) => ["-o", field])];
|
|
41
|
+
}
|
|
42
|
+
function psOptions(zone) {
|
|
43
|
+
const env = { ...process.env, LC_ALL: "C", ...(zone === undefined ? {} : { TZ: zone }) };
|
|
44
|
+
return { encoding: "utf8", timeout: 1000, env };
|
|
45
|
+
}
|
|
46
|
+
function readPsFields(pid, fields, zone) {
|
|
12
47
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
timeout: 1000,
|
|
48
|
+
return execFileSync("ps", psArguments(pid, fields), {
|
|
49
|
+
...psOptions(zone),
|
|
16
50
|
stdio: ["ignore", "pipe", "ignore"],
|
|
17
|
-
env: { ...process.env, LC_ALL: "C" },
|
|
18
51
|
})
|
|
19
52
|
.trim()
|
|
20
53
|
.replace(/\s+/g, " ");
|
|
21
|
-
return Number(identity.split(" ").at(-1)) === pid ? identity : undefined;
|
|
22
54
|
}
|
|
23
55
|
catch {
|
|
24
56
|
return undefined;
|
|
25
57
|
}
|
|
26
58
|
}
|
|
59
|
+
/** Nonblocking `readPsFields`; a failed run likewise observes nothing. */
|
|
60
|
+
function observePsFields(pid, fields, zone) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
execFile("ps", psArguments(pid, fields), psOptions(zone), (error, stdout) => {
|
|
63
|
+
resolve(error ? undefined : stdout.trim().replace(/\s+/g, " "));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// C-locale ps lstart (whole-second birth, in the reader's zone), then the process group.
|
|
68
|
+
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+$/;
|
|
69
|
+
const MONTHS = "JanFebMarAprMayJunJulAugSepOctNovDec";
|
|
70
|
+
/** Parses a birth observed with TZ=UTC0. */
|
|
71
|
+
function utcBirth(birth) {
|
|
72
|
+
const [, month = "", day, hours, minutes, seconds, year] = PS_IDENTITY.exec(birth ?? "") ?? [];
|
|
73
|
+
const index = MONTHS.indexOf(month);
|
|
74
|
+
// Unreadable or malformed observations yield NaN, which never proves a later birth.
|
|
75
|
+
return month.length === 3 && index % 3 === 0
|
|
76
|
+
? Date.UTC(Number(year), index / 3, Number(day), Number(hours), Number(minutes), Number(seconds))
|
|
77
|
+
: Number.NaN;
|
|
78
|
+
}
|
|
79
|
+
function sameClock(left, right) {
|
|
80
|
+
return left.boot === right.boot && left.namespace === right.namespace;
|
|
81
|
+
}
|
|
82
|
+
/** Android's elapsed age bounds birth in the saved monotonic clock, never wall time. */
|
|
83
|
+
function clockBound(clock) {
|
|
84
|
+
const before = readProcessClock();
|
|
85
|
+
return clock !== undefined && before !== undefined && sameClock(clock, before)
|
|
86
|
+
? { clock, before }
|
|
87
|
+
: undefined;
|
|
88
|
+
}
|
|
89
|
+
/** `elapsed` is observed after the bound's `before` read; the closing clock read follows both. */
|
|
90
|
+
function bornAfter({ clock, before }, elapsed = "") {
|
|
91
|
+
const after = readProcessClock();
|
|
92
|
+
if (after === undefined || !sameClock(before, after) || !/^\d+$/.test(elapsed)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
// Include displayed-second and native tick uncertainty. BOOTTIME age includes suspend,
|
|
96
|
+
// unlike hrtime's MONOTONIC clock, so suspend only moves this lower bound earlier.
|
|
97
|
+
const birth = BigInt(before.at) - (BigInt(elapsed) + 2n) * 1000000000n;
|
|
98
|
+
return BigInt(after.at) >= BigInt(before.at) && birth > BigInt(clock.at);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* True only when a live PID provably names a different process than an owner-published record.
|
|
102
|
+
* Darwin identities are unzoned local times, so they are never compared across zones; only a
|
|
103
|
+
* UTC-observed birth strictly after the record's publication proves replacement. Android uses
|
|
104
|
+
* a conservative elapsed-age bound against its recorded boot-qualified monotonic clock.
|
|
105
|
+
*/
|
|
106
|
+
export function isChildProcessReplaced(pid, recorded, published, clock) {
|
|
107
|
+
if (process.platform === "linux") {
|
|
108
|
+
return isLinuxProcessReplaced(pid, recorded);
|
|
109
|
+
}
|
|
110
|
+
if (!PS_IDENTITY.test(recorded)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (process.platform === "android") {
|
|
114
|
+
const bound = clockBound(clock);
|
|
115
|
+
return bound !== undefined && bornAfter(bound, readPsFields(pid, ["etimes="]));
|
|
116
|
+
}
|
|
117
|
+
return published !== undefined && utcBirth(readPsFields(pid, BIRTH_FIELDS, "UTC0")) > published;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Nonblocking `readChildProcessIdentity(pid) !== recorded && isChildProcessReplaced(...)` for
|
|
121
|
+
* routine liveness. Signals still need the fresh synchronous proof.
|
|
122
|
+
*/
|
|
123
|
+
export async function observeChildProcessReplaced(pid, recorded, published, clock) {
|
|
124
|
+
if (process.platform === "linux") {
|
|
125
|
+
return readChildProcessIdentity(pid) !== recorded && isLinuxProcessReplaced(pid, recorded);
|
|
126
|
+
}
|
|
127
|
+
if (!PS_IDENTITY.test(recorded) ||
|
|
128
|
+
groupLeader(pid, await observePsFields(pid, BIRTH_FIELDS)) === recorded) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (process.platform === "android") {
|
|
132
|
+
const bound = clockBound(clock);
|
|
133
|
+
return bound !== undefined && bornAfter(bound, await observePsFields(pid, ["etimes="]));
|
|
134
|
+
}
|
|
135
|
+
if (published === undefined) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return utcBirth(await observePsFields(pid, BIRTH_FIELDS, "UTC0")) > published;
|
|
139
|
+
}
|
|
27
140
|
function trySignalChild(child, signal) {
|
|
28
141
|
try {
|
|
29
142
|
return child.kill(signal);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MouseRegion, TruncatedText, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
|
|
2
|
+
export function actionText(text, action) {
|
|
3
|
+
const component = new TruncatedText(text, 0, 0);
|
|
4
|
+
return new MouseRegion(component, (event) => {
|
|
5
|
+
if ((event.type !== "press" && event.type !== "click") ||
|
|
6
|
+
event.button !== "left" ||
|
|
7
|
+
event.x >= visibleWidth(truncateToWidth(text, event.width))) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (event.type === "click") {
|
|
11
|
+
action(event.x);
|
|
12
|
+
}
|
|
13
|
+
return { handled: true, render: event.type === "click" };
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/** Each visible hint dispatches the same action as its corresponding keyboard binding. */
|
|
17
|
+
export function actionHints(hints) {
|
|
18
|
+
return actionText(hints.map((hint) => hint.text).join(" · "), (column) => {
|
|
19
|
+
let start = 0;
|
|
20
|
+
for (const hint of hints) {
|
|
21
|
+
const end = start + visibleWidth(hint.text);
|
|
22
|
+
if (column >= start && column < end) {
|
|
23
|
+
hint.action();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
start = end + 3;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** JSON and caught failures cross the process/filesystem boundary as unknown values. */
|
|
2
|
+
export type UnknownRecord = Readonly<Record<string, unknown>>;
|
|
3
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
4
|
+
export declare function isUnknownArray(value: unknown): value is readonly unknown[];
|
|
5
|
+
export declare function recordAt(value: unknown, key: string | number): UnknownRecord | undefined;
|
|
6
|
+
export declare function errorMessage(error: unknown): string;
|
|
7
|
+
export declare function hasErrorCode(error: unknown, code: string): boolean;
|
|
8
|
+
/** Retain native error names without letting a broken stringifier replace the caught failure. */
|
|
9
|
+
export declare function errorDescription(error: unknown): string;
|
package/docs/code-quality.md
CHANGED
|
@@ -37,3 +37,5 @@ suite or self-hosted runner requirement.
|
|
|
37
37
|
CI's lint job also enforces the production line/byte shrink budget before
|
|
38
38
|
qualification substitutes the selected host graph. The retired workflow schema
|
|
39
39
|
generator and its patching dependency are not part of the new runtime.
|
|
40
|
+
The active-child viewer has an approved aggregate budget of 19,000 source lines
|
|
41
|
+
and 22,500 production lines. The 1 MiB byte ceiling and all lint limits stay unchanged.
|
package/docs/development.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Development
|
|
2
2
|
|
|
3
|
+
The supported Intercom client's package export uses emitted declarations for consumers.
|
|
4
|
+
The repository's native `development` type condition points at its TypeScript source so
|
|
5
|
+
cold lint and type checks do not require a prior build. Packed-consumer checks compile
|
|
6
|
+
against the default declaration export without that condition or private path aliases.
|
|
7
|
+
|
|
3
8
|
Use a dedicated worktree, the committed npm lockfile, and a ready-for-review PR.
|
|
4
9
|
Never edit source or tests while checks run in that checkout. Do not modify an
|
|
5
10
|
active installed copy or reload the user's sessions to test a candidate.
|
|
@@ -16,8 +21,42 @@ Run narrow owner-boundary tests locally through `scripts/run-tests.mjs`. Full
|
|
|
16
21
|
`npm run ci` belongs in hosted CI and includes package/install smokes and the
|
|
17
22
|
complete suites. Keep the strict Oxlint/Oxfmt rules; see [code quality](code-quality.md).
|
|
18
23
|
|
|
24
|
+
## Native Android/Termux
|
|
25
|
+
|
|
26
|
+
Use app-private storage, not Android's shared `/sdcard` filesystem. No Termux app
|
|
27
|
+
patch, privileged access, or Android security-policy change is required.
|
|
28
|
+
|
|
29
|
+
TypeScript 7.0.2 does not publish its Android ARM64 npm compiler package. Source
|
|
30
|
+
installs, `build`, and `typecheck` therefore use an Android-native `tsgo` on PATH;
|
|
31
|
+
other platforms keep using the committed npm TypeScript version. Install a native
|
|
32
|
+
build from Microsoft's maintained [TypeScript source](https://github.com/microsoft/typescript-go)
|
|
33
|
+
with Android support. A Linux ARM64 binary is not an Android substitute. The
|
|
34
|
+
prebuilt npm package needs no compiler.
|
|
35
|
+
|
|
36
|
+
The package uses Termux's shared `$PREFIX/tmp` for its default broker and
|
|
37
|
+
observation endpoints, independently of individual sessions' HOME or TMPDIR.
|
|
38
|
+
Other Android environments without PREFIX use Node's native temporary directory.
|
|
39
|
+
Tests keep their isolated sockets short enough for the same 100-byte path limit.
|
|
40
|
+
|
|
41
|
+
Android process-reuse checks compare native elapsed age with a saved monotonic
|
|
42
|
+
bound, qualified by the real boot ID and time namespace, rather than wall clocks
|
|
43
|
+
or `ps` boot-epoch dates. Keep the conservative second/tick allowance and reject
|
|
44
|
+
missing or incomparable clock evidence. Legacy records without that evidence
|
|
45
|
+
retain fail-closed identity checks; do not bypass unavailable procfs PID-namespace
|
|
46
|
+
guards or invent a namespace.
|
|
47
|
+
|
|
48
|
+
The published Oxlint type-aware backend currently has no Android package. Keep
|
|
49
|
+
the canonical lint command and rules intact and obtain its passing evidence in
|
|
50
|
+
hosted CI; do not substitute a weakened checker or incompatible Linux executable.
|
|
51
|
+
|
|
19
52
|
## Runtime qualification
|
|
20
53
|
|
|
54
|
+
The official development SDK 1.4.0 pins Undici 8.10.2 exactly. A parent-scoped
|
|
55
|
+
override selects 8.12.3 for that development graph to fix GHSA-4qv8-2p9j-2685,
|
|
56
|
+
GHSA-f72v-6w45-q472 and GHSA-xjj6-f33q-929c. Remove the override when an official
|
|
57
|
+
SDK release supplies a fixed version. It does not replace a consumer's host SDK
|
|
58
|
+
or Node's built-in fetch.
|
|
59
|
+
|
|
21
60
|
Qualify the latest stable official Pi and the intended fork revision separately.
|
|
22
61
|
Record the exact package root, revision/build, Node binary, and executed test
|
|
23
62
|
commands. Matching versions do not establish matching runtime behavior.
|
|
@@ -52,7 +91,7 @@ negative assertions. A sleep does not prove an owner processed its input.
|
|
|
52
91
|
|
|
53
92
|
`scripts/measure-size.mjs` counts source, production scripts, generated code and
|
|
54
93
|
schemas, including new untracked files. `--report` prints without enforcing.
|
|
55
|
-
The ceilings are
|
|
94
|
+
The ceilings are 19,000 source TypeScript lines, 22,500 total production lines,
|
|
56
95
|
and 1 MiB total production bytes. Tests have no deletion quota. Moving machinery
|
|
57
96
|
into generated/minified output does not evade the gate.
|
|
58
97
|
|
package/docs/intercom-bridge.md
CHANGED
|
@@ -122,10 +122,15 @@ retries. A lost response is **UNKNOWN**, not proof that a message was unsent.
|
|
|
122
122
|
| `POST /v1/ack` | `{ids}` (at most 256) | `acked`; durable intake ownership transferred |
|
|
123
123
|
|
|
124
124
|
`to` uses the existing exact-ID/name resolver; ambiguity never selects a peer.
|
|
125
|
-
Explicit message IDs survive byte-for-byte. `
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
Explicit message IDs survive byte-for-byte. Use a fresh `--id` / `messageId` for
|
|
126
|
+
each new logical message; reuse it only to retry that same message. Updated native
|
|
127
|
+
recipients ignore an already-admitted ID even if a retry changes its payload,
|
|
128
|
+
while the broker can still report transport acceptance. Do not reuse a previous
|
|
129
|
+
ID to send a different instruction.
|
|
130
|
+
|
|
131
|
+
`passive:true` is mailbox-only and never wakes. Acceptance means transport/broker
|
|
132
|
+
admission, not native journal persistence, model start, reply completion or task
|
|
133
|
+
completion. A bare `ok:true` is not an acceptance receipt.
|
|
129
134
|
|
|
130
135
|
The maintained client is `scripts/intercom-remote.mjs`. Copy it unchanged with
|
|
131
136
|
only the CA, client certificate and private key to a private remote directory.
|