@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
|
@@ -9,7 +9,7 @@ import { createAsyncRunDiscovery } from "../background/async-status.js";
|
|
|
9
9
|
import { parseDetails, parseForegroundResumeRun, parseOwnedRun, parseSubagentExecutionResult, } from "../background/run-schemas.js";
|
|
10
10
|
import { collectInvocationAgentNames } from "../../shared/settings.js";
|
|
11
11
|
import { normalizeSubagentParamsLike, } from "../foreground/subagent-params.js";
|
|
12
|
-
import { getRunMetadataDir, migrateSupervisorQuestionSteps, readRunJson, saveQuestionOwner, saveRunStatus, } from "./supervisor-questions.js";
|
|
12
|
+
import { getRunMetadataDir, migrateSupervisorQuestionSteps, readQuestionOwner, readRunJson, saveQuestionOwner, saveRunStatus, } from "./supervisor-questions.js";
|
|
13
13
|
import { ASYNC_DIR, SLASH_RESULT_TYPE, } from "../../shared/types.js";
|
|
14
14
|
import { OWNED_RUN_ENTRY, rememberOwnedRun, saveForegroundRun } from "./run-persistence.js";
|
|
15
15
|
import { resetOwnedRuns, setOwnedRun, setForegroundRun, suspendRunChanges, } from "./run-state-owner.js";
|
|
@@ -181,15 +181,14 @@ function nativeLaunchCwd(run, details) {
|
|
|
181
181
|
: run;
|
|
182
182
|
}
|
|
183
183
|
function repairQuestionOwner(run) {
|
|
184
|
-
let
|
|
184
|
+
let sessionId;
|
|
185
185
|
try {
|
|
186
|
-
|
|
186
|
+
sessionId = readQuestionOwner(run.runId)?.sessionId;
|
|
187
187
|
}
|
|
188
188
|
catch {
|
|
189
189
|
// A genuine receipt can repair unusable supplemental metadata.
|
|
190
190
|
}
|
|
191
|
-
|
|
192
|
-
if (typeof sessionId !== "string" || sessionId.trim() === "") {
|
|
191
|
+
if (sessionId === undefined || sessionId.trim() === "") {
|
|
193
192
|
saveQuestionOwner(run.runId, run.ownerSessionId);
|
|
194
193
|
}
|
|
195
194
|
}
|
|
@@ -139,7 +139,7 @@ export function createSupervisorQuestion(input, root = QUESTIONS_DIR) {
|
|
|
139
139
|
writeAtomicJson(path.join(questionDir(question, root), "question.json"), question);
|
|
140
140
|
return question;
|
|
141
141
|
}
|
|
142
|
-
export function listRunQuestions(runDir) {
|
|
142
|
+
export function listRunQuestions(runDir, ownerSessionId) {
|
|
143
143
|
let entries;
|
|
144
144
|
try {
|
|
145
145
|
entries = fs.readdirSync(path.join(runDir, "questions"), { withFileTypes: true });
|
|
@@ -154,13 +154,16 @@ export function listRunQuestions(runDir) {
|
|
|
154
154
|
.filter((entry) => entry.isDirectory())
|
|
155
155
|
.flatMap((entry) => {
|
|
156
156
|
const question = readRunJson(path.join(runDir, "questions", entry.name, "question.json"), parseSupervisorQuestion);
|
|
157
|
-
return question
|
|
157
|
+
return question &&
|
|
158
|
+
(ownerSessionId === undefined || question.ownerSessionId === ownerSessionId)
|
|
159
|
+
? [readQuestionState(question, path.dirname(runDir))]
|
|
160
|
+
: [];
|
|
158
161
|
});
|
|
159
162
|
}
|
|
160
163
|
/** Known run IDs need neither a global directory walk nor prefix resolution. */
|
|
161
164
|
export function listOwnedRunQuestions(ownerSessionId, runId) {
|
|
162
165
|
migrateSupervisorQuestions(ownerSessionId, runId);
|
|
163
|
-
return listRunQuestions(getRunMetadataDir(runId)
|
|
166
|
+
return listRunQuestions(getRunMetadataDir(runId), ownerSessionId);
|
|
164
167
|
}
|
|
165
168
|
export function pendingSupervisorQuestion(input) {
|
|
166
169
|
if (input.sessionFile === undefined || input.sessionFile === "") {
|
|
@@ -211,7 +214,7 @@ function* supervisorQuestionSteps(ownerSessionId, runId, root = QUESTIONS_DIR) {
|
|
|
211
214
|
for (const entry of runs) {
|
|
212
215
|
yield;
|
|
213
216
|
try {
|
|
214
|
-
questions.push(...listRunQuestions(path.join(root, entry.name)
|
|
217
|
+
questions.push(...listRunQuestions(path.join(root, entry.name), ownerSessionId));
|
|
215
218
|
}
|
|
216
219
|
catch (error) {
|
|
217
220
|
if (runId !== undefined) {
|
|
@@ -137,7 +137,7 @@ export function readJsonProjection(file, select) {
|
|
|
137
137
|
const projection = new JsonProjection(select, undefined, () => Infinity);
|
|
138
138
|
try {
|
|
139
139
|
const end = fs.fstatSync(fd).size;
|
|
140
|
-
const bytes = Buffer.allocUnsafe(64 * 1024);
|
|
140
|
+
const bytes = Buffer.allocUnsafe(Math.min(64 * 1024, end));
|
|
141
141
|
for (let offset = 0; offset < end;) {
|
|
142
142
|
const count = fs.readSync(fd, bytes, 0, Math.min(bytes.length, end - offset), offset);
|
|
143
143
|
if (count === 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { discoverAgents,
|
|
1
|
+
import { discoverAgents, discoverChains } from "../agents/agents.js";
|
|
2
2
|
import { resolveExecutionCwd } from "../shared/execution-cwd.js";
|
|
3
3
|
function projectTrusted(state) {
|
|
4
4
|
return state.lastUiContext ? state.lastUiContext.isProjectTrusted() : true;
|
|
@@ -30,21 +30,19 @@ function completionPosition(prefix) {
|
|
|
30
30
|
}
|
|
31
31
|
export function makeAgentCompletions(pi, state, multiAgent) {
|
|
32
32
|
return (prefix) => {
|
|
33
|
+
const position = multiAgent ? completionPosition(prefix) : { word: prefix, before: "" };
|
|
34
|
+
if (!position || (!multiAgent && prefix.includes(" "))) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
33
37
|
const cwd = completionCwd(pi, state);
|
|
34
38
|
if (cwd === null || cwd.length === 0) {
|
|
35
39
|
return null;
|
|
36
40
|
}
|
|
37
41
|
const agents = discoverAgents(cwd, "both", { projectTrusted: projectTrusted(state) }).agents;
|
|
38
42
|
if (!multiAgent) {
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
:
|
|
42
|
-
.filter((agent) => agent.name.startsWith(prefix))
|
|
43
|
-
.map((agent) => ({ value: agent.name, label: agent.name }));
|
|
44
|
-
}
|
|
45
|
-
const position = completionPosition(prefix);
|
|
46
|
-
if (!position) {
|
|
47
|
-
return null;
|
|
43
|
+
return agents
|
|
44
|
+
.filter((agent) => agent.name.startsWith(prefix))
|
|
45
|
+
.map((agent) => ({ value: agent.name, label: agent.name }));
|
|
48
46
|
}
|
|
49
47
|
const { word: lastWord, before: beforeLastWord } = position;
|
|
50
48
|
if (lastWord === "->") {
|
|
@@ -57,7 +55,7 @@ export function makeAgentCompletions(pi, state, multiAgent) {
|
|
|
57
55
|
}
|
|
58
56
|
export function discoverSavedChains(cwd, trusted) {
|
|
59
57
|
const chains = new Map();
|
|
60
|
-
for (const chain of
|
|
58
|
+
for (const chain of discoverChains(cwd, { projectTrusted: trusted }).chains) {
|
|
61
59
|
const existing = chains.get(chain.name);
|
|
62
60
|
const projectOverride = existing?.source === "user" && chain.source === "project";
|
|
63
61
|
const jsonOverride = existing?.source === chain.source &&
|
|
@@ -71,8 +69,11 @@ export function discoverSavedChains(cwd, trusted) {
|
|
|
71
69
|
}
|
|
72
70
|
export function makeChainCompletions(pi, state) {
|
|
73
71
|
return (prefix) => {
|
|
72
|
+
if (prefix.includes(" ")) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
74
75
|
const cwd = completionCwd(pi, state);
|
|
75
|
-
if (
|
|
76
|
+
if (cwd === null || cwd.length === 0) {
|
|
76
77
|
return null;
|
|
77
78
|
}
|
|
78
79
|
return discoverSavedChains(cwd, projectTrusted(state))
|
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
import { stripAcceptanceReport } from "../runs/shared/acceptance-reports.js";
|
|
2
|
-
import { displayHistory } from "./history-display.js";
|
|
3
2
|
import { readableText } from "./history-text.js";
|
|
4
3
|
export { readableText } from "./history-text.js";
|
|
5
4
|
export { indexedHistory } from "./indexed-history.js";
|
|
6
|
-
/** Read native entries, not a compacted model context: earlier messages remain inspectable. */
|
|
7
|
-
export function historyItems(entries) {
|
|
8
|
-
const visible = [];
|
|
9
|
-
for (const entry of entries) {
|
|
10
|
-
if (entry.type === "message" ||
|
|
11
|
-
entry.type === "custom_message" ||
|
|
12
|
-
entry.type === "compaction" ||
|
|
13
|
-
entry.type === "branch_summary") {
|
|
14
|
-
visible.push(entry.type === "message" ? { ...entry, native: entry.message } : entry);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return displayHistory(visible);
|
|
18
|
-
}
|
|
19
5
|
function finalMatch(history, text) {
|
|
20
6
|
if (history.finalId !== undefined) {
|
|
21
7
|
return history.finalId;
|
package/dist/tui/agent-view.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { errorText } from "./text-values.js";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
3
|
import { historyStatus, pageNotices, taskNotices, outgoingHistory, } from "./conversation-notices.js";
|
|
3
4
|
import { getSingleResultOutput } from "../shared/utils.js";
|
|
4
5
|
import { withFinalResult } from "./agent-history.js";
|
|
@@ -142,9 +143,17 @@ export class ConversationHistory {
|
|
|
142
143
|
const items = history.items.map((item) => item.id === `result:${task.run.runId}`
|
|
143
144
|
? { ...item, load: () => this.controller.savedResult(this.key, item.id) }
|
|
144
145
|
: item);
|
|
146
|
+
const previous = task.page, sameSource = previous !== undefined &&
|
|
147
|
+
previous.sourceId === result.page.sourceId &&
|
|
148
|
+
previous.generation === result.page.generation &&
|
|
149
|
+
previous.sessionId === result.page.sessionId &&
|
|
150
|
+
previous.sourceState === result.page.sourceState &&
|
|
151
|
+
isDeepStrictEqual(previous.entries.map((entry) => entry.ref), result.page.entries.map((entry) => entry.ref));
|
|
145
152
|
this.seenSource ||= result.page.count > 0;
|
|
146
153
|
this.controller.applyHistoryPage(this.key, { ...history, items }, result.page, this.seenSource);
|
|
147
|
-
|
|
154
|
+
if (!sameSource) {
|
|
155
|
+
this.source.clearCards();
|
|
156
|
+
}
|
|
148
157
|
}
|
|
149
158
|
assignment() {
|
|
150
159
|
const task = this.controller.task(this.key);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import { AssistantMessageComponent, UserMessageComponent, ToolExecutionComponent, createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createPowerShellToolDefinition, createReadToolDefinition, createWriteToolDefinition, getMarkdownTheme, renderDiff, } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Container, Spacer, Text, } from "@earendil-works/pi-tui";
|
|
3
4
|
import { formatModelThinking } from "../shared/formatters.js";
|
|
@@ -5,27 +6,9 @@ import { stripAcceptanceReport } from "../runs/shared/acceptance-reports.js";
|
|
|
5
6
|
import { readableText } from "./agent-history.js";
|
|
6
7
|
import { short } from "./view-model.js";
|
|
7
8
|
import { hasText } from "./text-values.js";
|
|
8
|
-
function plainItem(item) {
|
|
9
|
-
return (item.load === undefined &&
|
|
10
|
-
item.assistant === undefined &&
|
|
11
|
-
item.previewAssistant === undefined &&
|
|
12
|
-
item.call === undefined &&
|
|
13
|
-
item.result === undefined);
|
|
14
|
-
}
|
|
15
|
-
function plainSignature(item) {
|
|
16
|
-
return JSON.stringify([
|
|
17
|
-
item.kind,
|
|
18
|
-
item.title,
|
|
19
|
-
item.text,
|
|
20
|
-
item.details,
|
|
21
|
-
item.diff,
|
|
22
|
-
item.model,
|
|
23
|
-
item.timestamp,
|
|
24
|
-
item.messageId,
|
|
25
|
-
]);
|
|
26
|
-
}
|
|
27
9
|
function sameDisplay(left, right) {
|
|
28
|
-
|
|
10
|
+
// Detail loaders stay fresh in contentItems; only the native display component is reused.
|
|
11
|
+
return isDeepStrictEqual({ ...left, load: undefined }, { ...right, load: undefined });
|
|
29
12
|
}
|
|
30
13
|
function previewStop(reason) {
|
|
31
14
|
if (reason === "error" || reason === "aborted" || reason === "length") {
|
|
@@ -132,8 +115,11 @@ export class HistoryCards {
|
|
|
132
115
|
}
|
|
133
116
|
card(item, expanded, detail) {
|
|
134
117
|
const signature = `${expanded}:${detail}`, cached = this.components.get(item.id);
|
|
135
|
-
if (cached?.signature === signature &&
|
|
136
|
-
|
|
118
|
+
if (cached?.signature === signature && cached.item === item) {
|
|
119
|
+
return cached.component;
|
|
120
|
+
}
|
|
121
|
+
if (cached?.signature === signature && sameDisplay(cached.item, item)) {
|
|
122
|
+
this.components.set(item.id, { ...cached, item });
|
|
137
123
|
return cached.component;
|
|
138
124
|
}
|
|
139
125
|
const component = this.create(item, { expanded, detail });
|
package/docs/intercom.md
CHANGED
|
@@ -183,7 +183,11 @@ The reply hint (enabled by default) points to `intercom({ action: "reply", ... }
|
|
|
183
183
|
|
|
184
184
|
If Esc clears a steered or follow-up message before native handoff, it is re-delivered after the actual agent run settles. Messages already in Pi's session history are not reinserted, even after compaction or reload; messages still in Pi's native queues remain there for its next continuation or prompt. Pi 1.0's turn-boundary pending messages expose the next queued batch, not the whole custom queue; its coarse `hasPendingMessages()` reports only UI text queues. An empty boundary proves all queues cleared, while a nonempty boundary retains admitted messages until their actual history receipts arrive. Selectively removing individual custom items while other queue items remain is not observable through these public APIs: such items remain pending rather than risk a duplicate. Full queue clearing, retained-queue abort, reload and fresh-process recovery are supported. A rejected overlapping prompt does not start receiver retries while the original run is active.
|
|
185
185
|
|
|
186
|
-
Official Pi 1.0 runs user preparation hooks before marking a prompt busy. During that pre-admission window, an incoming custom message can legitimately start a separate model request; custom-triggered runs do not run `before_agent_start`. Both inputs and their receipts are preserved, but one combined request is not guaranteed.
|
|
186
|
+
Official Pi 1.1.0 runs user preparation hooks before marking a prompt busy. During that pre-admission window, an incoming custom message can legitimately start a separate model request; custom-triggered runs do not run `before_agent_start`. Both inputs and their receipts are preserved, but one combined request is not guaranteed.
|
|
187
|
+
|
|
188
|
+
Newer hosts with discriminated `before_agent_start` events also prepare custom-triggered runs, including Intercom and background-command wakeups. A user event has `messageType:"user"` and `prompt`; a custom event has `messageType:"custom"` and `message`, without `prompt`. These hosts mark preparation busy, so Intercom steers arrivals into the starting run instead of launching a separate run. Native steering determines when queued content reaches the model; both inputs and their receipts remain preserved exactly once. Child role/output and peer-awareness sections apply to custom starts without treating notification content as user commands.
|
|
189
|
+
|
|
190
|
+
Intercom follows the host's live idle state; it does not reserve rejected input or emulate a core admission hook. Reload also does not cancel active work: abort explicitly when cancellation is intended.
|
|
187
191
|
|
|
188
192
|
An omitted `ask` still honors recipient availability; use explicit steer only when the sender must remain alive for a busy recipient's reply. The recipient should incorporate relevant context and continue its active task unless the message explicitly replaces it. Use `delivery:"queue"` only when delay is intentional; `queueMode:"replace"` keeps only the latest undelivered thread update.
|
|
189
193
|
|
package/docs/performance.md
CHANGED
|
@@ -1,4 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Performance
|
|
2
|
+
|
|
3
|
+
## 0.46.2 measurements
|
|
4
|
+
|
|
5
|
+
The baseline is `e00b241` (0.46.1). These synthetic workloads exercise the real storage, SQLite, discovery, native-card and broker owners. Counts describe work removed, not a guaranteed reduction in end-to-end latency, retained memory or provider cost.
|
|
6
|
+
|
|
7
|
+
| Workload | Counted operation | 0.46.1 | 0.46.2 |
|
|
8
|
+
| ---------------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------: | --------------------: |
|
|
9
|
+
| 10,000 text deltas | Empty text-spool truncations | 10,000 | 0 |
|
|
10
|
+
| Same deltas | 64 KiB scratch allocations / allocated bytes | 10,000 / 655,360,000 | 0 / 0 |
|
|
11
|
+
| 1,000 one-window native records, cold index | SQL prepares | 14,015 | 19 |
|
|
12
|
+
| Same records | Explicit main-database commits / changed-main autocommit statements | 2,002 / 1,002 | 1,002 / 2 |
|
|
13
|
+
| 1,000 narrowed prune calls beside 1,000 linked sources | Child rows visited | 1,000,000 | 1,000 |
|
|
14
|
+
| 1,000 pending human-message IDs with no later reply | Assistant-tail candidates / statements | 499,500 / 1,000 | 999 / 1 |
|
|
15
|
+
| 200 attempts sharing 1,000 common matching records, relevance search | Final entry-predicate evaluations | 200,000 | 1,000 |
|
|
16
|
+
| 217 profiles, completion after task text or inside an unfinished quote | Profile-file reads | 217 | 0 |
|
|
17
|
+
| Same profiles, management list | Profile-file reads | 434 | 217 |
|
|
18
|
+
| Same profiles and 10 saved chains, saved-chain completion | Profile-file / chain-file reads | 217 / 10 | 0 / 10 |
|
|
19
|
+
| Three identical 100-record history pages | Native cards constructed, including assignment | 303 | 101 |
|
|
20
|
+
| One 1 MiB Intercom frame, 4 KiB fragments | Bytes copied by frame assembly | 135,790,596 | 1,048,576 |
|
|
21
|
+
| Same frame, 1 KiB fragments | Bytes copied by frame assembly | 538,443,780 | 1,048,576 |
|
|
22
|
+
| 1,000 streamed topic rows | Topic text validations | 500,500 | 1,000 |
|
|
23
|
+
| Real broker restoring 1,000 topics | JSON encodings / serialized bytes | 505,504 / 112,612,468 | 7,003 / 873,268 |
|
|
24
|
+
| One socket message write | JSON encodings | 2 | 1 |
|
|
25
|
+
| 10,000 thinking deltas, 1,000 retained workflow steps | Step-status reads in event projection | 10,000,000 | 1,000 |
|
|
26
|
+
| Inert root extension, emitted static local import graph | Modules / JavaScript source bytes / TUI modules | 287 / 3,248,709 / 47 | 11 / 20,587 / 0 |
|
|
27
|
+
| 1,000 foreign async owners, 23-byte supplemental records | Allocated owner-read scratch bytes | 65,536,000 | 23,000 |
|
|
28
|
+
| Same owner census | Opens / reads / closes | 1,000 / 1,000 / 1,000 | 1,000 / 1,000 / 1,000 |
|
|
29
|
+
| 128 foreign questions beside one selected answered question | Foreign question-state opens | 512 | 0 |
|
|
30
|
+
|
|
31
|
+
### Method and retained contracts
|
|
32
|
+
|
|
33
|
+
Measurements used Node **24.21.0**, SQLite **3.53.4** and synthetic private files on macOS/APFS. Discovery used 100 user, 100 trusted-project and 17 builtin profiles. Repeated count probes produced identical results. The history counts begin at cold `SourceIngest` construction after database/source setup; they are SQL transaction and statement boundaries, **not measured fsync counts**. A 100-record control also falls from 1,415 prepares to 19 and from 202 explicit commits plus 102 changed-main autocommit statements to 102 plus 2.
|
|
34
|
+
|
|
35
|
+
Every streaming delta still reaches the observer and schedules status publication; exact wire bytes, malformed-record diagnostics, member ordering, surrogate handling and completed-text output remain covered. Tool aggregation still scans steps on actual tool transitions and coalesced status writes. The event-projection probe has no intervening status publication; its 10,000 scheduling requests are retained, rather than suppressed to obtain the count.
|
|
36
|
+
|
|
37
|
+
Reply lookup reads the latest matching human copy for each requested ID, then answers all IDs from one bounded assistant-tail lookup; physical order and terminal/time boundaries remain authoritative. Empty, missing and latest-singleton requests retain zero assistant-candidate scans.
|
|
38
|
+
|
|
39
|
+
Recovery right-sizes the existing fixed-snapshot streaming JSON buffer to the file's size, up to the unchanged **64 KiB** ceiling. It retains fatal UTF-8 decoding, complete JSON/object validation, positional reads on one opened descriptor and large discarded-field handling. The 1,000-owner probe still reads all 1,000 records and performs the same 3,000 streaming-parser writes; it removes oversized scratch allocation, not filesystem discovery or parsing. A separate native-startup keeper beside 2,048 35-byte foreign owners reduces observed read-buffer bytes from 134,217,728 to 71,680.
|
|
40
|
+
|
|
41
|
+
Owner-scoped question access still reads and validates all 129 question records in the probe, using each question's embedded owner before opening answer, delivery, revival or cancellation state. The selected question retains all four state opens; missing, corrupt or disagreeing supplemental owner files do not hide its question. Unfiltered readers retain full state validation, selected corruption still fails, and later publications are read afresh. There is no archive cap, persistent negative-owner cache or question-state cache.
|
|
42
|
+
|
|
43
|
+
Search qualification used 100,000 documents across 100 owned sources, including 99,000 out-of-scope documents and 200 attempts sharing the selected source. All 12 rare/common × child/shared-agent/global × relevance/newest cases retained independent native-`bm25` ordering and exact first/second-page output. Relevance uses hit-driven attribution; newest retains its previous fixed join order. Scoped eligible-ID materialization remains intentional: replacing it with a correlated entry lookup made common scoped queries about three times slower and was rejected. Contended medians for shared-source relevance were 62.00→29.25 ms (common) and 6.23→3.12 ms (rare); small timing differences in other cases are treated as variation, not gains. Common FTS postings still require scanning, and a bounded page does not bound all search work.
|
|
44
|
+
|
|
45
|
+
SQLite statement reuse is bounded to 128 SQL shapes. Records with at most 32 text windows stage and publish their entry, documents and cursor in one transaction; longer records keep bounded staging transactions. No browse transaction spans a pump yield. Native cold-kill coverage brackets both sides of publication at the 32/33-window boundary. WAL, `synchronous=FULL`, generation checks, the separate writer lock, strict source validation and detail budgets remain intact.
|
|
46
|
+
|
|
47
|
+
Settled pages reuse only their native display components. Equal refreshes advance the comparison item so subsequent frames retain the identity fast path. Selected detail loaders remain fresh; source generation/reference changes and theme invalidation rebuild cards, while unchanged refreshes retain expanded thinking and reading position. Completion and list work still observes current profile edits, overrides, disabled profiles and project trust rather than keeping a persistent discovery cache.
|
|
48
|
+
|
|
49
|
+
Intercom retains the 1 MiB encoded-frame limit, request-ID and escaped-envelope checks, unknown fields, duplicate ordering, first-match subscriptions, stale-revision behavior and validation before state commit or publication. Legacy identity-only lists do not reconstruct discarded peer topic arrays. Replacing or removing the largest cached topic record still scans cached numeric sizes; a requested full snapshot necessarily remains linear in its returned rows.
|
|
50
|
+
|
|
51
|
+
Dynamic output schemas deliberately retain native compilation. Object identity or JSON content alone cannot safely cache nested mutations, recursive references and host format-registry changes. Authoritative receipt/accounting checks, native publication/fsync boundaries, fallback polling, child isolation, fork/fresh choices and acceptance verification are not removed for a better benchmark.
|
|
52
|
+
|
|
53
|
+
The regression owners include `test/integration/{storage-owner-boundaries,session-resume-cost,async-job-tracker,history-index,agent-interaction,slash-commands,pi-intercom-broker,async-execution,process-lifecycle,parent-usage}.test.ts` and `test/unit/{supervisor-questions,compact-child-tools,agent-disabled,pi-intercom-framing}.test.ts`. These protect observable contracts and meaningful operation counts; there are no timing thresholds in CI.
|
|
54
|
+
|
|
55
|
+
### Startup qualification
|
|
56
|
+
|
|
57
|
+
Both production-only packages were loaded through the real official **Pi 1.1.0** and maintained fork **c329e624e73ba2086bd3ce15e091d667879dca9c** SDKs and bundled CLIs, using physical Node 24.21.0 and private HOME/agent/temp directories. SDK preflights asserted error-free registration and expected tools for both package entries, the inert root child, and the actual leaf route (prompt runtime plus both package entries and supervisor metadata). The compiled root deliberately imports the host SDK before deferred imports so production installs retain Pi's host-peer mapping; it does not install another SDK.
|
|
58
|
+
|
|
59
|
+
Fresh-process offline `--list-models zzznomatch` samples below used the same pruned artifacts, one warmup and nine rotated/interleaved runs per case on an Apple M5 Pro. Values are **minimum / median / maximum milliseconds**. Parent loads both entries; root-child loads only the inert global root; leaf loads the real non-delegating child route. No-op/native controls had minima 154.6/143.6 ms on official and 306.5/284.0 ms on the fork.
|
|
60
|
+
|
|
61
|
+
| Host | Route | 0.46.1 | 0.46.2 |
|
|
62
|
+
| -------------- | ---------- | ---------------------- | ---------------------- |
|
|
63
|
+
| Official 1.1.0 | parent | 421.1 / 479.4 / 652.1 | 408.4 / 527.5 / 1138.0 |
|
|
64
|
+
| Official 1.1.0 | root-child | 382.4 / 497.8 / 1105.7 | 252.7 / 301.9 / 498.8 |
|
|
65
|
+
| Official 1.1.0 | leaf | 414.4 / 509.0 / 687.9 | 283.2 / 406.7 / 715.7 |
|
|
66
|
+
| Fork c329e624e | parent | 615.2 / 717.2 / 1416.8 | 629.9 / 676.2 / 796.5 |
|
|
67
|
+
| Fork c329e624e | root-child | 592.5 / 635.8 / 732.1 | 379.3 / 417.5 / 688.3 |
|
|
68
|
+
| Fork c329e624e | leaf | 610.4 / 666.0 / 1160.6 | 464.1 / 481.0 / 607.8 |
|
|
69
|
+
|
|
70
|
+
The machine was contended and observed ranges overlap. These samples do **not** establish an isolated startup-latency gain or regression; operation counts and the smaller local import graph are the repeatable evidence. This measures extension-factory loading, not TUI readiness, model/provider requests or full-session latency. An additional production SDK/CLI/broker/history smoke passed on each host, and the previous fork revision `9007fefef` was qualified separately. No timing threshold is added to CI.
|
|
2
71
|
|
|
3
72
|
## Current behavior
|
|
4
73
|
|
|
@@ -32,7 +101,7 @@ Best-effort timeout samples use a separate `run-timing.sqlite` metadata database
|
|
|
32
101
|
The changed boundaries are exercised through real extension functions and registered SDK tools with synthetic isolated data, not personal transcripts or provider inference:
|
|
33
102
|
|
|
34
103
|
- 20 and 227 genuinely owned runs beside 8,000 unrelated records: Agents startup does no parent-thread child transcript reads or global question listing; only the selected conversation is formatted.
|
|
35
|
-
- Native extension startup beside 2,048 foreign owner directories services input during discovery and restores only the current parent's saved runs and unanswered questions.
|
|
104
|
+
- Native extension startup beside 2,048 foreign owner directories and questions services input during discovery, uses snapshot-sized owner scratch, never opens foreign question state and restores only the current parent's saved runs and unanswered questions.
|
|
36
105
|
- Native Intercom reload restores all 1,024 historical receipts while servicing input. Cleared-queue recovery also services input before its 100 followers finish appending, retaining each message once and one final wake-up across an in-flight reload or a cleared busy handoff. New results and explicit steers are staged before acknowledgement and handed to native steering for the next tool boundary; ordinary queued and passive arrivals wait.
|
|
37
106
|
- 125 owned assignments: global filtering finds an item beyond the first two picker pages; all later pages remain reachable without UI-thread source access.
|
|
38
107
|
- 65 retained runs: off-page reads/stats and migration scans are zero while displayed controls, questions, review and continuation identities remain fresh.
|
|
@@ -77,7 +146,7 @@ Cold recovery still enumerates genuine owner handles and verifies relevant publi
|
|
|
77
146
|
|
|
78
147
|
Pi 1.0 still eagerly loads native session bodies at its own startup; extension installation alone cannot remove that host cost. Explicit fork context still copies and publishes each sibling, with required fsync. An explicit full individual record must fit its detail budget and the consumer's heap.
|
|
79
148
|
|
|
80
|
-
Full-archive backfill/capacity, sustained contention, power-loss durability and cross-platform performance are not qualified by these macOS/APFS synthetic regressions.
|
|
149
|
+
Full-archive backfill/capacity, sustained contention, power-loss durability and cross-platform performance are not qualified by these macOS/APFS synthetic regressions. The measurement workloads do not delete or migrate native journals, inspect personal transcripts or make inference requests. Installed extension updates are separate from these workloads. Intentional tool/queue boundaries, provider response time, host rendering and other extensions remain separate possible costs.
|
|
81
150
|
|
|
82
151
|
## Cached-input traffic and delegation
|
|
83
152
|
|
package/package.json
CHANGED
|
@@ -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>,
|