fix(tui): refreshed prewalked subagent model

Tracked active subagent session model changes in progress snapshots so prewalk handoffs replace the starting-model badge.

Added regression coverage for a prewalk handoff and documented the fix.

Fixes #6083
This commit is contained in:
roboomp
2026-07-21 21:00:10 +00:00
parent 39c95e5e29
commit d52a759e5e
4 changed files with 48 additions and 9 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed prewalked subagents continuing to display their starting model after switching to the target model. ([#6083](https://github.com/can1357/oh-my-pi/issues/6083))
## [17.0.5] - 2026-07-18
### Added
@@ -47,7 +47,7 @@ For independent per-item chains (review → verify, fetch → extract → score)
schema: FINDINGS_SCHEMA,
});
return await parallel(found.findings.map((f) => async () => ({
...f,
…f,
verdict: await agent(
`Refute if you can (default refuted when unsure): ${f.title}`,
{ label: `verify:${f.file}`, schema: VERDICT_SCHEMA },
@@ -57,8 +57,6 @@ For independent per-item chains (review → verify, fetch → extract → score)
phase("Review");
const results = await parallel(DIMENSIONS.map((d) => async () => reviewAndVerify(d)));
const confirmed = results.flat().filter((f) => f.verdict.is_real);
Reach for `pipeline()` only when a stage genuinely needs ALL of the previous stage first — dedup/merge across the whole set, early-exit on zero, or "compare against the other findings" — because its inter-stage barrier makes every item wait for the slowest peer:
**Python (`eval`, Python backend):**
@@ -80,8 +78,6 @@ Reach for `pipeline()` only when a stage genuinely needs ALL of the previous sta
const verdicts = await parallel(findings.map((f) => async () =>
await agent(verifyPrompt(f), { schema: VERDICT_SCHEMA }),
));
Use ordinary code between calls to flatten/map/filter; don't add a barrier just for that. Nested `parallel()` pools each cap independently, so keep total fan-out sane.
</structure>
+10 -2
View File
@@ -1477,9 +1477,16 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
scheduleProgress(flushProgress);
};
const attach = (session: AgentSession): (() => void) =>
session.subscribe(event => {
const attach = (session: AgentSession): (() => void) => {
let activeModel = session.model ? formatModelStringWithRouting(session.model) : undefined;
return session.subscribe(event => {
emitSubagentEvent(event);
const nextModel = session.model ? formatModelStringWithRouting(session.model) : undefined;
if (nextModel && nextModel !== activeModel) {
activeModel = nextModel;
progress.resolvedModel = nextModel;
scheduleProgress(true);
}
if (event.type === "auto_retry_start") {
progress.retryState = {
attempt: event.attempt,
@@ -1530,6 +1537,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
return;
}
});
};
const captureSalvage = (session: AgentSession): void => {
// Best-effort salvage: capture the last assistant text so
@@ -24,13 +24,16 @@ import type { AgentDefinition, SingleResult } from "@oh-my-pi/pi-coding-agent/ta
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { EventBus } from "@oh-my-pi/pi-coding-agent/utils/event-bus";
function yieldEmittingSession(initialTools: string[] = ["read", "yield"]): AgentSession {
function yieldEmittingSession(
initialTools: string[] = ["read", "yield"],
modelSwitch?: { from: Model; to: Model },
): AgentSession {
const listeners: Array<(event: AgentSessionEvent) => void> = [];
let activeTools = initialTools;
const session = {
state: { messages: [] },
agent: { state: { systemPrompt: ["test"] } },
model: undefined,
model: modelSwitch?.from,
extensionRunner: undefined,
sessionManager: { appendSessionInit: () => {} },
getActiveToolNames: () => activeTools,
@@ -47,6 +50,12 @@ function yieldEmittingSession(initialTools: string[] = ["read", "yield"]): Agent
};
},
prompt: async (_text: string, _options?: PromptOptions) => {
if (modelSwitch) {
session.model = modelSwitch.to;
for (const listener of listeners) {
listener({ type: "notice", level: "info", message: "Prewalk switched", source: "prewalk" });
}
}
for (const listener of listeners) {
listener({
type: "tool_execution_end",
@@ -140,6 +149,28 @@ describe("runSubprocess per-agent prewalk", () => {
expect(forwarded?.prewalk?.target.provider).toBe(target.provider);
});
it("reports the prewalk target as the active model after handoff", async () => {
const progressModels: string[] = [];
vi.spyOn(sdkModule, "createAgentSession").mockResolvedValue(
createSessionResult(yieldEmittingSession(["read", "yield"], { from: primary, to: target })),
);
const result = await runSubprocess({
...baseOptions("subagent-prewalk-progress-model", Settings.isolated()),
agent: {
...baseAgent,
model: [`${primary.provider}/${primary.id}`],
prewalk: `${target.provider}/${target.id}`,
},
onProgress: progress => {
if (progress.resolvedModel) progressModels.push(progress.resolvedModel);
},
});
expect(result.exitCode).toBe(0);
expect(progressModels.at(-1)).toBe(`${target.provider}/${target.id}`);
});
it("resolves prewalk: true through the smol role default target", async () => {
const settings = Settings.isolated();
settings.setModelRole("smol", `${target.provider}/${target.id}`);