fix: skip compaction messages in parent-session context lookup

This commit is contained in:
YeonGyu-Kim 2026-02-17 01:36:52 +09:00
parent e1e449164a
commit 285d8d58dd
2 changed files with 185 additions and 72 deletions

View File

@ -805,6 +805,62 @@ interface CurrentMessage {
} }
describe("BackgroundManager.notifyParentSession - dynamic message lookup", () => { describe("BackgroundManager.notifyParentSession - dynamic message lookup", () => {
test("should skip compaction agent and use nearest non-compaction message", async () => {
//#given
let capturedBody: Record<string, unknown> | undefined
const client = {
session: {
prompt: async () => ({}),
promptAsync: async (args: { body: Record<string, unknown> }) => {
capturedBody = args.body
return {}
},
abort: async () => ({}),
messages: async () => ({
data: [
{
info: {
agent: "sisyphus",
model: { providerID: "anthropic", modelID: "claude-opus-4-6" },
},
},
{
info: {
agent: "compaction",
model: { providerID: "anthropic", modelID: "claude-sonnet-4-5" },
},
},
],
}),
},
}
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
const task: BackgroundTask = {
id: "task-skip-compaction",
sessionID: "session-child",
parentSessionID: "session-parent",
parentMessageID: "msg-parent",
description: "task with compaction at tail",
prompt: "test",
agent: "explore",
status: "completed",
startedAt: new Date(),
completedAt: new Date(),
parentAgent: "fallback-agent",
}
getPendingByParent(manager).set("session-parent", new Set([task.id, "still-running"]))
//#when
await (manager as unknown as { notifyParentSession: (value: BackgroundTask) => Promise<void> })
.notifyParentSession(task)
//#then
expect(capturedBody?.agent).toBe("sisyphus")
expect(capturedBody?.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" })
manager.shutdown()
})
test("should use currentMessage model/agent when available", async () => { test("should use currentMessage model/agent when available", async () => {
// given - currentMessage has model and agent // given - currentMessage has model and agent
const task: BackgroundTask = { const task: BackgroundTask = {

View File

@ -23,8 +23,8 @@ import {
import { subagentSessions } from "../claude-code-session-state" import { subagentSessions } from "../claude-code-session-state"
import { getTaskToastManager } from "../task-toast-manager" import { getTaskToastManager } from "../task-toast-manager"
import { findNearestMessageWithFields, MESSAGE_STORAGE } from "../hook-message-injector" import { MESSAGE_STORAGE, type StoredMessage } from "../hook-message-injector"
import { existsSync, readdirSync } from "node:fs" import { existsSync, readFileSync, readdirSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
type ProcessCleanupEvent = NodeJS.Signals | "beforeExit" | "exit" type ProcessCleanupEvent = NodeJS.Signals | "beforeExit" | "exit"
@ -1211,7 +1211,6 @@ export class BackgroundManager {
.filter(t => t.parentSessionID === task.parentSessionID && t.status !== "running" && t.status !== "pending") .filter(t => t.parentSessionID === task.parentSessionID && t.status !== "running" && t.status !== "pending")
: [] : []
if (this.enableParentSessionNotifications) {
const statusText = task.status === "completed" ? "COMPLETED" : task.status === "interrupt" ? "INTERRUPTED" : "CANCELLED" const statusText = task.status === "completed" ? "COMPLETED" : task.status === "interrupt" ? "INTERRUPTED" : "CANCELLED"
const errorInfo = task.error ? `\n**Error:** ${task.error}` : "" const errorInfo = task.error ? `\n**Error:** ${task.error}` : ""
@ -1247,6 +1246,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
let agent: string | undefined = task.parentAgent let agent: string | undefined = task.parentAgent
let model: { providerID: string; modelID: string } | undefined let model: { providerID: string; modelID: string } | undefined
if (this.enableParentSessionNotifications) {
try { try {
const messagesResp = await this.client.session.messages({ path: { id: task.parentSessionID } }) const messagesResp = await this.client.session.messages({ path: { id: task.parentSessionID } })
const messages = normalizeSDKResponse(messagesResp, [] as Array<{ const messages = normalizeSDKResponse(messagesResp, [] as Array<{
@ -1254,6 +1254,9 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
}>) }>)
for (let i = messages.length - 1; i >= 0; i--) { for (let i = messages.length - 1; i >= 0; i--) {
const info = messages[i].info const info = messages[i].info
if (isCompactionAgent(info?.agent)) {
continue
}
if (info?.agent || info?.model || (info?.modelID && info?.providerID)) { if (info?.agent || info?.model || (info?.modelID && info?.providerID)) {
agent = info.agent ?? task.parentAgent agent = info.agent ?? task.parentAgent
model = info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined) model = info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined)
@ -1268,7 +1271,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
}) })
} }
const messageDir = getMessageDir(task.parentSessionID) const messageDir = getMessageDir(task.parentSessionID)
const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null const currentMessage = messageDir ? findNearestMessageExcludingCompaction(messageDir) : null
agent = currentMessage?.agent ?? task.parentAgent agent = currentMessage?.agent ?? task.parentAgent
model = currentMessage?.model?.providerID && currentMessage?.model?.modelID model = currentMessage?.model?.providerID && currentMessage?.model?.modelID
? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID } ? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID }
@ -1715,3 +1718,57 @@ function getMessageDir(sessionID: string): string | null {
} }
return null return null
} }
function isCompactionAgent(agent: string | undefined): boolean {
return agent?.trim().toLowerCase() === "compaction"
}
function hasFullAgentAndModel(message: StoredMessage): boolean {
return !!message.agent &&
!isCompactionAgent(message.agent) &&
!!message.model?.providerID &&
!!message.model?.modelID
}
function hasPartialAgentOrModel(message: StoredMessage): boolean {
const hasAgent = !!message.agent && !isCompactionAgent(message.agent)
const hasModel = !!message.model?.providerID && !!message.model?.modelID
return hasAgent || hasModel
}
function findNearestMessageExcludingCompaction(messageDir: string): StoredMessage | null {
try {
const files = readdirSync(messageDir)
.filter((name) => name.endsWith(".json"))
.sort()
.reverse()
for (const file of files) {
try {
const content = readFileSync(join(messageDir, file), "utf-8")
const parsed = JSON.parse(content) as StoredMessage
if (hasFullAgentAndModel(parsed)) {
return parsed
}
} catch {
continue
}
}
for (const file of files) {
try {
const content = readFileSync(join(messageDir, file), "utf-8")
const parsed = JSON.parse(content) as StoredMessage
if (hasPartialAgentOrModel(parsed)) {
return parsed
}
} catch {
continue
}
}
} catch {
return null
}
return null
}