@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,24 +1,42 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { isRecord } from "../../shared/unknown.js";
|
|
3
|
-
import { normalizeSessionInfo, } from "../types.js";
|
|
3
|
+
import { isDeliveryGuard, isPublicationReplyHint, isLastMessageQuery, isReceiptQuery, LAST_MESSAGE_PAGE_LIMIT, normalizeSessionInfo, } from "../types.js";
|
|
4
|
+
import { finiteNumber, nonNegativeInteger, optionalLabel, safeLabel } from "../validation.js";
|
|
4
5
|
import { validateIntercomMessageSize, writeMessage } from "./framing.js";
|
|
5
6
|
import { messageSender } from "./snapshots.js";
|
|
6
7
|
import { findSessions } from "./targets.js";
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
8
|
+
export function isNativeReadQuery(value) {
|
|
9
|
+
return isReceiptQuery(value) || isLastMessageQuery(value);
|
|
10
10
|
}
|
|
11
|
-
function
|
|
12
|
-
|
|
11
|
+
function isNativeReceipt(value) {
|
|
12
|
+
if (!isReceiptQuery(value) || !isRecord(value) || !isRecord(value.sender)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return (value.version === 1 &&
|
|
16
|
+
safeLabel(value.sender.peerId) &&
|
|
17
|
+
[value.sender.rootSessionId, value.sender.incarnation].every(optionalLabel) &&
|
|
18
|
+
typeof value.textSha256 === "string" &&
|
|
19
|
+
/^[a-f0-9]{64}$/.test(value.textSha256) &&
|
|
20
|
+
receiptPublication(value) &&
|
|
21
|
+
receiptFacts(value));
|
|
13
22
|
}
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
|
|
17
|
-
(value.
|
|
18
|
-
(value.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
function receiptPublication(value) {
|
|
24
|
+
return (typeof value.delivery === "string" &&
|
|
25
|
+
["queue", "steer", "passive"].includes(value.delivery) &&
|
|
26
|
+
(value.guard === undefined || isDeliveryGuard(value.guard)) &&
|
|
27
|
+
(value.replyHint === undefined || isPublicationReplyHint(value.replyHint)));
|
|
28
|
+
}
|
|
29
|
+
function receiptFacts(value) {
|
|
30
|
+
return (safeLabel(value.checkpointEntryId) &&
|
|
31
|
+
finiteNumber(value.receivedAt) &&
|
|
32
|
+
[value.queued, value.nativeHandoff, value.discarded].every((field) => typeof field === "boolean") &&
|
|
33
|
+
(value.placementEntryId === null || safeLabel(value.placementEntryId)));
|
|
34
|
+
}
|
|
35
|
+
export function isReceiptResult(value) {
|
|
36
|
+
return isRecord(value) && (typeof value.error === "string" || isNativeReceipt(value.receipt));
|
|
37
|
+
}
|
|
38
|
+
export function isNativeReadResult(value) {
|
|
39
|
+
return isReceiptResult(value) || isLastMessageResult(value);
|
|
22
40
|
}
|
|
23
41
|
function pageIdentity(value) {
|
|
24
42
|
return (normalizeSessionInfo(value.session) !== null &&
|
|
@@ -45,7 +63,7 @@ export function isLastMessageResult(value) {
|
|
|
45
63
|
return (isRecord(value) &&
|
|
46
64
|
(typeof value.error === "string" || (pageIdentity(value) && pageOffsets(value))));
|
|
47
65
|
}
|
|
48
|
-
function unavailable(targets, name, pending) {
|
|
66
|
+
function unavailable(targets, name, pending, query) {
|
|
49
67
|
if (targets.length === 0) {
|
|
50
68
|
return "Session not found";
|
|
51
69
|
}
|
|
@@ -55,6 +73,10 @@ function unavailable(targets, name, pending) {
|
|
|
55
73
|
if (!targets[0].lastMessages) {
|
|
56
74
|
return "Recipient does not support last-message reads; reload its extension.";
|
|
57
75
|
}
|
|
76
|
+
if ("messageId" in query &&
|
|
77
|
+
(!targets[0].receiptReads || targets[0].info.rootSessionId !== query.rootSessionId)) {
|
|
78
|
+
return "publication_unavailable";
|
|
79
|
+
}
|
|
58
80
|
if (pending >= 128) {
|
|
59
81
|
return "Too many pending last-message reads; retry after they settle.";
|
|
60
82
|
}
|
|
@@ -64,10 +86,21 @@ function matchesPage(result, pending, fromId) {
|
|
|
64
86
|
if ("error" in result) {
|
|
65
87
|
return true;
|
|
66
88
|
}
|
|
89
|
+
if ("messageId" in pending.query) {
|
|
90
|
+
return ("receipt" in result &&
|
|
91
|
+
result.receipt.messageId === pending.query.messageId &&
|
|
92
|
+
result.receipt.rootSessionId === pending.query.rootSessionId);
|
|
93
|
+
}
|
|
94
|
+
if ("receipt" in result) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return matchesLastPage(result, pending.query, fromId);
|
|
98
|
+
}
|
|
99
|
+
function matchesLastPage(result, query, fromId) {
|
|
67
100
|
return (result.session.id === fromId &&
|
|
68
|
-
result.offset === (
|
|
69
|
-
(
|
|
70
|
-
result.text.length <= (
|
|
101
|
+
result.offset === (query.offset ?? 0) &&
|
|
102
|
+
(query.entryId === undefined || result.entryId === query.entryId) &&
|
|
103
|
+
result.text.length <= (query.limit ?? LAST_MESSAGE_PAGE_LIMIT));
|
|
71
104
|
}
|
|
72
105
|
/** Correlation belongs to the broker, not the requester or the recipient's model queue. */
|
|
73
106
|
export class LastMessageRelay {
|
|
@@ -79,7 +112,7 @@ export class LastMessageRelay {
|
|
|
79
112
|
request(fromId, message) {
|
|
80
113
|
if (typeof message.requestId !== "string" ||
|
|
81
114
|
typeof message.to !== "string" ||
|
|
82
|
-
!
|
|
115
|
+
!isNativeReadQuery(message.query)) {
|
|
83
116
|
throw new Error("Invalid last message request");
|
|
84
117
|
}
|
|
85
118
|
const targets = findSessions(this.sessions, message.to, fromId);
|
|
@@ -88,15 +121,6 @@ export class LastMessageRelay {
|
|
|
88
121
|
if (!from) {
|
|
89
122
|
return;
|
|
90
123
|
}
|
|
91
|
-
const error = unavailable(targets, message.to, this.pending.size);
|
|
92
|
-
if (error !== undefined) {
|
|
93
|
-
writeMessage(from.socket, {
|
|
94
|
-
type: "last_response",
|
|
95
|
-
requestId: message.requestId,
|
|
96
|
-
result: { error },
|
|
97
|
-
});
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
124
|
const token = randomUUID();
|
|
101
125
|
const frame = {
|
|
102
126
|
type: "last_request",
|
|
@@ -104,12 +128,13 @@ export class LastMessageRelay {
|
|
|
104
128
|
from: messageSender(from.info),
|
|
105
129
|
query: message.query,
|
|
106
130
|
};
|
|
107
|
-
const
|
|
108
|
-
|
|
131
|
+
const error = unavailable(targets, message.to, this.pending.size, message.query) ??
|
|
132
|
+
validateIntercomMessageSize(frame)?.message;
|
|
133
|
+
if (error !== undefined) {
|
|
109
134
|
writeMessage(from.socket, {
|
|
110
135
|
type: "last_response",
|
|
111
136
|
requestId: message.requestId,
|
|
112
|
-
result: { error
|
|
137
|
+
result: { error },
|
|
113
138
|
});
|
|
114
139
|
return;
|
|
115
140
|
}
|
|
@@ -138,7 +163,7 @@ export class LastMessageRelay {
|
|
|
138
163
|
return;
|
|
139
164
|
}
|
|
140
165
|
const result = message.result;
|
|
141
|
-
this.finish(message.requestId,
|
|
166
|
+
this.finish(message.requestId, isNativeReadResult(result) && matchesPage(result, pending, fromId)
|
|
142
167
|
? result
|
|
143
168
|
: { error: "Recipient returned an invalid last-message page" });
|
|
144
169
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Old agent-directory namespaces are inspected for migration, never used for routing. */
|
|
2
|
+
export declare function getPreviousBrokerSocketPaths(): readonly string[];
|
|
3
|
+
export declare function isOwnedBrokerSocket(socketPath: string): boolean;
|
|
4
|
+
/** A deployment can explicitly opt out of same-user discovery with a private runtime directory. */
|
|
5
|
+
export declare function getBrokerRuntimeDir(): string;
|
|
6
|
+
/** Read-only child observation endpoints share the runtime policy, not the broker namespace.
|
|
7
|
+
* The endpoint name is a short token digest because the private runtime directory also counts
|
|
8
|
+
* toward the Unix-socket path limit; a full token made nested test/deployment roots overflow. */
|
|
9
|
+
export declare function agentObservationSocketPath(token: string): string;
|
|
10
|
+
export declare function getBrokerSocketPath(): string;
|
|
11
|
+
/** Create or verify an owned, non-symlink, private socket directory. */
|
|
12
|
+
export declare function preparePrivateSocketPath(socketPath: string): string;
|
|
13
|
+
export declare function prepareBrokerSocketPath(): string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { getPiAgentDir } from "../agent-dir.js";
|
|
6
6
|
const MAX_UNIX_SOCKET_PATH_BYTES = 100;
|
|
7
7
|
function agentDigest(agentDir, uid) {
|
|
@@ -32,33 +32,56 @@ export function isOwnedBrokerSocket(socketPath) {
|
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
function defaultRuntimeRoot() {
|
|
36
|
+
if (process.platform !== "android") {
|
|
37
|
+
return "/tmp";
|
|
38
|
+
}
|
|
39
|
+
return process.env.PREFIX === undefined ? tmpdir() : join(process.env.PREFIX, "tmp");
|
|
40
|
+
}
|
|
41
|
+
function runtimeDirectory(socketName, sibling = "") {
|
|
37
42
|
const configured = process.env.PI_INTERCOM_RUNTIME_DIR;
|
|
38
43
|
const uid = process.getuid?.();
|
|
39
44
|
if (uid === undefined) {
|
|
40
45
|
throw new Error("Intercom requires Unix user IDs and Unix domain sockets.");
|
|
41
46
|
}
|
|
42
|
-
const
|
|
47
|
+
const base = configured ?? join(defaultRuntimeRoot(), `pi-intercom-${uid}`);
|
|
48
|
+
const directory = isAbsolute(base) && sibling !== "" ? `${resolve(base)}${sibling}` : base;
|
|
43
49
|
if (!isAbsolute(directory) ||
|
|
44
50
|
directory.includes("\0") ||
|
|
45
|
-
Buffer.byteLength(join(directory,
|
|
51
|
+
Buffer.byteLength(join(directory, socketName)) > MAX_UNIX_SOCKET_PATH_BYTES) {
|
|
46
52
|
throw new Error("PI_INTERCOM_RUNTIME_DIR must be an absolute short Unix socket directory.");
|
|
47
53
|
}
|
|
48
54
|
return directory;
|
|
49
55
|
}
|
|
56
|
+
/** A deployment can explicitly opt out of same-user discovery with a private runtime directory. */
|
|
57
|
+
export function getBrokerRuntimeDir() {
|
|
58
|
+
return runtimeDirectory("broker.sock");
|
|
59
|
+
}
|
|
60
|
+
/** Read-only child observation endpoints share the runtime policy, not the broker namespace.
|
|
61
|
+
* The endpoint name is a short token digest because the private runtime directory also counts
|
|
62
|
+
* toward the Unix-socket path limit; a full token made nested test/deployment roots overflow. */
|
|
63
|
+
export function agentObservationSocketPath(token) {
|
|
64
|
+
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(token)) {
|
|
65
|
+
throw new Error("Invalid actor token.");
|
|
66
|
+
}
|
|
67
|
+
const name = `obs-${createHash("sha256").update(token).digest("hex").slice(0, 16)}.sock`;
|
|
68
|
+
return join(runtimeDirectory(name, "-agents"), name);
|
|
69
|
+
}
|
|
50
70
|
export function getBrokerSocketPath() {
|
|
51
71
|
return join(getBrokerRuntimeDir(), "broker.sock");
|
|
52
72
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
fs.mkdirSync(
|
|
57
|
-
const stat = fs.lstatSync(
|
|
73
|
+
/** Create or verify an owned, non-symlink, private socket directory. */
|
|
74
|
+
export function preparePrivateSocketPath(socketPath) {
|
|
75
|
+
const directory = dirname(socketPath);
|
|
76
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
77
|
+
const stat = fs.lstatSync(directory);
|
|
58
78
|
const uid = process.getuid?.();
|
|
59
79
|
if (!stat.isDirectory() || stat.isSymbolicLink() || (uid !== undefined && stat.uid !== uid)) {
|
|
60
|
-
throw new Error(`Unsafe intercom socket directory: ${
|
|
80
|
+
throw new Error(`Unsafe intercom socket directory: ${directory}`);
|
|
61
81
|
}
|
|
62
|
-
fs.chmodSync(
|
|
82
|
+
fs.chmodSync(directory, 0o700);
|
|
63
83
|
return socketPath;
|
|
64
84
|
}
|
|
85
|
+
export function prepareBrokerSocketPath() {
|
|
86
|
+
return preparePrivateSocketPath(getBrokerSocketPath());
|
|
87
|
+
}
|
|
@@ -73,6 +73,10 @@ export function brokerPidRecord() {
|
|
|
73
73
|
return `${process.pid}\n${identity !== undefined ? `${identity}\n` : ""}`;
|
|
74
74
|
}
|
|
75
75
|
export function isBrokerPidReused(pid, record) {
|
|
76
|
+
return isLinuxProcessReplaced(pid, record.slice(record.indexOf("\n") + 1));
|
|
77
|
+
}
|
|
78
|
+
/** True only when a live Linux PID provably names a different process than the identity. */
|
|
79
|
+
export function isLinuxProcessReplaced(pid, identity) {
|
|
76
80
|
const current = readLinuxProcess(pid);
|
|
77
81
|
if (!current) {
|
|
78
82
|
return false;
|
|
@@ -80,7 +84,7 @@ export function isBrokerPidReused(pid, record) {
|
|
|
80
84
|
if (current.tgid !== pid) {
|
|
81
85
|
return true;
|
|
82
86
|
}
|
|
83
|
-
const saved = IDENTITY.exec(
|
|
87
|
+
const saved = IDENTITY.exec(identity.trim());
|
|
84
88
|
const actual = IDENTITY.exec(current.identity ?? "");
|
|
85
89
|
if (!saved || !actual) {
|
|
86
90
|
return false;
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { createMessageReader } from "./framing.js";
|
|
3
|
+
import { assertMainParent } from "./targets.js";
|
|
4
|
+
import { isSessionRegistration } from "../types.js";
|
|
5
|
+
export function registeredSessionInfo(msg, sessions) {
|
|
6
|
+
if (!isSessionRegistration(msg.session)) {
|
|
7
|
+
throw new Error("Invalid register message");
|
|
8
|
+
}
|
|
9
|
+
assertMainParent(msg.session, sessions);
|
|
10
|
+
const requestedId = typeof msg.requestedId === "string" && /^[a-zA-Z0-9_-]{8,80}$/.test(msg.requestedId)
|
|
11
|
+
? msg.requestedId
|
|
12
|
+
: undefined;
|
|
13
|
+
const id = requestedId !== undefined && !sessions.has(requestedId) ? requestedId : randomUUID();
|
|
14
|
+
return {
|
|
15
|
+
...msg.session,
|
|
16
|
+
id,
|
|
17
|
+
guardedDelivery: msg.guardedDelivery === 1 ? msg.session.guardedDelivery : undefined,
|
|
18
|
+
lastSeen: msg.session.lastSeen ?? Date.now(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
2
21
|
/** A server connection owns its registration identity until the socket closes. */
|
|
3
22
|
export function acceptBrokerConnection(socket, handleMessage, disconnected) {
|
|
4
23
|
let sessionId = null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type SessionSnapshot } from "../types.ts";
|
|
2
|
+
/** Multipart frames build a private cache; only a complete validated snapshot escapes. */
|
|
3
|
+
export declare class SnapshotReader {
|
|
4
|
+
private readonly sessions;
|
|
5
|
+
private readonly receipts;
|
|
6
|
+
private addSession;
|
|
7
|
+
append(sessions: readonly unknown[], receipts: unknown): void;
|
|
8
|
+
finish(): SessionSnapshot;
|
|
9
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type SessionInfo, type TopicUpdate, type Message, type BrokerMessage, type TopicSubscription, type SendResult } from "../types.ts";
|
|
2
|
+
/** Direct messages and old clients receive identity, not potentially large topic state. */
|
|
3
|
+
export declare function messageSender(info: SessionInfo): SessionInfo;
|
|
4
|
+
export declare function snapshotFrames(requestId: string, infos: readonly SessionInfo[], stream: boolean): BrokerMessage[];
|
|
5
|
+
interface PreparedTopicChange {
|
|
6
|
+
readonly topic?: TopicUpdate;
|
|
7
|
+
readonly subscription?: TopicSubscription;
|
|
8
|
+
readonly unsubscribe?: string;
|
|
9
|
+
readonly publication?: TopicUpdate;
|
|
10
|
+
readonly restore: boolean;
|
|
11
|
+
readonly changed: boolean;
|
|
12
|
+
readonly bytes: number;
|
|
13
|
+
}
|
|
14
|
+
/** Keep committed records indexed; restores validate only new bytes and cached envelope maxima. */
|
|
15
|
+
export declare class TopicSnapshot {
|
|
16
|
+
private readonly topics;
|
|
17
|
+
private readonly subscriptions;
|
|
18
|
+
constructor(info: SessionInfo);
|
|
19
|
+
subscription(topic: string): TopicSubscription | undefined;
|
|
20
|
+
prepare(change: unknown, supported: boolean): PreparedTopicChange;
|
|
21
|
+
private preparePublication;
|
|
22
|
+
snapshot(info: SessionInfo, prepared?: PreparedTopicChange): SessionInfo;
|
|
23
|
+
validateRestore(info: SessionInfo, prepared: PreparedTopicChange, requestId: string): void;
|
|
24
|
+
commit(prepared: PreparedTopicChange): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function deliveryAcknowledgement(receipt: SendResult): BrokerMessage;
|
|
27
|
+
export declare function shouldReplaceDelivery(message: Message, info: SessionInfo): boolean;
|
|
28
|
+
export declare function publicationMessage(publication: TopicUpdate, awaitRelease: boolean): Message;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SessionInfo } from "../types.ts";
|
|
2
|
+
export declare function canSeeSession(from: SessionInfo, target: SessionInfo): boolean;
|
|
3
|
+
export declare function assertMainParent(session: Omit<SessionInfo, "id">, sessions: ReadonlyMap<string, {
|
|
4
|
+
readonly info: SessionInfo;
|
|
5
|
+
}>): void;
|
|
6
|
+
/** Preserve exact-ID precedence and case-insensitive duplicate-name ambiguity. */
|
|
7
|
+
export declare function findSessions<T extends {
|
|
8
|
+
readonly info: SessionInfo;
|
|
9
|
+
}>(sessions: ReadonlyMap<string, T>, nameOrId: string, fromId: string): T[];
|
|
@@ -2,6 +2,7 @@ import { IntercomClient } from "./broker/client.js";
|
|
|
2
2
|
import { spawnBrokerIfNeeded } from "./broker/spawn.js";
|
|
3
3
|
import { readLastMessage } from "./last-message.js";
|
|
4
4
|
import { asError } from "./validation.js";
|
|
5
|
+
import { RecoveryError } from "./tool-results.js";
|
|
5
6
|
import { formatTargetOptions, resolveSessionTarget, nativePeerId } from "./session-targets.js";
|
|
6
7
|
/** Owns socket registration, coalesced connects and the reconnect/startup timers. */
|
|
7
8
|
export class IntercomConnection {
|
|
@@ -93,6 +94,7 @@ export class IntercomConnection {
|
|
|
93
94
|
this.client.updatePresence({
|
|
94
95
|
status: this.lifecycle.status(),
|
|
95
96
|
...this.lifecycle.health(this.hooks.pendingAsks()),
|
|
97
|
+
applicationGuard: this.lifecycle.snapshot().applicationGuard ?? null,
|
|
96
98
|
});
|
|
97
99
|
}
|
|
98
100
|
}
|
|
@@ -110,6 +112,10 @@ export class IntercomConnection {
|
|
|
110
112
|
return;
|
|
111
113
|
}
|
|
112
114
|
try {
|
|
115
|
+
if ("messageId" in request.query) {
|
|
116
|
+
next.respondLast(request.requestId, this.hooks.receipt(request.query));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
113
119
|
next.respondLast(request.requestId, readLastMessage(ctx.sessionManager, {
|
|
114
120
|
id,
|
|
115
121
|
...this.lifecycle.identity(ctx.sessionManager.getSessionId()),
|
|
@@ -143,6 +149,7 @@ export class IntercomConnection {
|
|
|
143
149
|
}
|
|
144
150
|
this.hooks.disconnected(error);
|
|
145
151
|
this.client = null;
|
|
152
|
+
this.lifecycle.transport(null);
|
|
146
153
|
this.clearReconnect();
|
|
147
154
|
this.scheduleReconnect();
|
|
148
155
|
});
|
|
@@ -161,6 +168,7 @@ export class IntercomConnection {
|
|
|
161
168
|
throw new Error("Intercom runtime no longer active");
|
|
162
169
|
}
|
|
163
170
|
this.client = next;
|
|
171
|
+
this.lifecycle.transport(next.sessionId, next.supportsGuardedDelivery);
|
|
164
172
|
this.syncIdentity(ctx.sessionManager.getSessionId());
|
|
165
173
|
this.attempt = 0;
|
|
166
174
|
return next;
|
|
@@ -239,10 +247,10 @@ export async function resolveConnectedTarget(active, name) {
|
|
|
239
247
|
return resolution.target.id;
|
|
240
248
|
}
|
|
241
249
|
if (resolution.status === "ambiguous") {
|
|
242
|
-
throw new
|
|
250
|
+
throw new RecoveryError("ambiguous_target", `Target "${name}" matches multiple sessions. Use one of these targets: ${formatTargetOptions(resolution.matches, sessions)}.`);
|
|
243
251
|
}
|
|
244
252
|
if (resolution.status === "prefix_too_short") {
|
|
245
|
-
throw new
|
|
253
|
+
throw new RecoveryError("ambiguous_target", `Target "${name}" is too short. Use the displayed target from intercom list, such as ${formatTargetOptions(resolution.matches, sessions)}.`);
|
|
246
254
|
}
|
|
247
255
|
return null;
|
|
248
256
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
// Launches set both variables to the parent's exact native session ID. Absence means a main;
|
|
2
|
+
// anything else is refused, never normalized into a different identity.
|
|
1
3
|
export function readIntercomLineage() {
|
|
2
|
-
const rootSessionId = process.env.PI_INTERCOM_ROOT_SESSION_ID
|
|
3
|
-
const parentSessionId = process.env.PI_INTERCOM_PARENT_SESSION_ID
|
|
4
|
+
const rootSessionId = process.env.PI_INTERCOM_ROOT_SESSION_ID;
|
|
5
|
+
const parentSessionId = process.env.PI_INTERCOM_PARENT_SESSION_ID;
|
|
4
6
|
if (rootSessionId === undefined && parentSessionId === undefined) {
|
|
5
7
|
return;
|
|
6
8
|
}
|
|
7
9
|
if (rootSessionId === undefined ||
|
|
8
|
-
parentSessionId === undefined ||
|
|
9
10
|
rootSessionId === "" ||
|
|
10
|
-
|
|
11
|
+
rootSessionId !== rootSessionId.trim() ||
|
|
11
12
|
rootSessionId !== parentSessionId) {
|
|
12
|
-
throw new Error("
|
|
13
|
+
throw new Error("Child launch requires identical, unpadded root and parent native session IDs.");
|
|
13
14
|
}
|
|
14
15
|
return { rootSessionId, parentSessionId };
|
|
15
16
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { requestedDelivery } from "./inbound-record.js";
|
|
3
|
-
import { isRecord } from "./validation.js";
|
|
4
2
|
import { runCooperatively } from "../shared/cooperative.js";
|
|
5
3
|
/** Owns cooperative inbound batches, restoration and the flush timer. */
|
|
6
4
|
export class InboundDeliveryOwner {
|
|
@@ -83,6 +81,9 @@ export class InboundDeliveryOwner {
|
|
|
83
81
|
this.timer.unref();
|
|
84
82
|
}
|
|
85
83
|
queue(entry, flushDelivery = "auto", delayMs = 200) {
|
|
84
|
+
if (!this.journal.owns(entry)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
86
87
|
this.journal.remember(entry, "queued", flushDelivery);
|
|
87
88
|
this.hooks.syncStatus();
|
|
88
89
|
this.schedule(delayMs);
|
|
@@ -112,8 +113,7 @@ export class InboundDeliveryOwner {
|
|
|
112
113
|
}
|
|
113
114
|
triggerIndex(entries) {
|
|
114
115
|
const canTrigger = (entry) => entry.flushDelivery !== "passive" && requestedDelivery(entry.message) !== "passive";
|
|
115
|
-
|
|
116
|
-
return ask === -1 ? entries.findIndex(canTrigger) : ask;
|
|
116
|
+
return entries.findLastIndex(canTrigger);
|
|
117
117
|
}
|
|
118
118
|
*sendTriggerLast(entries, generation) {
|
|
119
119
|
const passives = entries.filter((entry) => entry.flushDelivery === "passive");
|
|
@@ -124,14 +124,15 @@ export class InboundDeliveryOwner {
|
|
|
124
124
|
}
|
|
125
125
|
const index = this.triggerIndex(rest);
|
|
126
126
|
const trigger = index === -1 ? undefined : rest.splice(index, 1)[0];
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
const replyTarget = entries.find((entry) => entry.message.expectsReply === true) ?? trigger;
|
|
128
|
+
if (replyTarget && this.journal.get(replyTarget.message.id) === replyTarget) {
|
|
129
|
+
this.hooks.replies.queueTurnContext({ ...replyTarget, receivedAt: Date.now() });
|
|
129
130
|
}
|
|
130
131
|
for (const entry of rest) {
|
|
131
132
|
if (!this.live(generation)) {
|
|
132
133
|
return;
|
|
133
134
|
}
|
|
134
|
-
this.send(entry, entry.
|
|
135
|
+
this.send(entry, requestedDelivery(entry.message) === "queue" ? "followUp" : "steer", generation);
|
|
135
136
|
yield;
|
|
136
137
|
}
|
|
137
138
|
if (trigger) {
|
|
@@ -150,7 +151,7 @@ export class InboundDeliveryOwner {
|
|
|
150
151
|
live(generation) {
|
|
151
152
|
return this.lifecycle.generation === generation && this.lifecycle.live() !== null;
|
|
152
153
|
}
|
|
153
|
-
*sendSteers(entries, generation
|
|
154
|
+
*sendSteers(entries, generation) {
|
|
154
155
|
for (const entry of entries) {
|
|
155
156
|
if (!this.live(generation)) {
|
|
156
157
|
return;
|
|
@@ -159,9 +160,6 @@ export class InboundDeliveryOwner {
|
|
|
159
160
|
continue;
|
|
160
161
|
}
|
|
161
162
|
this.send(entry, "steer", generation);
|
|
162
|
-
if (requestedDelivery(entry.message) === "steer") {
|
|
163
|
-
onDetach(this.detach(entry, entry.message.expectsReply !== true));
|
|
164
|
-
}
|
|
165
163
|
yield;
|
|
166
164
|
}
|
|
167
165
|
}
|
|
@@ -184,13 +182,7 @@ export class InboundDeliveryOwner {
|
|
|
184
182
|
this.schedule(500);
|
|
185
183
|
return;
|
|
186
184
|
}
|
|
187
|
-
return this.runBatch(
|
|
188
|
-
const detachments = [];
|
|
189
|
-
await runCooperatively(this.sendSteers(steers, generation, (task) => {
|
|
190
|
-
detachments.push(task);
|
|
191
|
-
}));
|
|
192
|
-
await Promise.all(detachments);
|
|
193
|
-
}, generation);
|
|
185
|
+
return this.runBatch(() => runCooperatively(this.sendSteers(steers, generation)), generation);
|
|
194
186
|
}
|
|
195
187
|
messageOptions(delivery) {
|
|
196
188
|
if (delivery === "trigger") {
|
|
@@ -229,56 +221,20 @@ export class InboundDeliveryOwner {
|
|
|
229
221
|
}
|
|
230
222
|
}
|
|
231
223
|
nativeMessage(entry) {
|
|
232
|
-
const sender = entry.from.name
|
|
233
|
-
? entry.from.id.slice(0, 8)
|
|
234
|
-
: entry.from.name;
|
|
224
|
+
const sender = entry.from.name ?? entry.from.id.slice(0, 8);
|
|
235
225
|
const command = entry.replyCommand ?? "";
|
|
236
226
|
const reply = command === "" ? "" : `\n\nTo reply, use the intercom tool: ${command}`;
|
|
227
|
+
const hint = entry.message.replyHint;
|
|
228
|
+
const publication = hint
|
|
229
|
+
? `\n\nTo publish a correlated reply, use ${hint.tool} with commandId ${hint.commandId}. This is not an intercom ask.`
|
|
230
|
+
: "";
|
|
237
231
|
return {
|
|
238
232
|
customType: "intercom_message",
|
|
239
|
-
content: `**📨 From ${sender}** (Native session cwd: ${entry.from.cwd})${reply}\n\n${entry.bodyText}`,
|
|
233
|
+
content: `**📨 From ${sender}** (Native session cwd: ${entry.from.cwd})${reply}${publication}\n\n${entry.bodyText}`,
|
|
240
234
|
display: true,
|
|
241
235
|
details: { ...entry, bodyText: entry.bodyText },
|
|
242
236
|
};
|
|
243
237
|
}
|
|
244
|
-
async detach(entry, attention = false) {
|
|
245
|
-
if (!attention && entry.message.expectsReply !== true) {
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
248
|
-
const requestId = randomUUID();
|
|
249
|
-
return await new Promise((resolve) => {
|
|
250
|
-
let settled = false;
|
|
251
|
-
const finish = (accepted) => {
|
|
252
|
-
if (settled) {
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
settled = true;
|
|
256
|
-
clearTimeout(timer);
|
|
257
|
-
unsubscribe();
|
|
258
|
-
resolve(accepted);
|
|
259
|
-
};
|
|
260
|
-
const timer = setTimeout(() => finish(false), 500);
|
|
261
|
-
timer.unref();
|
|
262
|
-
const unsubscribe = this.pi.events.on("pi-intercom:detach-response", (payload) => {
|
|
263
|
-
if (isRecord(payload) && payload.requestId === requestId) {
|
|
264
|
-
finish(payload.accepted === true);
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
try {
|
|
268
|
-
this.pi.events.emit("pi-intercom:detach-request", {
|
|
269
|
-
requestId,
|
|
270
|
-
...(attention ? { reason: "attention" } : {}),
|
|
271
|
-
});
|
|
272
|
-
// Owned waits answer ordinary attention synchronously; an unanswered probe completes now.
|
|
273
|
-
if (attention) {
|
|
274
|
-
finish(false);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
finish(false);
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
238
|
async redeliver(ctx, generation) {
|
|
283
239
|
if (this.batchTask) {
|
|
284
240
|
await this.batchTask;
|