Files
oh-my-opencode/src/hooks/prometheus-md-only/agent-resolution.ts
T
conversun 22b4b30dd7 fix(todo-continuation-enforcer): prevent post-compaction agent fallback to General
After compaction, message history is truncated and the original agent
(e.g. Prometheus) can no longer be resolved from messages. The todo
continuation enforcer would then inject a continuation prompt with
agent=undefined, causing the host to default to General -- which has
write permissions Prometheus should never have.

Root cause chain:
1. handler.ts had no session.compacted handler (unlike Atlas)
2. idle-event.ts relied on finding a compaction marker in truncated
   message history -- the marker disappears after real compaction
3. continuation-injection.ts proceeded when agentName was undefined
   because the skipAgents check only matched truthy agent names
4. prometheus-md-only/agent-resolution.ts did not filter compaction
   agent from message history fallback results

Fixes:
- Add session.compacted handler that sets hasRecentCompaction state flag
- Replace fragile history-based compaction detection with state flag
- Block continuation injection when agent is unknown post-compaction
- Filter compaction agent in Prometheus agent resolution fallback
2026-03-13 00:36:03 +08:00

71 lines
2.6 KiB
TypeScript

import type { PluginInput } from "@opencode-ai/plugin"
import { findNearestMessageWithFields, findFirstMessageWithAgent } from "../../features/hook-message-injector"
import {
findFirstMessageWithAgentFromSDK,
findNearestMessageWithFieldsFromSDK,
} from "../../features/hook-message-injector"
import { getSessionAgent } from "../../features/claude-code-session-state"
import { readBoulderState } from "../../features/boulder-state"
import { getMessageDir } from "../../shared/opencode-message-dir"
import { isSqliteBackend } from "../../shared/opencode-storage-detection"
type OpencodeClient = PluginInput["client"]
function isCompactionAgent(agent: string): boolean {
return agent.toLowerCase() === "compaction"
}
async function getAgentFromMessageFiles(
sessionID: string,
client?: OpencodeClient
): Promise<string | undefined> {
if (isSqliteBackend() && client) {
const firstAgent = await findFirstMessageWithAgentFromSDK(client, sessionID)
if (firstAgent && !isCompactionAgent(firstAgent)) return firstAgent
const nearest = await findNearestMessageWithFieldsFromSDK(client, sessionID)
if (nearest?.agent && !isCompactionAgent(nearest.agent)) return nearest.agent
return undefined
}
const messageDir = getMessageDir(sessionID)
if (!messageDir) return undefined
const firstAgent = findFirstMessageWithAgent(messageDir)
if (firstAgent && !isCompactionAgent(firstAgent)) return firstAgent
const nearestAgent = findNearestMessageWithFields(messageDir)?.agent
if (nearestAgent && !isCompactionAgent(nearestAgent)) return nearestAgent
return undefined
}
/**
* Get the effective agent for the session.
* Priority order:
* 1. In-memory session agent (most recent, set by /start-work)
* 2. Boulder state agent (persisted across restarts, fixes #927)
* 3. Message files (fallback for sessions without boulder state)
*
* This fixes issue #927 where after interruption:
* - In-memory map is cleared (process restart)
* - Message files return "prometheus" (oldest message from /plan)
* - But boulder.json has agent: "atlas" (set by /start-work)
*/
export async function getAgentFromSession(
sessionID: string,
directory: string,
client?: OpencodeClient
): Promise<string | undefined> {
// Check in-memory first (current session)
const memoryAgent = getSessionAgent(sessionID)
if (memoryAgent) return memoryAgent
// Check boulder state (persisted across restarts) - fixes #927
const boulderState = readBoulderState(directory)
if (boulderState?.session_ids?.includes(sessionID) && boulderState.agent) {
return boulderState.agent
}
// Fallback to message files
return await getAgentFromMessageFiles(sessionID, client)
}