@fitchmultz/pi-subagents 0.46.1 → 0.46.2
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 +16 -0
- 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/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.md +5 -1
- package/docs/performance.md +72 -3
- package/package.json +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/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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.46.2] - 2026-10-12
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Keep parent execution, history and UI imports behind the child-mode guard. Skip empty streaming-spool replay/truncation and right-size copy buffers while preserving exact native wire logs and completed-text output.
|
|
8
|
+
- Reuse a bounded set of SQLite statements, prune through indexed source lookups and publish small indexed records in one transaction. Bound pending-human reply lookup and improve relevance search without changing global scores, attempt boundaries or pagination.
|
|
9
|
+
- Avoid irrelevant completion discovery and duplicate profile reads when listing agents. Reuse unchanged native history cards while retaining fresh detail validation, expanded state and source/theme invalidation.
|
|
10
|
+
- Assemble fragmented Intercom frames and restore topic snapshots incrementally. Encode socket payloads once and keep legacy lists identity-only without reconstructing discarded topic arrays; encoded limits and validation-before-commit remain unchanged.
|
|
11
|
+
- Aggregate workflow activity on tool transitions and coalesced status publication rather than every streaming delta. Preserve all observer updates and scheduling requests.
|
|
12
|
+
- Right-size saved-owner JSON scratch buffers and share supplemental-owner reads across recovery. Filter validated embedded question owners before reading foreign answer/delivery/revival/cancellation state, preserving full archive discovery, selected validation and fresh publication.
|
|
13
|
+
- Record reproducible operation-count baselines and qualified startup measurements in the performance guide.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Clear stale top-level tool/path activity when a parallel child exits or fails without a tool-end event, while retaining activity until a resistant process actually exits.
|
|
18
|
+
|
|
3
19
|
## [0.46.1] - 2026-10-11
|
|
4
20
|
|
|
5
21
|
### Fixed
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { discoverAgentsAll } from "./agents.js";
|
|
2
|
+
import { mergeAgents } from "./agent-selection.js";
|
|
2
3
|
import { formatAgentGroups, formatAgentDetail, formatChainDetail } from "./management-format.js";
|
|
3
4
|
import { handleCreate, handleUpdate, handleDelete } from "./management-persistence.js";
|
|
4
5
|
import { result, normalizeListScope, discoveryOptions, findEffectiveAgents, findChains, availableNamesText, } from "./management-targets.js";
|
|
@@ -9,7 +10,12 @@ export function handleList(params, ctx) {
|
|
|
9
10
|
return result("agentScope must be 'user', 'project', or 'both'.", true);
|
|
10
11
|
}
|
|
11
12
|
const discovery = discoverAgentsAll(ctx.cwd, discoveryOptions(ctx), scope);
|
|
12
|
-
const agents =
|
|
13
|
+
const agents = mergeAgents(discovery.user, discovery.project, [
|
|
14
|
+
...discovery.builtin,
|
|
15
|
+
...discovery.package,
|
|
16
|
+
])
|
|
17
|
+
.filter((agent) => agent.disabled !== true)
|
|
18
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
13
19
|
const chains = discovery.chains
|
|
14
20
|
.filter((chain) => scope === "both" || chain.source === scope)
|
|
15
21
|
.sort((a, b) => a.name.localeCompare(b.name));
|
package/dist/agents/agents.js
CHANGED
|
@@ -59,10 +59,14 @@ function loadChainsFromDir(dir, source) {
|
|
|
59
59
|
}
|
|
60
60
|
return { chains, diagnostics };
|
|
61
61
|
}
|
|
62
|
-
function discoverChains(
|
|
63
|
-
const
|
|
64
|
-
const
|
|
62
|
+
export function discoverChains(cwd, options = {}, scope = "both") {
|
|
63
|
+
const userChainDir = path.join(getAgentDir(), "chains");
|
|
64
|
+
const project = resolveNearestProjectChainDirs(cwd, options);
|
|
65
|
+
const projectChains = (scope === "user" ? [] : project.readDirs).map((dir) => loadChainsFromDir(dir, "project"));
|
|
66
|
+
const userChains = scope === "project" ? { chains: [], diagnostics: [] } : loadChainsFromDir(userChainDir, "user");
|
|
65
67
|
return {
|
|
68
|
+
userChainDir,
|
|
69
|
+
projectChainDir: project.preferredDir,
|
|
66
70
|
chains: [...userChains.chains, ...projectChains.flatMap((entry) => entry.chains)],
|
|
67
71
|
chainDiagnostics: [
|
|
68
72
|
...userChains.diagnostics,
|
|
@@ -88,10 +92,8 @@ export function discoverAgents(cwd, scope, options = {}) {
|
|
|
88
92
|
}
|
|
89
93
|
export function discoverAgentsAll(cwd, options = {}, scope = "both") {
|
|
90
94
|
const paths = profilePaths(cwd, options);
|
|
91
|
-
const userChainDir = path.join(getAgentDir(), "chains");
|
|
92
|
-
const projectChainPaths = resolveNearestProjectChainDirs(cwd, options);
|
|
93
95
|
const profiles = discoverLocalProfiles(paths, scope);
|
|
94
|
-
const chains = discoverChains(
|
|
96
|
+
const chains = discoverChains(cwd, options, scope);
|
|
95
97
|
const packages = loadConfiguredPackageAgents(cwd, scope, options);
|
|
96
98
|
return {
|
|
97
99
|
...profiles,
|
|
@@ -103,8 +105,6 @@ export function discoverAgentsAll(cwd, options = {}, scope = "both") {
|
|
|
103
105
|
],
|
|
104
106
|
userDir: paths.userDirs.at(0) ?? path.join(getAgentDir(), "agents"),
|
|
105
107
|
projectDir: paths.projectDir,
|
|
106
|
-
userChainDir,
|
|
107
|
-
projectChainDir: projectChainPaths.preferredDir,
|
|
108
108
|
userSettingsPath: paths.userSettingsPath,
|
|
109
109
|
projectSettingsPath: paths.projectSettingsPath,
|
|
110
110
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const EXTENSION_BUILD = Object.freeze({"version":"0.46.
|
|
1
|
+
export const EXTENSION_BUILD = Object.freeze({"version":"0.46.2","sha256":"3d48e9cb92ee79f898e74db19a38eecf8e61c126587a6a3d7958acfad4ee487e"});
|
package/dist/extension/index.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Keep deferred imports on Pi's host-peer mapping path in production-only installs.
|
|
2
|
+
// oxlint-disable-next-line import/no-unassigned-import
|
|
3
|
+
import "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { SUBAGENT_CHILD_ENV } from "../runs/shared/child-route-env.js";
|
|
3
5
|
import { errorMessage } from "../shared/unknown.js";
|
|
4
6
|
import { loadConfig } from "./config.js";
|
|
5
|
-
import { ParentSubagentRuntime } from "./parent-runtime.js";
|
|
6
|
-
import { cleanupStaleRuntime } from "./runtime-reload.js";
|
|
7
7
|
export { loadConfig } from "./config.js";
|
|
8
|
-
export default function registerSubagentExtension(pi) {
|
|
8
|
+
export default async function registerSubagentExtension(pi) {
|
|
9
9
|
if (process.env[SUBAGENT_CHILD_ENV] === "1") {
|
|
10
10
|
// Explicit fanout-child owns nested delegation; globally discovered parent code remains inert.
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
|
+
// Children discover this entry too; keep the parent execution/history/UI graph behind the guard.
|
|
14
|
+
const [{ ensureTempRoot }, { ParentSubagentRuntime }, { cleanupStaleRuntime }] = await Promise.all([
|
|
15
|
+
import("../shared/temp-root.js"),
|
|
16
|
+
import("./parent-runtime.js"),
|
|
17
|
+
import("./runtime-reload.js"),
|
|
18
|
+
]);
|
|
13
19
|
try {
|
|
14
20
|
ensureTempRoot();
|
|
15
21
|
}
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
/** A new result owns its progress buffers; native messages and accounting stay shared snapshots. */
|
|
2
|
-
export function copyProgress(progress) {
|
|
3
|
-
return {
|
|
4
|
-
...progress,
|
|
5
|
-
recentTools: progress.recentTools.map((tool) => ({ ...tool })),
|
|
6
|
-
recentOutput: [...progress.recentOutput],
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
export function copyResult(result) {
|
|
10
|
-
return { ...result, progress: result.progress ? copyProgress(result.progress) : undefined };
|
|
11
|
-
}
|
|
12
|
-
export function copyDetails(details) {
|
|
13
|
-
return {
|
|
14
|
-
...details,
|
|
15
|
-
results: details.results.map(copyResult),
|
|
16
|
-
progress: details.progress?.map(copyProgress),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
1
|
/** Preserve the native result envelope while giving the receiving tool its own content array. */
|
|
20
2
|
export function copyExecutionResult(result) {
|
|
21
3
|
return { ...result, content: [...result.content] };
|
package/dist/history/ingest.js
CHANGED
|
@@ -147,7 +147,6 @@ export class SourceIngest {
|
|
|
147
147
|
});
|
|
148
148
|
return;
|
|
149
149
|
}
|
|
150
|
-
this.stagedEntry = stageEntry(this.store, this.source, record);
|
|
151
150
|
this.textCursor = 0;
|
|
152
151
|
this.phase = "text";
|
|
153
152
|
}
|
|
@@ -197,41 +196,51 @@ export class SourceIngest {
|
|
|
197
196
|
}
|
|
198
197
|
indexText() {
|
|
199
198
|
const record = this.record;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
throw new HistoryIndexError("CORRUPT", "Missing staged entry.");
|
|
199
|
+
if (!record) {
|
|
200
|
+
throw new HistoryIndexError("CORRUPT", "Missing ingestion record.");
|
|
203
201
|
}
|
|
204
|
-
const chunks = this.store.all("SELECT rowid,* FROM pending_text WHERE rowid>? ORDER BY rowid LIMIT
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.store.run("INSERT INTO corpus(rowid,text) VALUES (?,?)", id, body);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
const last = chunks.at(-1);
|
|
217
|
-
if (last) {
|
|
218
|
-
this.textCursor = number(last, "rowid");
|
|
202
|
+
const chunks = this.store.all("SELECT rowid,* FROM pending_text WHERE rowid>? ORDER BY rowid LIMIT 33", this.textCursor);
|
|
203
|
+
const complete = chunks.length <= 32;
|
|
204
|
+
const batch = chunks.slice(0, 32);
|
|
205
|
+
let rowid;
|
|
206
|
+
const write = () => {
|
|
207
|
+
rowid = this.stagedEntry ?? stageEntry(this.store, this.source, record);
|
|
208
|
+
this.indexChunks(rowid, record, batch);
|
|
209
|
+
if (complete) {
|
|
210
|
+
this.publishEntry(rowid, record);
|
|
219
211
|
}
|
|
220
|
-
|
|
212
|
+
};
|
|
213
|
+
if (complete) {
|
|
214
|
+
this.published(record.end, write);
|
|
215
|
+
this.stagedEntry = undefined;
|
|
216
|
+
this.phase = "read";
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
this.store.transaction(write);
|
|
220
|
+
this.stagedEntry = rowid;
|
|
221
|
+
this.textCursor = number(batch[31], "rowid");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
indexChunks(rowid, record, chunks) {
|
|
225
|
+
for (const chunk of chunks) {
|
|
226
|
+
if (!allowed(record.value, text(chunk, "field"))) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const body = safeText(text(chunk, "text"), 1_048_576);
|
|
230
|
+
const id = this.store.run("INSERT INTO documents(entry_rowid,field,text_start,text_end,preview) VALUES (?,?,?,?,?)", rowid, text(chunk, "field"), number(chunk, "position"), number(chunk, "end_position"), safeText(body, 512)).lastInsertRowid;
|
|
231
|
+
this.store.run("INSERT INTO corpus(rowid,text) VALUES (?,?)", id, body);
|
|
221
232
|
}
|
|
233
|
+
}
|
|
234
|
+
publishEntry(rowid, record) {
|
|
222
235
|
const value = record.value;
|
|
223
236
|
const answers = this.answerTexts.fingerprints(value, typeof value.id === "string" ? value.id : `legacy-${record.start}`);
|
|
224
|
-
this.published
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
publishTools(this.store, this.source, rowid, value);
|
|
232
|
-
});
|
|
233
|
-
this.stagedEntry = undefined;
|
|
234
|
-
this.phase = "read";
|
|
237
|
+
if (this.store.run("UPDATE entries SET published=1 WHERE rowid=? AND source_id=? AND generation=? AND published=0", rowid, this.id, this.source.generation).changes !== 1) {
|
|
238
|
+
throw new HistoryIndexError("SOURCE_CHANGED", "Staged entry disappeared before publication; reconcile before advancing the source.");
|
|
239
|
+
}
|
|
240
|
+
for (const [digest, item] of answers) {
|
|
241
|
+
this.store.run("INSERT OR IGNORE INTO answers VALUES (?,?,?)", rowid, digest, item);
|
|
242
|
+
}
|
|
243
|
+
publishTools(this.store, this.source, rowid, value);
|
|
235
244
|
}
|
|
236
245
|
/** Each pump reads <=64 KiB or indexes <=32 chunks, then returns to the IPC event loop. */
|
|
237
246
|
step() {
|
package/dist/history/prune.js
CHANGED
|
@@ -17,7 +17,7 @@ export function pruneHistory(store, runs, previousSources, release) {
|
|
|
17
17
|
}
|
|
18
18
|
return store.transaction(() => {
|
|
19
19
|
let removed = previousSources === undefined && pruneRuns(store, runs);
|
|
20
|
-
for (const source of store.all(`SELECT id,path FROM sources WHERE
|
|
20
|
+
for (const source of store.all(`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(",")})` : ""}`, ...(previousSources ?? []))) {
|
|
21
21
|
const id = text(source, "id");
|
|
22
22
|
release(id, text(source, "path"));
|
|
23
23
|
store.deleteDocuments(id);
|
|
@@ -123,8 +123,7 @@ export function compactView(view) {
|
|
|
123
123
|
/** Canonical owner/filesystem projection. Native transcript presence never establishes completion. */
|
|
124
124
|
export function projectRun(run, ownerSessionId, runs, foreground) {
|
|
125
125
|
const state = { ownedRuns: runs, foregroundRuns: foreground };
|
|
126
|
-
const questions = listRunQuestions(getRunMetadataDir(run.runId)).filter((question) => question.
|
|
127
|
-
["awaiting_input", "answer_pending"].includes(question.state));
|
|
126
|
+
const questions = listRunQuestions(getRunMetadataDir(run.runId), ownerSessionId).filter((question) => ["awaiting_input", "answer_pending"].includes(question.state));
|
|
128
127
|
return {
|
|
129
128
|
...ownedRunView(run, state, {
|
|
130
129
|
pendingInput: questions.length > 0,
|
|
@@ -64,7 +64,8 @@ function plan(input, expression, cursor, score) {
|
|
|
64
64
|
clauses.push("c.agent=?");
|
|
65
65
|
params.push(input.agent);
|
|
66
66
|
}
|
|
67
|
-
//
|
|
67
|
+
// Materialized IDs reject common out-of-scope postings without per-hit entry lookups.
|
|
68
|
+
// Each source is scoped once; bm25 still uses the whole corpus.
|
|
68
69
|
const eligible = hasText(input.runId) || hasText(input.agent)
|
|
69
70
|
? `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 ")})),`
|
|
70
71
|
: "";
|
|
@@ -122,13 +123,18 @@ export class SearchQueries {
|
|
|
122
123
|
}
|
|
123
124
|
rows(expression, filter, score, limit) {
|
|
124
125
|
// Match every token across a record's visible windows; a quoted phrase cannot span fields.
|
|
126
|
+
// Relevance benefits from hit-driven attribution. Newest keeps the native join order:
|
|
127
|
+
// forcing hit order regresses newest sorting across many shared attempts.
|
|
128
|
+
const joined = score === "matched.score"
|
|
129
|
+
? "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"
|
|
130
|
+
: "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";
|
|
125
131
|
// Ranking/filters precede LIMIT. The parent owns the hard deadline for a costly FTS scan.
|
|
126
132
|
return this.scope.store.all(`WITH ${filter.eligible} hits AS MATERIALIZED (
|
|
127
133
|
${expression.map((_term, index) => `SELECT d.id,d.entry_rowid,bm25(corpus) AS score,${index} AS term FROM corpus JOIN documents d ON d.id=corpus.rowid WHERE corpus MATCH ?${filter.eligible.length > 0 ? " AND d.entry_rowid IN (SELECT rowid FROM eligible)" : ""}`).join(" UNION ALL ")}
|
|
128
134
|
), terms AS (SELECT entry_rowid,term,MIN(score) AS score FROM hits GROUP BY entry_rowid,term),
|
|
129
135
|
matched AS (SELECT entry_rowid,SUM(score) AS score FROM terms GROUP BY entry_rowid HAVING COUNT(*)=${expression.length}),
|
|
130
136
|
excerpts AS (SELECT id,entry_rowid,ROW_NUMBER() OVER (PARTITION BY entry_rowid ORDER BY score,id) AS choice FROM hits)
|
|
131
|
-
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
|
|
137
|
+
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 ?`, ...filter.eligibleParams, ...filter.params, limit + 1);
|
|
132
138
|
}
|
|
133
139
|
search(input) {
|
|
134
140
|
const expression = searchGrammar(input.query);
|
package/dist/history/store.js
CHANGED
|
@@ -26,6 +26,7 @@ export class HistoryStore {
|
|
|
26
26
|
this.counters[operation] += amount;
|
|
27
27
|
}
|
|
28
28
|
ingestLock;
|
|
29
|
+
statements = new Map();
|
|
29
30
|
outputDigests = new OutputDigests();
|
|
30
31
|
constructor(agentDir, owner) {
|
|
31
32
|
this.owner = owner;
|
|
@@ -49,17 +50,31 @@ export class HistoryStore {
|
|
|
49
50
|
return row;
|
|
50
51
|
}
|
|
51
52
|
get(sql, ...params) {
|
|
52
|
-
const row = this.
|
|
53
|
+
const row = this.statement(sql).get(...params);
|
|
53
54
|
return row === undefined ? undefined : indexRow(row);
|
|
54
55
|
}
|
|
55
56
|
all(sql, ...params) {
|
|
56
|
-
return this.
|
|
57
|
-
.prepare(sql)
|
|
57
|
+
return this.statement(sql)
|
|
58
58
|
.all(...params)
|
|
59
59
|
.map(indexRow);
|
|
60
60
|
}
|
|
61
61
|
run(sql, ...params) {
|
|
62
|
-
return this.
|
|
62
|
+
return this.statement(sql).run(...params);
|
|
63
|
+
}
|
|
64
|
+
statement(sql) {
|
|
65
|
+
let statement = this.statements.get(sql);
|
|
66
|
+
if (!statement) {
|
|
67
|
+
statement = this.db.prepare(sql);
|
|
68
|
+
// Dynamic browse/filter shapes cannot grow this connection's cache indefinitely.
|
|
69
|
+
if (this.statements.size >= 128) {
|
|
70
|
+
const oldest = this.statements.keys().next().value;
|
|
71
|
+
if (oldest !== undefined) {
|
|
72
|
+
this.statements.delete(oldest);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.statements.set(sql, statement);
|
|
76
|
+
}
|
|
77
|
+
return statement;
|
|
63
78
|
}
|
|
64
79
|
deleteDocuments(sourceId, unpublishedOnly = false) {
|
|
65
80
|
const where = `entry_rowid IN (SELECT rowid FROM entries WHERE source_id=?${unpublishedOnly ? " AND published=0" : ""})`;
|
|
@@ -194,6 +209,7 @@ export class HistoryStore {
|
|
|
194
209
|
}
|
|
195
210
|
}
|
|
196
211
|
close() {
|
|
212
|
+
this.statements.clear();
|
|
197
213
|
this.ingestLock?.close();
|
|
198
214
|
this.db.close();
|
|
199
215
|
}
|
|
@@ -159,9 +159,8 @@ export class TextChunks {
|
|
|
159
159
|
this.fields.clear();
|
|
160
160
|
}
|
|
161
161
|
if (this.staged.length > 0) {
|
|
162
|
-
const insert = store.db.prepare("INSERT INTO pending_text VALUES (?,?,?,?)");
|
|
163
162
|
for (const item of this.staged) {
|
|
164
|
-
|
|
163
|
+
store.run("INSERT INTO pending_text VALUES (?,?,?,?)", item.field, item.start, item.end, item.text);
|
|
165
164
|
}
|
|
166
165
|
this.staged = [];
|
|
167
166
|
}
|
|
@@ -51,18 +51,24 @@ export class TranscriptQueries {
|
|
|
51
51
|
this.scope = scope;
|
|
52
52
|
}
|
|
53
53
|
delivered(boundary, ids) {
|
|
54
|
-
const
|
|
54
|
+
const humans = [];
|
|
55
55
|
const where = boundary.clauses.join(" AND ");
|
|
56
56
|
for (const id of new Set(ids)) {
|
|
57
57
|
const human = this.scope.store.get(`SELECT start FROM entries WHERE ${where} AND human_id=? ORDER BY start DESC LIMIT 1`, ...boundary.params, id);
|
|
58
58
|
if (human) {
|
|
59
|
-
|
|
60
|
-
id,
|
|
61
|
-
this.scope.store.get(`SELECT rowid FROM entries WHERE ${where} AND start>? AND assistant_text=1 LIMIT 1`, ...boundary.params, number(human, "start")) !== undefined,
|
|
62
|
-
]);
|
|
59
|
+
humans.push([id, number(human, "start")]);
|
|
63
60
|
}
|
|
64
61
|
}
|
|
65
|
-
|
|
62
|
+
if (humans.length === 0) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
// One tail lookup answers every ID, without scanning before the earliest requested human.
|
|
66
|
+
const assistant = this.scope.store.get(`SELECT start FROM entries WHERE ${where} AND start>? AND assistant_text=1 ORDER BY start DESC LIMIT 1`, ...boundary.params, Math.min(...humans.map(([, start]) => start)));
|
|
67
|
+
const assistantStart = assistant ? number(assistant, "start") : undefined;
|
|
68
|
+
return humans.map(([id, start]) => [
|
|
69
|
+
id,
|
|
70
|
+
assistantStart !== undefined && start < assistantStart,
|
|
71
|
+
]);
|
|
66
72
|
}
|
|
67
73
|
readMetadata(boundary, readThrough) {
|
|
68
74
|
const where = boundary.clauses.join(" AND ");
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { copyDetails, copyResult } from "../extension/result-snapshot.js";
|
|
2
1
|
import { compactNestedResultChildren, formatNestedResultLines } from "./nested-result-tree.js";
|
|
3
2
|
export { compactNestedResultChildren, attachNestedChildrenToResultChildren, } from "./nested-result-tree.js";
|
|
4
3
|
export { deliverSubagentResultIntercomEvent, deliverSubagentIntercomMessageEvent, } from "./result-delivery.js";
|
|
@@ -215,76 +214,3 @@ export function buildSubagentResultIntercomPayload(input) {
|
|
|
215
214
|
};
|
|
216
215
|
return { ...payload, message: formatSubagentResultIntercomMessage(payload) };
|
|
217
216
|
}
|
|
218
|
-
function stripSingleResultOutputs(result) {
|
|
219
|
-
return {
|
|
220
|
-
...copyResult(result),
|
|
221
|
-
messages: undefined,
|
|
222
|
-
finalOutput: undefined,
|
|
223
|
-
truncation: undefined,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
export function stripDetailsOutputsForIntercomReceipt(details, delivery) {
|
|
227
|
-
return {
|
|
228
|
-
...copyDetails(details),
|
|
229
|
-
results: details.results.map(stripSingleResultOutputs),
|
|
230
|
-
...(delivery ? { intercomDelivery: delivery } : {}),
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
function compactReceiptSummary(summary) {
|
|
234
|
-
const withoutOutput = summary.split(/\n\nOutput:\n|\sOutput:\s/)[0] ?? summary;
|
|
235
|
-
const normalized = withoutOutput.replace(/\s+/g, " ").trim();
|
|
236
|
-
return normalized.length > 240 ? `${normalized.slice(0, 239)}…` : normalized;
|
|
237
|
-
}
|
|
238
|
-
function formatChildReferences(children, field, heading) {
|
|
239
|
-
const lines = [];
|
|
240
|
-
for (const child of children) {
|
|
241
|
-
const value = child[field];
|
|
242
|
-
if (typeof value === "string") {
|
|
243
|
-
lines.push(`- ${child.agent} [${child.status}]: ${value}`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return lines.length > 0 ? [heading, ...lines] : [];
|
|
247
|
-
}
|
|
248
|
-
function formatReceiptReferences(payload) {
|
|
249
|
-
const lines = [];
|
|
250
|
-
if (hasText(payload.resultPath)) {
|
|
251
|
-
lines.push(`Saved result (acceptance details when configured): ${payload.resultPath}`);
|
|
252
|
-
}
|
|
253
|
-
for (const child of payload.children) {
|
|
254
|
-
if (hasText(child.metadataPath)) {
|
|
255
|
-
lines.push(`Result metadata (${child.agent}; acceptance details when configured): ${child.metadataPath}${fs.existsSync(child.metadataPath) ? "" : " (missing)"}`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
lines.push(...formatChildReferences(payload.children, "artifactPath", "Artifacts:"), ...formatChildReferences(payload.children, "intercomTarget", "Run intercom targets (may be inactive after completion):"), ...formatChildReferences(payload.children, "sessionPath", "Sessions:"));
|
|
259
|
-
const nonCompleted = payload.children.filter((child) => child.status !== "completed");
|
|
260
|
-
if (nonCompleted.length > 0) {
|
|
261
|
-
lines.push("Non-completed children:");
|
|
262
|
-
for (const child of nonCompleted) {
|
|
263
|
-
lines.push(`- ${child.agent} [${child.status}]: ${compactReceiptSummary(child.summary)}`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return lines;
|
|
267
|
-
}
|
|
268
|
-
export function formatSubagentResultReceipt(input) {
|
|
269
|
-
const counts = countStatuses(input.payload.children);
|
|
270
|
-
const modeLabel = {
|
|
271
|
-
single: "single subagent result",
|
|
272
|
-
parallel: "parallel subagent results",
|
|
273
|
-
chain: "chain subagent results",
|
|
274
|
-
}[input.mode];
|
|
275
|
-
const lines = [
|
|
276
|
-
`Delivered ${modeLabel} via intercom.`,
|
|
277
|
-
"Delivery: succeeded",
|
|
278
|
-
`Run: ${input.runId}`,
|
|
279
|
-
`Child outcome: ${input.payload.status}`,
|
|
280
|
-
`Children: ${formatStatusCounts(counts)}`,
|
|
281
|
-
...(hasText(input.payload.error)
|
|
282
|
-
? [
|
|
283
|
-
`Workflow ${input.payload.status === "paused" ? "paused" : "error"}: ${input.payload.error}`,
|
|
284
|
-
]
|
|
285
|
-
: []),
|
|
286
|
-
];
|
|
287
|
-
lines.push(...formatReceiptReferences(input.payload));
|
|
288
|
-
lines.push("Full grouped output was sent over intercom.");
|
|
289
|
-
return lines.join("\n");
|
|
290
|
-
}
|
|
@@ -8,7 +8,7 @@ import { prepareBrokerSocketPath } from "./paths.js";
|
|
|
8
8
|
import { brokerPidRecord } from "./pid.js";
|
|
9
9
|
import { compactTopicMessage, isSessionRegistration, normalizeMessage, normalizeSessionInfo, } from "../types.js";
|
|
10
10
|
import { errorMessage, isRecord } from "../../shared/unknown.js";
|
|
11
|
-
import { messageSender, snapshotFrames,
|
|
11
|
+
import { messageSender, snapshotFrames, TopicSnapshot, publicationMessage, deliveryAcknowledgement, shouldReplaceDelivery, } from "./snapshots.js";
|
|
12
12
|
const INTERCOM_DIR = join(getPiAgentDir(), "intercom");
|
|
13
13
|
const PID_PATH = join(INTERCOM_DIR, "broker.pid");
|
|
14
14
|
const REPLACE_DELIVERY_DELAY_MS = 1500;
|
|
@@ -124,7 +124,12 @@ class IntercomBroker {
|
|
|
124
124
|
const info = { ...msg.session, id, lastSeen: msg.session.lastSeen ?? Date.now() };
|
|
125
125
|
snapshotFrames(randomUUID(), [info], true);
|
|
126
126
|
const topicFrames = msg.topicFrames === true;
|
|
127
|
-
this.sessions.set(id, {
|
|
127
|
+
this.sessions.set(id, {
|
|
128
|
+
socket,
|
|
129
|
+
info: messageSender(info),
|
|
130
|
+
topics: new TopicSnapshot(info),
|
|
131
|
+
topicFrames,
|
|
132
|
+
});
|
|
128
133
|
setId(id);
|
|
129
134
|
if (this.shutdownTimer) {
|
|
130
135
|
clearTimeout(this.shutdownTimer);
|
|
@@ -148,7 +153,7 @@ class IntercomBroker {
|
|
|
148
153
|
return deliveries;
|
|
149
154
|
}
|
|
150
155
|
for (const target of this.sessions.values()) {
|
|
151
|
-
const subscription = target.
|
|
156
|
+
const subscription = target.topics.subscription(publication.topic);
|
|
152
157
|
if (target.info.id === fromId || !subscription) {
|
|
153
158
|
continue;
|
|
154
159
|
}
|
|
@@ -171,22 +176,28 @@ class IntercomBroker {
|
|
|
171
176
|
if (!session) {
|
|
172
177
|
throw new Error("Sender session not found");
|
|
173
178
|
}
|
|
174
|
-
const prepared =
|
|
175
|
-
const nextInfo = prepared.
|
|
179
|
+
const prepared = session.topics.prepare(msg.change, session.topicFrames);
|
|
180
|
+
const nextInfo = prepared.restore
|
|
181
|
+
? session.info
|
|
182
|
+
: session.topics.snapshot(session.info, prepared);
|
|
176
183
|
const from = messageSender(nextInfo);
|
|
177
184
|
const deliveries = this.planPublications(currentId, from, prepared.publication);
|
|
178
|
-
const restore = prepared.restore;
|
|
179
|
-
const infos = [...this.sessions.values()].map((entry) => entry === session ? nextInfo : entry.info);
|
|
180
185
|
// Validate every frame before committing state or delivering publications.
|
|
181
|
-
if (restore) {
|
|
182
|
-
|
|
186
|
+
if (prepared.restore) {
|
|
187
|
+
session.topics.validateRestore(session.info, prepared, requestId);
|
|
183
188
|
}
|
|
184
|
-
const
|
|
189
|
+
const infos = prepared.restore
|
|
190
|
+
? []
|
|
191
|
+
: [...this.sessions.values()].map((entry) => {
|
|
192
|
+
if (entry === session) {
|
|
193
|
+
return nextInfo;
|
|
194
|
+
}
|
|
195
|
+
return msg.stream === true ? entry.topics.snapshot(entry.info) : entry.info;
|
|
196
|
+
});
|
|
197
|
+
const frames = snapshotFrames(requestId, infos, msg.stream === true);
|
|
185
198
|
const last = frames.pop();
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
session.info = nextInfo;
|
|
199
|
+
session.topics.commit(prepared);
|
|
200
|
+
session.info = messageSender(nextInfo);
|
|
190
201
|
const receipts = [];
|
|
191
202
|
for (const { to, message } of deliveries) {
|
|
192
203
|
receipts.push({ to, ...this.sendToSession(currentId, to, from, message) });
|
|
@@ -254,7 +265,10 @@ class IntercomBroker {
|
|
|
254
265
|
if (!session) {
|
|
255
266
|
return;
|
|
256
267
|
}
|
|
257
|
-
const update = {
|
|
268
|
+
const update = {
|
|
269
|
+
...session.topics.snapshot(session.info),
|
|
270
|
+
lastSeen: Date.now(),
|
|
271
|
+
};
|
|
258
272
|
for (const key of [
|
|
259
273
|
"name",
|
|
260
274
|
"status",
|
|
@@ -274,7 +288,10 @@ class IntercomBroker {
|
|
|
274
288
|
throw new Error("Invalid presence update");
|
|
275
289
|
}
|
|
276
290
|
snapshotFrames(randomUUID(), [nextInfo], true);
|
|
277
|
-
|
|
291
|
+
if (msg.topics !== undefined || msg.subscriptions !== undefined) {
|
|
292
|
+
session.topics = new TopicSnapshot(nextInfo);
|
|
293
|
+
}
|
|
294
|
+
session.info = messageSender(nextInfo);
|
|
278
295
|
}
|
|
279
296
|
sendToSession(fromId, toId, from, message) {
|
|
280
297
|
const target = this.sessions.get(toId);
|