fix: skip compaction messages in parent-session context lookup
This commit is contained in:
@@ -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 = {
|
||||||
|
|||||||
@@ -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,12 +1211,11 @@ 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}` : ""
|
|
||||||
|
|
||||||
let notification: string
|
let notification: string
|
||||||
if (allComplete) {
|
if (allComplete) {
|
||||||
const completedTasksText = completedTasks
|
const completedTasksText = completedTasks
|
||||||
.map(t => `- \`${t.id}\`: ${t.description}`)
|
.map(t => `- \`${t.id}\`: ${t.description}`)
|
||||||
.join("\n")
|
.join("\n")
|
||||||
@@ -1229,9 +1228,9 @@ ${completedTasksText || `- \`${task.id}\`: ${task.description}`}
|
|||||||
|
|
||||||
Use \`background_output(task_id="<id>")\` to retrieve each result.
|
Use \`background_output(task_id="<id>")\` to retrieve each result.
|
||||||
</system-reminder>`
|
</system-reminder>`
|
||||||
} else {
|
} else {
|
||||||
// Individual completion - silent notification
|
// Individual completion - silent notification
|
||||||
notification = `<system-reminder>
|
notification = `<system-reminder>
|
||||||
[BACKGROUND TASK ${statusText}]
|
[BACKGROUND TASK ${statusText}]
|
||||||
**ID:** \`${task.id}\`
|
**ID:** \`${task.id}\`
|
||||||
**Description:** ${task.description}
|
**Description:** ${task.description}
|
||||||
@@ -1242,77 +1241,81 @@ Do NOT poll - continue productive work.
|
|||||||
|
|
||||||
Use \`background_output(task_id="${task.id}")\` to retrieve this result when ready.
|
Use \`background_output(task_id="${task.id}")\` to retrieve this result when ready.
|
||||||
</system-reminder>`
|
</system-reminder>`
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
try {
|
if (this.enableParentSessionNotifications) {
|
||||||
const messagesResp = await this.client.session.messages({ path: { id: task.parentSessionID } })
|
try {
|
||||||
const messages = normalizeSDKResponse(messagesResp, [] as Array<{
|
const messagesResp = await this.client.session.messages({ path: { id: task.parentSessionID } })
|
||||||
info?: { agent?: string; model?: { providerID: string; modelID: string }; modelID?: string; providerID?: string }
|
const messages = normalizeSDKResponse(messagesResp, [] as Array<{
|
||||||
}>)
|
info?: { agent?: string; model?: { providerID: string; modelID: string }; modelID?: string; providerID?: string }
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
}>)
|
||||||
const info = messages[i].info
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
if (info?.agent || info?.model || (info?.modelID && info?.providerID)) {
|
const info = messages[i].info
|
||||||
agent = info.agent ?? task.parentAgent
|
if (isCompactionAgent(info?.agent)) {
|
||||||
model = info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined)
|
continue
|
||||||
break
|
}
|
||||||
|
if (info?.agent || info?.model || (info?.modelID && info?.providerID)) {
|
||||||
|
agent = info.agent ?? task.parentAgent
|
||||||
|
model = info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.isAbortedSessionError(error)) {
|
||||||
|
log("[background-agent] Parent session aborted while loading messages; using messageDir fallback:", {
|
||||||
|
taskId: task.id,
|
||||||
|
parentSessionID: task.parentSessionID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const messageDir = getMessageDir(task.parentSessionID)
|
||||||
|
const currentMessage = messageDir ? findNearestMessageExcludingCompaction(messageDir) : null
|
||||||
|
agent = currentMessage?.agent ?? task.parentAgent
|
||||||
|
model = currentMessage?.model?.providerID && currentMessage?.model?.modelID
|
||||||
|
? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[background-agent] notifyParentSession context:", {
|
||||||
|
taskId: task.id,
|
||||||
|
resolvedAgent: agent,
|
||||||
|
resolvedModel: model,
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.client.session.promptAsync({
|
||||||
|
path: { id: task.parentSessionID },
|
||||||
|
body: {
|
||||||
|
noReply: !allComplete,
|
||||||
|
...(agent !== undefined ? { agent } : {}),
|
||||||
|
...(model !== undefined ? { model } : {}),
|
||||||
|
...(task.parentTools ? { tools: task.parentTools } : {}),
|
||||||
|
parts: [{ type: "text", text: notification }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
log("[background-agent] Sent notification to parent session:", {
|
||||||
|
taskId: task.id,
|
||||||
|
allComplete,
|
||||||
|
noReply: !allComplete,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (this.isAbortedSessionError(error)) {
|
||||||
|
log("[background-agent] Parent session aborted while sending notification; continuing cleanup:", {
|
||||||
|
taskId: task.id,
|
||||||
|
parentSessionID: task.parentSessionID,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
log("[background-agent] Failed to send notification:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else {
|
||||||
if (this.isAbortedSessionError(error)) {
|
log("[background-agent] Parent session notifications disabled, skipping prompt injection:", {
|
||||||
log("[background-agent] Parent session aborted while loading messages; using messageDir fallback:", {
|
|
||||||
taskId: task.id,
|
|
||||||
parentSessionID: task.parentSessionID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const messageDir = getMessageDir(task.parentSessionID)
|
|
||||||
const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
|
||||||
agent = currentMessage?.agent ?? task.parentAgent
|
|
||||||
model = currentMessage?.model?.providerID && currentMessage?.model?.modelID
|
|
||||||
? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
log("[background-agent] notifyParentSession context:", {
|
|
||||||
taskId: task.id,
|
|
||||||
resolvedAgent: agent,
|
|
||||||
resolvedModel: model,
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.client.session.promptAsync({
|
|
||||||
path: { id: task.parentSessionID },
|
|
||||||
body: {
|
|
||||||
noReply: !allComplete,
|
|
||||||
...(agent !== undefined ? { agent } : {}),
|
|
||||||
...(model !== undefined ? { model } : {}),
|
|
||||||
...(task.parentTools ? { tools: task.parentTools } : {}),
|
|
||||||
parts: [{ type: "text", text: notification }],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
log("[background-agent] Sent notification to parent session:", {
|
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
allComplete,
|
parentSessionID: task.parentSessionID,
|
||||||
noReply: !allComplete,
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
|
||||||
if (this.isAbortedSessionError(error)) {
|
|
||||||
log("[background-agent] Parent session aborted while sending notification; continuing cleanup:", {
|
|
||||||
taskId: task.id,
|
|
||||||
parentSessionID: task.parentSessionID,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
log("[background-agent] Failed to send notification:", error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log("[background-agent] Parent session notifications disabled, skipping prompt injection:", {
|
|
||||||
taskId: task.id,
|
|
||||||
parentSessionID: task.parentSessionID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allComplete) {
|
if (allComplete) {
|
||||||
for (const completedTask of completedTasks) {
|
for (const completedTask of completedTasks) {
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user