@fitchmultz/pi-subagents 0.46.1 → 0.47.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 +29 -0
- package/README.md +1 -1
- package/dist/agents/agent-management.js +8 -2
- package/dist/agents/agents.js +8 -8
- package/dist/extension/build-info.js +1 -1
- package/dist/extension/index.js +11 -5
- package/dist/extension/result-snapshot.js +0 -18
- package/dist/history/ingest.js +40 -31
- package/dist/history/prune.js +1 -1
- package/dist/history/run-projection.js +1 -2
- package/dist/history/search-queries.js +8 -2
- package/dist/history/store.js +20 -4
- package/dist/history/text-chunks.js +1 -2
- package/dist/history/transcript-queries.js +12 -6
- package/dist/intercom/result-intercom.js +0 -74
- package/dist/pi-intercom/bridge-config.js +35 -26
- package/dist/pi-intercom/bridge-files.js +47 -0
- package/dist/pi-intercom/bridge-mailbox.js +324 -0
- package/dist/pi-intercom/bridge-presence.js +100 -0
- package/dist/pi-intercom/bridge-routes.js +27 -3
- package/dist/pi-intercom/bridge-session.js +60 -64
- package/dist/pi-intercom/bridge-wake.js +158 -0
- package/dist/pi-intercom/bridge.js +55 -7
- package/dist/pi-intercom/broker/broker.js +33 -16
- package/dist/pi-intercom/broker/framing.js +67 -17
- package/dist/pi-intercom/broker/snapshot-reader.js +29 -8
- package/dist/pi-intercom/broker/snapshots.js +144 -41
- package/dist/pi-intercom/session-targets.js +0 -1
- package/dist/runs/background/async-run-discovery.js +4 -6
- package/dist/runs/background/completion-dedupe.js +4 -55
- package/dist/runs/background/notify.js +1 -9
- package/dist/runs/background/result-watcher.js +1 -1
- package/dist/runs/background/runner-child-observer.js +12 -7
- package/dist/runs/background/runner-monitor.js +2 -0
- package/dist/runs/background/runner-parallel.js +2 -3
- package/dist/runs/shared/child-observation-spool.js +8 -3
- package/dist/runs/shared/run-restoration.js +4 -5
- package/dist/runs/shared/supervisor-questions.js +7 -4
- package/dist/shared/journal-frames.js +1 -1
- package/dist/slash/command-discovery.js +13 -12
- package/dist/tui/agent-history.js +0 -14
- package/dist/tui/agent-view.js +0 -1
- package/dist/tui/conversation-history.js +10 -1
- package/dist/tui/history-cards.js +8 -22
- package/docs/intercom-bridge.md +39 -17
- package/docs/intercom.md +5 -1
- package/docs/performance.md +72 -3
- package/package.json +1 -1
- package/scripts/intercom-bridge-credentials.mjs +54 -12
- package/scripts/intercom-remote.mjs +25 -1
- package/scripts/package-smoke.mjs +1 -1
- package/src/agents/agent-management.ts +8 -4
- package/src/agents/agents.ts +17 -12
- package/src/extension/index.ts +12 -5
- package/src/extension/result-snapshot.ts +1 -27
- package/src/history/ingest.ts +57 -49
- package/src/history/prune.ts +1 -1
- package/src/history/run-projection.ts +2 -4
- package/src/history/search-queries.ts +9 -2
- package/src/history/store.ts +21 -5
- package/src/history/text-chunks.ts +7 -2
- package/src/history/transcript-queries.ts +16 -10
- package/src/intercom/result-intercom.ts +0 -103
- package/src/pi-intercom/bridge-config.ts +46 -36
- package/src/pi-intercom/bridge-files.ts +59 -0
- package/src/pi-intercom/bridge-mailbox.ts +395 -0
- package/src/pi-intercom/bridge-presence.ts +128 -0
- package/src/pi-intercom/bridge-routes.ts +41 -4
- package/src/pi-intercom/bridge-session.ts +73 -71
- package/src/pi-intercom/bridge-wake.ts +188 -0
- package/src/pi-intercom/bridge.ts +58 -7
- package/src/pi-intercom/broker/broker.ts +34 -20
- package/src/pi-intercom/broker/framing.ts +71 -20
- package/src/pi-intercom/broker/snapshot-reader.ts +47 -9
- package/src/pi-intercom/broker/snapshots.ts +184 -51
- package/src/pi-intercom/session-targets.ts +0 -2
- package/src/runs/background/async-run-discovery.ts +4 -10
- package/src/runs/background/completion-dedupe.ts +6 -69
- package/src/runs/background/notify.ts +2 -12
- package/src/runs/background/result-watcher.ts +1 -1
- package/src/runs/background/runner-child-observer.ts +19 -11
- package/src/runs/background/runner-monitor.ts +2 -0
- package/src/runs/background/runner-parallel.ts +2 -3
- package/src/runs/shared/child-observation-spool.ts +8 -3
- package/src/runs/shared/run-restoration.ts +4 -4
- package/src/runs/shared/supervisor-questions.ts +10 -10
- package/src/shared/journal-frames.ts +1 -1
- package/src/slash/command-discovery.ts +13 -12
- package/src/tui/agent-history.ts +1 -18
- package/src/tui/agent-view.ts +0 -1
- package/src/tui/conversation-history.ts +15 -1
- package/src/tui/history-cards.ts +8 -26
|
@@ -22,13 +22,16 @@ function errorMessage(error) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function help() {
|
|
25
|
-
console.log(`Usage: node scripts/intercom-bridge-credentials.mjs <init|issue|revoke> DIR [NAME] [--cwd PATH]
|
|
25
|
+
console.log(`Usage: node scripts/intercom-bridge-credentials.mjs <init|issue|revoke|renew-server> DIR [NAME] [--cwd PATH] [--port N] [--days N]
|
|
26
26
|
|
|
27
27
|
init Create a new private CA, loopback server certificate and grok-bot credential.
|
|
28
|
-
issue Issue a fresh
|
|
28
|
+
issue Issue a fresh client certificate; preserve local identity policies.
|
|
29
29
|
revoke Remove the named client from the allowlist. Does not delete saved credentials.
|
|
30
|
+
renew-server Renew the loopback server key/certificate using the same CA; preserve clients.
|
|
30
31
|
|
|
31
32
|
Options: --cwd PATH Local project identity for init/issue (default: current directory)
|
|
33
|
+
--port N Loopback bridge port for init (default: 9443)
|
|
34
|
+
--days N Certificate validity (1–3650; client default: 7, renew-server: 365)
|
|
32
35
|
-h, --help Show help
|
|
33
36
|
Environment: OPENSSL OpenSSL executable (default: openssl; requires -addext support)
|
|
34
37
|
|
|
@@ -38,6 +41,7 @@ Examples:
|
|
|
38
41
|
node scripts/intercom-bridge-credentials.mjs revoke ~/.pi/intercom-bridge grok-bot
|
|
39
42
|
|
|
40
43
|
After issue/revoke, send SIGHUP to the helper PID to activate the allowlist.
|
|
44
|
+
After renew-server, restart only the helper; existing client credentials remain valid.
|
|
41
45
|
Rotation/revocation stops that identity's helper-launched Pi children; journals remain.
|
|
42
46
|
Concurrent credential commands fail rather than overwrite each other's authorization.
|
|
43
47
|
After a crashed command, inspect DIR/.credentials.lock/owner and remove that lock
|
|
@@ -80,7 +84,7 @@ function key(path) {
|
|
|
80
84
|
privatePath(path);
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
function issueCertificate(dir, stem, name, server = false) {
|
|
87
|
+
function issueCertificate(dir, stem, name, { server = false, days = 7 } = {}) {
|
|
84
88
|
const keyPath = join(dir, `${stem}.key`);
|
|
85
89
|
const certPath = join(dir, `${stem}.crt`);
|
|
86
90
|
const csrPath = join(dir, `${stem}.csr`);
|
|
@@ -105,7 +109,7 @@ function issueCertificate(dir, stem, name, server = false) {
|
|
|
105
109
|
"-set_serial",
|
|
106
110
|
`0x${randomBytes(16).toString("hex")}`,
|
|
107
111
|
"-days",
|
|
108
|
-
|
|
112
|
+
String(days),
|
|
109
113
|
"-sha256",
|
|
110
114
|
"-extfile",
|
|
111
115
|
extPath,
|
|
@@ -145,23 +149,41 @@ function readConfig(path) {
|
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
let args;
|
|
152
|
+
let port;
|
|
153
|
+
let certificateDays;
|
|
148
154
|
try {
|
|
149
155
|
args = parseArgs({
|
|
150
156
|
allowPositionals: true,
|
|
151
|
-
options: {
|
|
157
|
+
options: {
|
|
158
|
+
help: { type: "boolean", short: "h" },
|
|
159
|
+
cwd: { type: "string" },
|
|
160
|
+
port: { type: "string" },
|
|
161
|
+
days: { type: "string" },
|
|
162
|
+
},
|
|
152
163
|
});
|
|
153
164
|
if (args.values.help) {
|
|
154
165
|
help();
|
|
155
166
|
process.exit(0);
|
|
156
167
|
}
|
|
157
168
|
const [action, dir, name] = args.positionals;
|
|
169
|
+
port = args.values.port === undefined ? 9443 : Number(args.values.port);
|
|
170
|
+
const defaultDays = action === "renew-server" ? 365 : 7;
|
|
171
|
+
certificateDays = args.values.days === undefined ? defaultDays : Number(args.values.days);
|
|
158
172
|
if (
|
|
159
|
-
!["init", "issue", "revoke"].includes(action) ||
|
|
173
|
+
!["init", "issue", "revoke", "renew-server"].includes(action) ||
|
|
160
174
|
!dir ||
|
|
161
175
|
args.positionals.length > 3 ||
|
|
162
|
-
(
|
|
176
|
+
(["issue", "revoke"].includes(action) && !name) ||
|
|
177
|
+
(action === "renew-server" && (name || args.values.cwd)) ||
|
|
163
178
|
(name && !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name)) ||
|
|
164
|
-
(action === "revoke" && args.values.cwd)
|
|
179
|
+
(action === "revoke" && (args.values.cwd || args.values.days)) ||
|
|
180
|
+
(action !== "init" && args.values.port !== undefined) ||
|
|
181
|
+
!Number.isInteger(port) ||
|
|
182
|
+
port < 0 ||
|
|
183
|
+
port > 65535 ||
|
|
184
|
+
!Number.isInteger(certificateDays) ||
|
|
185
|
+
certificateDays < 1 ||
|
|
186
|
+
certificateDays > 3650
|
|
165
187
|
) {
|
|
166
188
|
throw new Error("Invalid arguments; use --help.");
|
|
167
189
|
}
|
|
@@ -217,9 +239,9 @@ try {
|
|
|
217
239
|
"-out",
|
|
218
240
|
join(dir, "ca.crt"),
|
|
219
241
|
);
|
|
220
|
-
const server = issueCertificate(dir, "server", "localhost", true);
|
|
242
|
+
const server = issueCertificate(dir, "server", "localhost", { server: true, days: 365 });
|
|
221
243
|
config = {
|
|
222
|
-
port
|
|
244
|
+
port,
|
|
223
245
|
ca: join(dir, "ca.crt"),
|
|
224
246
|
cert: server.cert,
|
|
225
247
|
key: server.key,
|
|
@@ -239,16 +261,36 @@ try {
|
|
|
239
261
|
console.log(
|
|
240
262
|
JSON.stringify({ revoked: name, config: configPath, next: "Send SIGHUP to the helper PID." }),
|
|
241
263
|
);
|
|
264
|
+
} else if (action === "renew-server") {
|
|
265
|
+
privatePath(join(dir, "ca.key"));
|
|
266
|
+
const server = issueCertificate(dir, `server-${randomBytes(8).toString("hex")}`, "localhost", {
|
|
267
|
+
server: true,
|
|
268
|
+
days: certificateDays,
|
|
269
|
+
});
|
|
270
|
+
config.cert = server.cert;
|
|
271
|
+
config.key = server.key;
|
|
272
|
+
atomicConfig(configPath, config);
|
|
273
|
+
console.log(
|
|
274
|
+
JSON.stringify({
|
|
275
|
+
renewed: "server",
|
|
276
|
+
...server,
|
|
277
|
+
ca: config.ca,
|
|
278
|
+
config: configPath,
|
|
279
|
+
next: "Restart only this helper; client keys and CA are unchanged.",
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
242
282
|
} else {
|
|
243
283
|
privatePath(join(dir, "ca.key"));
|
|
244
|
-
const credential = issueCertificate(dir, `${name}-${randomBytes(8).toString("hex")}`, name
|
|
284
|
+
const credential = issueCertificate(dir, `${name}-${randomBytes(8).toString("hex")}`, name, {
|
|
285
|
+
days: certificateDays,
|
|
286
|
+
});
|
|
245
287
|
const previous = config.clients.find((client) => client.name === name);
|
|
246
288
|
config.clients = config.clients.filter((client) => client.name !== name);
|
|
247
289
|
config.clients.push({
|
|
290
|
+
...previous,
|
|
248
291
|
fingerprint256: credential.fingerprint256,
|
|
249
292
|
name,
|
|
250
293
|
cwd: resolve(args.values.cwd || previous?.cwd || process.cwd()),
|
|
251
|
-
...(previous?.launch === undefined ? {} : { launch: previous.launch }),
|
|
252
294
|
});
|
|
253
295
|
atomicConfig(configPath, config);
|
|
254
296
|
console.log(
|
|
@@ -17,6 +17,8 @@ Options:
|
|
|
17
17
|
--message TEXT Message for send/ask/reply
|
|
18
18
|
--reply-to ID Inbound ask ID from inbox, for reply
|
|
19
19
|
--timeout-ms MS Ask timeout (100..120000, default: 120000)
|
|
20
|
+
--after CURSOR Inbox cursor from nextCursor (default: 0)
|
|
21
|
+
--limit COUNT Inbox page size (1..256, default: 256)
|
|
20
22
|
--name NAME Start/reuse a named idle Pi session under local policy
|
|
21
23
|
--session-id ID Stop only a Pi session launched by this identity/helper
|
|
22
24
|
-h, --help Show help
|
|
@@ -25,6 +27,7 @@ Examples:
|
|
|
25
27
|
node intercom-remote.mjs list
|
|
26
28
|
node intercom-remote.mjs ask --to planner --message 'What is the current status?'
|
|
27
29
|
node intercom-remote.mjs inbox
|
|
30
|
+
node intercom-remote.mjs inbox --after NEXT_CURSOR --limit 64
|
|
28
31
|
node intercom-remote.mjs reply --reply-to QUESTION_ID --message 'Proceed.'
|
|
29
32
|
node intercom-remote.mjs ack MESSAGE_ID
|
|
30
33
|
node intercom-remote.mjs start --name planner
|
|
@@ -36,6 +39,9 @@ No automatic retries: a lost response does not prove a message was unsent.`);
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
function allowedOptions(action) {
|
|
42
|
+
if (action === "inbox") {
|
|
43
|
+
return ["after", "limit"];
|
|
44
|
+
}
|
|
39
45
|
if (action === "start") {
|
|
40
46
|
return ["name"];
|
|
41
47
|
}
|
|
@@ -119,7 +125,21 @@ function privateKey(path) {
|
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
|
|
122
|
-
|
|
128
|
+
function inboxQuery(options) {
|
|
129
|
+
const after = options.after ?? "0";
|
|
130
|
+
const limit = options.limit ?? "256";
|
|
131
|
+
if (
|
|
132
|
+
!/^(0|[1-9]\d{0,15})$/.test(after) ||
|
|
133
|
+
!Number.isSafeInteger(Number(after)) ||
|
|
134
|
+
!/^[1-9]\d{0,2}$/.test(limit) ||
|
|
135
|
+
Number(limit) > 256
|
|
136
|
+
) {
|
|
137
|
+
throw new Error("--after must be a nonnegative safe integer cursor; --limit must be 1..256.");
|
|
138
|
+
}
|
|
139
|
+
return new URLSearchParams({ after, limit }).toString();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let body, action, origin, timeout, query;
|
|
123
143
|
try {
|
|
124
144
|
const parsed = parseArgs({
|
|
125
145
|
allowPositionals: true,
|
|
@@ -129,6 +149,8 @@ try {
|
|
|
129
149
|
message: { type: "string" },
|
|
130
150
|
"reply-to": { type: "string" },
|
|
131
151
|
"timeout-ms": { type: "string" },
|
|
152
|
+
after: { type: "string" },
|
|
153
|
+
limit: { type: "string" },
|
|
132
154
|
name: { type: "string" },
|
|
133
155
|
"session-id": { type: "string" },
|
|
134
156
|
},
|
|
@@ -167,6 +189,7 @@ try {
|
|
|
167
189
|
throw new Error("--timeout-ms must be 100..120000.");
|
|
168
190
|
}
|
|
169
191
|
body = requestBody(action, options, parsed.positionals.slice(1), timeout);
|
|
192
|
+
query = action === "inbox" ? inboxQuery(options) : "";
|
|
170
193
|
origin = new URL(process.env.INTERCOM_BRIDGE_URL || "https://127.0.0.1:9443");
|
|
171
194
|
if (
|
|
172
195
|
origin.protocol !== "https:" ||
|
|
@@ -194,6 +217,7 @@ try {
|
|
|
194
217
|
try {
|
|
195
218
|
const files = tlsFiles();
|
|
196
219
|
origin.pathname = `/v1/${action}`;
|
|
220
|
+
origin.search = query;
|
|
197
221
|
const payload = JSON.stringify(body);
|
|
198
222
|
const get = ["list", "inbox", "sessions"].includes(action);
|
|
199
223
|
const result = await new Promise((resolve, reject) => {
|
|
@@ -200,7 +200,7 @@ function installProduction(packed) {
|
|
|
200
200
|
);
|
|
201
201
|
if (
|
|
202
202
|
!credentialHelp.includes(
|
|
203
|
-
"Usage: node scripts/intercom-bridge-credentials.mjs <init|issue|revoke>",
|
|
203
|
+
"Usage: node scripts/intercom-bridge-credentials.mjs <init|issue|revoke|renew-server>",
|
|
204
204
|
)
|
|
205
205
|
) {
|
|
206
206
|
throw new Error("installed credential CLI did not provide usage help");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { discoverAgentsAll } from "./agents.ts";
|
|
2
|
+
import { mergeAgents } from "./agent-selection.ts";
|
|
2
3
|
import { formatAgentGroups, formatAgentDetail, formatChainDetail } from "./management-format.ts";
|
|
3
4
|
import { handleCreate, handleUpdate, handleDelete } from "./management-persistence.ts";
|
|
4
5
|
import {
|
|
@@ -24,9 +25,12 @@ export function handleList(
|
|
|
24
25
|
return result("agentScope must be 'user', 'project', or 'both'.", true);
|
|
25
26
|
}
|
|
26
27
|
const discovery = discoverAgentsAll(ctx.cwd, discoveryOptions(ctx), scope);
|
|
27
|
-
const agents =
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const agents = mergeAgents(discovery.user, discovery.project, [
|
|
29
|
+
...discovery.builtin,
|
|
30
|
+
...discovery.package,
|
|
31
|
+
])
|
|
32
|
+
.filter((agent) => agent.disabled !== true)
|
|
33
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
30
34
|
const chains = discovery.chains
|
|
31
35
|
.filter((chain) => scope === "both" || chain.source === scope)
|
|
32
36
|
.sort((a, b) => a.name.localeCompare(b.name));
|
package/src/agents/agents.ts
CHANGED
|
@@ -124,17 +124,26 @@ function loadChainsFromDir(
|
|
|
124
124
|
}
|
|
125
125
|
return { chains, diagnostics };
|
|
126
126
|
}
|
|
127
|
-
function discoverChains(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
scope: AgentScope,
|
|
131
|
-
): {
|
|
132
|
-
|
|
127
|
+
export function discoverChains(
|
|
128
|
+
cwd: string,
|
|
129
|
+
options: AgentDiscoveryOptions = {},
|
|
130
|
+
scope: AgentScope = "both",
|
|
131
|
+
): {
|
|
132
|
+
chains: ChainConfig[];
|
|
133
|
+
chainDiagnostics: ChainDiscoveryDiagnostic[];
|
|
134
|
+
userChainDir: string;
|
|
135
|
+
projectChainDir: string | null;
|
|
136
|
+
} {
|
|
137
|
+
const userChainDir = path.join(getAgentDir(), "chains");
|
|
138
|
+
const project = resolveNearestProjectChainDirs(cwd, options);
|
|
139
|
+
const projectChains = (scope === "user" ? [] : project.readDirs).map((dir) =>
|
|
133
140
|
loadChainsFromDir(dir, "project"),
|
|
134
141
|
);
|
|
135
142
|
const userChains =
|
|
136
|
-
scope === "project" ? { chains: [], diagnostics: [] } : loadChainsFromDir(
|
|
143
|
+
scope === "project" ? { chains: [], diagnostics: [] } : loadChainsFromDir(userChainDir, "user");
|
|
137
144
|
return {
|
|
145
|
+
userChainDir,
|
|
146
|
+
projectChainDir: project.preferredDir,
|
|
138
147
|
chains: [...userChains.chains, ...projectChains.flatMap((entry) => entry.chains)],
|
|
139
148
|
chainDiagnostics: [
|
|
140
149
|
...userChains.diagnostics,
|
|
@@ -186,10 +195,8 @@ export function discoverAgentsAll(
|
|
|
186
195
|
projectSettingsPath: string | null;
|
|
187
196
|
} {
|
|
188
197
|
const paths = profilePaths(cwd, options);
|
|
189
|
-
const userChainDir = path.join(getAgentDir(), "chains");
|
|
190
|
-
const projectChainPaths = resolveNearestProjectChainDirs(cwd, options);
|
|
191
198
|
const profiles = discoverLocalProfiles(paths, scope);
|
|
192
|
-
const chains = discoverChains(
|
|
199
|
+
const chains = discoverChains(cwd, options, scope);
|
|
193
200
|
const packages = loadConfiguredPackageAgents(cwd, scope, options);
|
|
194
201
|
return {
|
|
195
202
|
...profiles,
|
|
@@ -201,8 +208,6 @@ export function discoverAgentsAll(
|
|
|
201
208
|
],
|
|
202
209
|
userDir: paths.userDirs.at(0) ?? path.join(getAgentDir(), "agents"),
|
|
203
210
|
projectDir: paths.projectDir,
|
|
204
|
-
userChainDir,
|
|
205
|
-
projectChainDir: projectChainPaths.preferredDir,
|
|
206
211
|
userSettingsPath: paths.userSettingsPath,
|
|
207
212
|
projectSettingsPath: paths.projectSettingsPath,
|
|
208
213
|
};
|
package/src/extension/index.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
// Keep deferred imports on Pi's host-peer mapping path in production-only installs.
|
|
3
|
+
// oxlint-disable-next-line import/no-unassigned-import
|
|
4
|
+
import "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { SUBAGENT_CHILD_ENV } from "../runs/shared/child-route-env.ts";
|
|
4
6
|
import { errorMessage } from "../shared/unknown.ts";
|
|
5
7
|
import { loadConfig } from "./config.ts";
|
|
6
|
-
import { ParentSubagentRuntime } from "./parent-runtime.ts";
|
|
7
|
-
import { cleanupStaleRuntime } from "./runtime-reload.ts";
|
|
8
8
|
|
|
9
9
|
export { loadConfig } from "./config.ts";
|
|
10
10
|
|
|
11
|
-
export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
11
|
+
export default async function registerSubagentExtension(pi: ExtensionAPI): Promise<void> {
|
|
12
12
|
if (process.env[SUBAGENT_CHILD_ENV] === "1") {
|
|
13
13
|
// Explicit fanout-child owns nested delegation; globally discovered parent code remains inert.
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
+
// Children discover this entry too; keep the parent execution/history/UI graph behind the guard.
|
|
17
|
+
const [{ ensureTempRoot }, { ParentSubagentRuntime }, { cleanupStaleRuntime }] =
|
|
18
|
+
await Promise.all([
|
|
19
|
+
import("../shared/temp-root.ts"),
|
|
20
|
+
import("./parent-runtime.ts"),
|
|
21
|
+
import("./runtime-reload.ts"),
|
|
22
|
+
]);
|
|
16
23
|
try {
|
|
17
24
|
ensureTempRoot();
|
|
18
25
|
} catch (error) {
|
|
@@ -1,32 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentProgress,
|
|
3
|
-
Details,
|
|
4
|
-
SingleResult,
|
|
5
|
-
SubagentExecutionResult,
|
|
6
|
-
} from "../shared/types.ts";
|
|
1
|
+
import type { SubagentExecutionResult } from "../shared/types.ts";
|
|
7
2
|
import type { ReadonlyInput } from "../shared/types/inputs.ts";
|
|
8
3
|
|
|
9
|
-
/** A new result owns its progress buffers; native messages and accounting stay shared snapshots. */
|
|
10
|
-
export function copyProgress(progress: ReadonlyInput<AgentProgress>): AgentProgress {
|
|
11
|
-
return {
|
|
12
|
-
...progress,
|
|
13
|
-
recentTools: progress.recentTools.map((tool) => ({ ...tool })),
|
|
14
|
-
recentOutput: [...progress.recentOutput],
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function copyResult(result: ReadonlyInput<SingleResult>): SingleResult {
|
|
19
|
-
return { ...result, progress: result.progress ? copyProgress(result.progress) : undefined };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function copyDetails(details: ReadonlyInput<Details>): Details {
|
|
23
|
-
return {
|
|
24
|
-
...details,
|
|
25
|
-
results: details.results.map(copyResult),
|
|
26
|
-
progress: details.progress?.map(copyProgress),
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
4
|
/** Preserve the native result envelope while giving the receiving tool its own content array. */
|
|
31
5
|
export function copyExecutionResult(
|
|
32
6
|
result: ReadonlyInput<SubagentExecutionResult>,
|
package/src/history/ingest.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { AnswerTexts } from "./answer-texts.ts";
|
|
|
9
9
|
import { object } from "./values.ts";
|
|
10
10
|
import { identity, stamp, openSource, requireSource } from "./source-file.ts";
|
|
11
11
|
export { identity, stamp } from "./source-file.ts";
|
|
12
|
-
import { number, text } from "./rows.ts";
|
|
12
|
+
import { number, text, type Row } from "./rows.ts";
|
|
13
13
|
import { stageEntry, publishTools, type IngestRecord } from "./entry-ingestion.ts";
|
|
14
14
|
export { previewProjection, previewLimits, compactEntry } from "./preview.ts";
|
|
15
15
|
|
|
@@ -173,7 +173,6 @@ export class SourceIngest {
|
|
|
173
173
|
});
|
|
174
174
|
return;
|
|
175
175
|
}
|
|
176
|
-
this.stagedEntry = stageEntry(this.store, this.source, record);
|
|
177
176
|
this.textCursor = 0;
|
|
178
177
|
this.phase = "text";
|
|
179
178
|
}
|
|
@@ -235,64 +234,73 @@ export class SourceIngest {
|
|
|
235
234
|
}
|
|
236
235
|
private indexText(): void {
|
|
237
236
|
const record = this.record;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
throw new HistoryIndexError("CORRUPT", "Missing staged entry.");
|
|
237
|
+
if (!record) {
|
|
238
|
+
throw new HistoryIndexError("CORRUPT", "Missing ingestion record.");
|
|
241
239
|
}
|
|
242
240
|
const chunks = this.store.all(
|
|
243
|
-
"SELECT rowid,* FROM pending_text WHERE rowid>? ORDER BY rowid LIMIT
|
|
241
|
+
"SELECT rowid,* FROM pending_text WHERE rowid>? ORDER BY rowid LIMIT 33",
|
|
244
242
|
this.textCursor,
|
|
245
243
|
);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
"INSERT INTO documents(entry_rowid,field,text_start,text_end,preview) VALUES (?,?,?,?,?)",
|
|
255
|
-
rowid,
|
|
256
|
-
text(chunk, "field"),
|
|
257
|
-
number(chunk, "position"),
|
|
258
|
-
number(chunk, "end_position"),
|
|
259
|
-
safeText(body, 512),
|
|
260
|
-
).lastInsertRowid;
|
|
261
|
-
this.store.run("INSERT INTO corpus(rowid,text) VALUES (?,?)", id, body);
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
const last = chunks.at(-1);
|
|
265
|
-
if (last) {
|
|
266
|
-
this.textCursor = number(last, "rowid");
|
|
244
|
+
const complete = chunks.length <= 32;
|
|
245
|
+
const batch = chunks.slice(0, 32);
|
|
246
|
+
let rowid: number | undefined;
|
|
247
|
+
const write = () => {
|
|
248
|
+
rowid = this.stagedEntry ?? stageEntry(this.store, this.source, record);
|
|
249
|
+
this.indexChunks(rowid, record, batch);
|
|
250
|
+
if (complete) {
|
|
251
|
+
this.publishEntry(rowid, record);
|
|
267
252
|
}
|
|
268
|
-
|
|
253
|
+
};
|
|
254
|
+
if (complete) {
|
|
255
|
+
this.published(record.end, write);
|
|
256
|
+
this.stagedEntry = undefined;
|
|
257
|
+
this.phase = "read";
|
|
258
|
+
} else {
|
|
259
|
+
this.store.transaction(write);
|
|
260
|
+
this.stagedEntry = rowid;
|
|
261
|
+
this.textCursor = number(batch[31], "rowid");
|
|
269
262
|
}
|
|
263
|
+
}
|
|
264
|
+
private indexChunks(rowid: number, record: IngestRecord, chunks: readonly Row[]): void {
|
|
265
|
+
for (const chunk of chunks) {
|
|
266
|
+
if (!allowed(record.value, text(chunk, "field"))) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const body = safeText(text(chunk, "text"), 1_048_576);
|
|
270
|
+
const id = this.store.run(
|
|
271
|
+
"INSERT INTO documents(entry_rowid,field,text_start,text_end,preview) VALUES (?,?,?,?,?)",
|
|
272
|
+
rowid,
|
|
273
|
+
text(chunk, "field"),
|
|
274
|
+
number(chunk, "position"),
|
|
275
|
+
number(chunk, "end_position"),
|
|
276
|
+
safeText(body, 512),
|
|
277
|
+
).lastInsertRowid;
|
|
278
|
+
this.store.run("INSERT INTO corpus(rowid,text) VALUES (?,?)", id, body);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
private publishEntry(rowid: number, record: IngestRecord): void {
|
|
270
282
|
const value = record.value;
|
|
271
283
|
const answers = this.answerTexts.fingerprints(
|
|
272
284
|
value,
|
|
273
285
|
typeof value.id === "string" ? value.id : `legacy-${record.start}`,
|
|
274
286
|
);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
publishTools(this.store, this.source, rowid, value);
|
|
293
|
-
});
|
|
294
|
-
this.stagedEntry = undefined;
|
|
295
|
-
this.phase = "read";
|
|
287
|
+
if (
|
|
288
|
+
this.store.run(
|
|
289
|
+
"UPDATE entries SET published=1 WHERE rowid=? AND source_id=? AND generation=? AND published=0",
|
|
290
|
+
rowid,
|
|
291
|
+
this.id,
|
|
292
|
+
this.source.generation,
|
|
293
|
+
).changes !== 1
|
|
294
|
+
) {
|
|
295
|
+
throw new HistoryIndexError(
|
|
296
|
+
"SOURCE_CHANGED",
|
|
297
|
+
"Staged entry disappeared before publication; reconcile before advancing the source.",
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
for (const [digest, item] of answers) {
|
|
301
|
+
this.store.run("INSERT OR IGNORE INTO answers VALUES (?,?,?)", rowid, digest, item);
|
|
302
|
+
}
|
|
303
|
+
publishTools(this.store, this.source, rowid, value);
|
|
296
304
|
}
|
|
297
305
|
/** Each pump reads <=64 KiB or indexes <=32 chunks, then returns to the IPC event loop. */
|
|
298
306
|
step(): boolean {
|
package/src/history/prune.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function pruneHistory(
|
|
|
30
30
|
return store.transaction(() => {
|
|
31
31
|
let removed = previousSources === undefined && pruneRuns(store, runs);
|
|
32
32
|
for (const source of store.all(
|
|
33
|
-
`SELECT id,path FROM sources WHERE
|
|
33
|
+
`SELECT id,path FROM sources WHERE NOT EXISTS (SELECT 1 FROM children WHERE children.source_id=sources.id)${previousSources ? ` AND id IN (${previousSources.map(() => "?").join(",")})` : ""}`,
|
|
34
34
|
...(previousSources ?? []),
|
|
35
35
|
)) {
|
|
36
36
|
const id = text(source, "id");
|
|
@@ -147,10 +147,8 @@ export function projectRun(
|
|
|
147
147
|
foreground: Readonly<ReadonlyMap<string, ReadonlyForegroundResumeRun>>,
|
|
148
148
|
): HistoryRunRow {
|
|
149
149
|
const state: OwnedRunReadState = { ownedRuns: runs, foregroundRuns: foreground };
|
|
150
|
-
const questions = listRunQuestions(getRunMetadataDir(run.runId)).filter(
|
|
151
|
-
(question) =>
|
|
152
|
-
question.ownerSessionId === ownerSessionId &&
|
|
153
|
-
["awaiting_input", "answer_pending"].includes(question.state),
|
|
150
|
+
const questions = listRunQuestions(getRunMetadataDir(run.runId), ownerSessionId).filter(
|
|
151
|
+
(question) => ["awaiting_input", "answer_pending"].includes(question.state),
|
|
154
152
|
);
|
|
155
153
|
return {
|
|
156
154
|
...ownedRunView(run, state, {
|
|
@@ -93,7 +93,8 @@ function plan(
|
|
|
93
93
|
clauses.push("c.agent=?");
|
|
94
94
|
params.push(input.agent);
|
|
95
95
|
}
|
|
96
|
-
//
|
|
96
|
+
// Materialized IDs reject common out-of-scope postings without per-hit entry lookups.
|
|
97
|
+
// Each source is scoped once; bm25 still uses the whole corpus.
|
|
97
98
|
const eligible =
|
|
98
99
|
hasText(input.runId) || hasText(input.agent)
|
|
99
100
|
? `eligible AS MATERIALIZED (SELECT e.rowid FROM entries e JOIN sources s ON s.id=e.source_id WHERE e.published=1 AND e.generation=s.generation AND e.source_id IN (SELECT c.source_id FROM children c WHERE ${clauses.slice(4).join(" AND ")})),`
|
|
@@ -166,6 +167,12 @@ export class SearchQueries {
|
|
|
166
167
|
limit: number,
|
|
167
168
|
): readonly Row[] {
|
|
168
169
|
// Match every token across a record's visible windows; a quoted phrase cannot span fields.
|
|
170
|
+
// Relevance benefits from hit-driven attribution. Newest keeps the native join order:
|
|
171
|
+
// forcing hit order regresses newest sorting across many shared attempts.
|
|
172
|
+
const joined =
|
|
173
|
+
score === "matched.score"
|
|
174
|
+
? "FROM matched CROSS JOIN entries e ON e.rowid=matched.entry_rowid CROSS JOIN sources s ON s.id=e.source_id CROSS JOIN children c ON c.source_id=s.id JOIN excerpts ON excerpts.entry_rowid=matched.entry_rowid AND excerpts.choice=1 JOIN documents d ON d.id=excerpts.id"
|
|
175
|
+
: "FROM matched JOIN excerpts ON excerpts.entry_rowid=matched.entry_rowid AND excerpts.choice=1 JOIN documents d ON d.id=excerpts.id JOIN entries e ON e.rowid=matched.entry_rowid JOIN sources s ON s.id=e.source_id JOIN children c ON c.source_id=s.id";
|
|
169
176
|
// Ranking/filters precede LIMIT. The parent owns the hard deadline for a costly FTS scan.
|
|
170
177
|
return this.scope.store.all(
|
|
171
178
|
`WITH ${filter.eligible} hits AS MATERIALIZED (
|
|
@@ -173,7 +180,7 @@ export class SearchQueries {
|
|
|
173
180
|
), terms AS (SELECT entry_rowid,term,MIN(score) AS score FROM hits GROUP BY entry_rowid,term),
|
|
174
181
|
matched AS (SELECT entry_rowid,SUM(score) AS score FROM terms GROUP BY entry_rowid HAVING COUNT(*)=${expression.length}),
|
|
175
182
|
excerpts AS (SELECT id,entry_rowid,ROW_NUMBER() OVER (PARTITION BY entry_rowid ORDER BY score,id) AS choice FROM hits)
|
|
176
|
-
SELECT d.*,${score} AS score,e.*,d.id AS document_id,s.path,s.session_id,c.run_id,c.child_index,c.agent,d.preview AS document_preview
|
|
183
|
+
SELECT d.*,${score} AS score,e.*,d.id AS document_id,s.path,s.session_id,c.run_id,c.child_index,c.agent,d.preview AS document_preview ${joined} LEFT JOIN entries terminal ON terminal.source_id=s.id AND terminal.generation=s.generation AND terminal.id=c.terminal_entry_id AND terminal.published=1 WHERE ${filter.clauses.join(" AND ")} ORDER BY score,d.id,c.run_id,c.child_index LIMIT ?`,
|
|
177
184
|
...filter.eligibleParams,
|
|
178
185
|
...filter.params,
|
|
179
186
|
limit + 1,
|
package/src/history/store.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { openGeneration } from "./database-generation.ts";
|
|
3
3
|
import { OutputDigests } from "./output-digests.ts";
|
|
4
|
-
import type { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import type { DatabaseSync, StatementSync } from "node:sqlite";
|
|
5
5
|
import { historyTransaction, openHistoryDatabase } from "./database.ts";
|
|
6
6
|
import { hash, safeText, hasText } from "./text.ts";
|
|
7
7
|
export { hash, exactTextDigest, safeText, integer, pageLimit } from "./text.ts";
|
|
@@ -45,6 +45,7 @@ export class HistoryStore {
|
|
|
45
45
|
this.counters[operation] += amount;
|
|
46
46
|
}
|
|
47
47
|
private ingestLock?: DatabaseSync;
|
|
48
|
+
private readonly statements = new Map<string, StatementSync>();
|
|
48
49
|
private readonly outputDigests = new OutputDigests();
|
|
49
50
|
constructor(agentDir: string, owner: string) {
|
|
50
51
|
this.owner = owner;
|
|
@@ -72,12 +73,11 @@ export class HistoryStore {
|
|
|
72
73
|
return row;
|
|
73
74
|
}
|
|
74
75
|
get(sql: string, ...params: readonly IndexInput[]): Row | undefined {
|
|
75
|
-
const row = this.
|
|
76
|
+
const row = this.statement(sql).get(...params);
|
|
76
77
|
return row === undefined ? undefined : indexRow(row);
|
|
77
78
|
}
|
|
78
79
|
all(sql: string, ...params: readonly IndexInput[]): readonly Row[] {
|
|
79
|
-
return this.
|
|
80
|
-
.prepare(sql)
|
|
80
|
+
return this.statement(sql)
|
|
81
81
|
.all(...params)
|
|
82
82
|
.map(indexRow);
|
|
83
83
|
}
|
|
@@ -85,7 +85,22 @@ export class HistoryStore {
|
|
|
85
85
|
sql: string,
|
|
86
86
|
...params: readonly IndexInput[]
|
|
87
87
|
): ReturnType<ReturnType<DatabaseSync["prepare"]>["run"]> {
|
|
88
|
-
return this.
|
|
88
|
+
return this.statement(sql).run(...params);
|
|
89
|
+
}
|
|
90
|
+
private statement(sql: string): StatementSync {
|
|
91
|
+
let statement = this.statements.get(sql);
|
|
92
|
+
if (!statement) {
|
|
93
|
+
statement = this.db.prepare(sql);
|
|
94
|
+
// Dynamic browse/filter shapes cannot grow this connection's cache indefinitely.
|
|
95
|
+
if (this.statements.size >= 128) {
|
|
96
|
+
const oldest = this.statements.keys().next().value;
|
|
97
|
+
if (oldest !== undefined) {
|
|
98
|
+
this.statements.delete(oldest);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
this.statements.set(sql, statement);
|
|
102
|
+
}
|
|
103
|
+
return statement;
|
|
89
104
|
}
|
|
90
105
|
deleteDocuments(sourceId: string, unpublishedOnly = false): void {
|
|
91
106
|
const where = `entry_rowid IN (SELECT rowid FROM entries WHERE source_id=?${unpublishedOnly ? " AND published=0" : ""})`;
|
|
@@ -279,6 +294,7 @@ export class HistoryStore {
|
|
|
279
294
|
}
|
|
280
295
|
}
|
|
281
296
|
close(): void {
|
|
297
|
+
this.statements.clear();
|
|
282
298
|
this.ingestLock?.close();
|
|
283
299
|
this.db.close();
|
|
284
300
|
}
|
|
@@ -171,9 +171,14 @@ export class TextChunks {
|
|
|
171
171
|
this.fields.clear();
|
|
172
172
|
}
|
|
173
173
|
if (this.staged.length > 0) {
|
|
174
|
-
const insert = store.db.prepare("INSERT INTO pending_text VALUES (?,?,?,?)");
|
|
175
174
|
for (const item of this.staged) {
|
|
176
|
-
|
|
175
|
+
store.run(
|
|
176
|
+
"INSERT INTO pending_text VALUES (?,?,?,?)",
|
|
177
|
+
item.field,
|
|
178
|
+
item.start,
|
|
179
|
+
item.end,
|
|
180
|
+
item.text,
|
|
181
|
+
);
|
|
177
182
|
}
|
|
178
183
|
this.staged = [];
|
|
179
184
|
}
|