fix(plugin): normalize event session ids
Handle OpenCode session events that carry the session ID under properties.info.id or properties.info.sessionID so background tasks and continuation hooks do not miss idle/error/delete events. Add regression coverage for nested session.idle events completing background tasks and waking continuation hooks.
This commit is contained in:
+44
-42
@@ -47,6 +47,7 @@ import type { CreatedHooks } from "../create-hooks";
|
||||
import type { Managers } from "../create-managers";
|
||||
import { pruneRecentSyntheticIdles } from "./recent-synthetic-idles";
|
||||
import { normalizeSessionStatusToIdle } from "./session-status-normalizer";
|
||||
import { resolveMessageEventSessionID, resolveSessionEventID } from "../shared/event-session-id";
|
||||
|
||||
type FirstMessageVariantGate = {
|
||||
markSessionCreated: (sessionInfo: { id?: string; title?: string; parentID?: string } | undefined) => void;
|
||||
@@ -235,15 +236,15 @@ export function createEventHandler(args: {
|
||||
|
||||
const getEventSessionID = (input: EventInput): string | undefined => {
|
||||
const properties = input.event.properties;
|
||||
if (
|
||||
!properties ||
|
||||
typeof properties !== "object" ||
|
||||
!("sessionID" in properties) ||
|
||||
typeof properties.sessionID !== "string"
|
||||
) {
|
||||
return undefined;
|
||||
if (input.event.type.startsWith("session.")) {
|
||||
return resolveSessionEventID(properties);
|
||||
}
|
||||
return properties.sessionID;
|
||||
if (input.event.type.startsWith("message.") || input.event.type.startsWith("tool.")) {
|
||||
return resolveMessageEventSessionID(properties);
|
||||
}
|
||||
const record: Record<string, unknown> | undefined = isRecord(properties) ? properties : undefined;
|
||||
const sessionID = record?.sessionID;
|
||||
return typeof sessionID === "string" && sessionID.length > 0 ? sessionID : undefined;
|
||||
};
|
||||
|
||||
const runEventHookSafely = async (
|
||||
@@ -467,10 +468,11 @@ export function createEventHandler(args: {
|
||||
|
||||
if (event.type === "session.created") {
|
||||
const sessionInfo = props?.info as { id?: string; title?: string; parentID?: string } | undefined;
|
||||
const isSubagentSession = !!sessionInfo?.parentID || !!sessionInfo?.id && subagentSessions.has(sessionInfo.id);
|
||||
const sessionID = resolveSessionEventID(props);
|
||||
const isSubagentSession = !!sessionInfo?.parentID || !!sessionID && subagentSessions.has(sessionID);
|
||||
|
||||
if (!isSubagentSession) {
|
||||
setMainSession(sessionInfo?.id);
|
||||
setMainSession(sessionID);
|
||||
}
|
||||
|
||||
firstMessageVariantGate.markSessionCreated(sessionInfo);
|
||||
@@ -489,62 +491,62 @@ export function createEventHandler(args: {
|
||||
|
||||
// Skip subagent sessions — they are dispatched by specialized callbacks
|
||||
// in create-managers.ts (async) and tool-registry.ts (sync)
|
||||
if (pluginConfig.openclaw && sessionInfo?.id && !isSubagentSession) {
|
||||
if (pluginConfig.openclaw && sessionID && !isSubagentSession) {
|
||||
await dispatchOpenClawEvent({
|
||||
config: pluginConfig.openclaw,
|
||||
rawEvent: event.type,
|
||||
context: {
|
||||
sessionId: sessionInfo.id,
|
||||
sessionId: sessionID,
|
||||
projectPath: pluginContext.directory,
|
||||
tmuxPaneId: managers.tmuxSessionManager.getTrackedPaneId?.(sessionInfo.id) ?? process.env.TMUX_PANE,
|
||||
tmuxPaneId: managers.tmuxSessionManager.getTrackedPaneId?.(sessionID) ?? process.env.TMUX_PANE,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "session.deleted") {
|
||||
const sessionInfo = props?.info as { id?: string } | undefined;
|
||||
if (sessionInfo?.id === getMainSessionID()) {
|
||||
const sessionID = resolveSessionEventID(props);
|
||||
if (sessionID === getMainSessionID()) {
|
||||
setMainSession(undefined);
|
||||
}
|
||||
|
||||
if (sessionInfo?.id) {
|
||||
const wasSyncSubagentSession = syncSubagentSessions.has(sessionInfo.id);
|
||||
clearSessionAgent(sessionInfo.id);
|
||||
lastHandledModelErrorMessageID.delete(sessionInfo.id);
|
||||
lastHandledRetryStatusKey.delete(sessionInfo.id);
|
||||
lastKnownModelBySession.delete(sessionInfo.id);
|
||||
if (sessionID) {
|
||||
const wasSyncSubagentSession = syncSubagentSessions.has(sessionID);
|
||||
clearSessionAgent(sessionID);
|
||||
lastHandledModelErrorMessageID.delete(sessionID);
|
||||
lastHandledRetryStatusKey.delete(sessionID);
|
||||
lastKnownModelBySession.delete(sessionID);
|
||||
if (modelFallback) {
|
||||
clearPendingModelFallback(modelFallback, sessionInfo.id);
|
||||
clearSessionFallbackChain(modelFallback, sessionInfo.id);
|
||||
clearPendingModelFallback(modelFallback, sessionID);
|
||||
clearSessionFallbackChain(modelFallback, sessionID);
|
||||
}
|
||||
resetMessageCursor(sessionInfo.id);
|
||||
clearBackgroundOutputConsumptionsForParentSession(sessionInfo.id);
|
||||
clearBackgroundOutputConsumptionsForTaskSession(sessionInfo.id);
|
||||
firstMessageVariantGate.clear(sessionInfo.id);
|
||||
clearSessionModel(sessionInfo.id);
|
||||
clearSessionPromptParams(sessionInfo.id);
|
||||
syncSubagentSessions.delete(sessionInfo.id);
|
||||
resetMessageCursor(sessionID);
|
||||
clearBackgroundOutputConsumptionsForParentSession(sessionID);
|
||||
clearBackgroundOutputConsumptionsForTaskSession(sessionID);
|
||||
firstMessageVariantGate.clear(sessionID);
|
||||
clearSessionModel(sessionID);
|
||||
clearSessionPromptParams(sessionID);
|
||||
syncSubagentSessions.delete(sessionID);
|
||||
if (pluginConfig.openclaw) {
|
||||
await dispatchOpenClawEvent({
|
||||
config: pluginConfig.openclaw,
|
||||
rawEvent: event.type,
|
||||
context: {
|
||||
sessionId: sessionInfo.id,
|
||||
sessionId: sessionID,
|
||||
projectPath: pluginContext.directory,
|
||||
tmuxPaneId: managers.tmuxSessionManager.getTrackedPaneId?.(sessionInfo.id) ?? process.env.TMUX_PANE,
|
||||
tmuxPaneId: managers.tmuxSessionManager.getTrackedPaneId?.(sessionID) ?? process.env.TMUX_PANE,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (wasSyncSubagentSession) {
|
||||
subagentSessions.delete(sessionInfo.id);
|
||||
subagentSessions.delete(sessionID);
|
||||
}
|
||||
deleteSessionTools(sessionInfo.id);
|
||||
await managers.skillMcpManager.disconnectSession(sessionInfo.id);
|
||||
deleteSessionTools(sessionID);
|
||||
await managers.skillMcpManager.disconnectSession(sessionID);
|
||||
await lspManager.cleanupTempDirectoryClients();
|
||||
if (tmuxIntegrationEnabled) {
|
||||
await managers.tmuxSessionManager.onSessionDeleted({
|
||||
sessionID: sessionInfo.id,
|
||||
sessionID,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -555,12 +557,12 @@ export function createEventHandler(args: {
|
||||
|
||||
if (event.type === "message.removed") {
|
||||
const messageID = props?.messageID as string | undefined;
|
||||
const sessionID = props?.sessionID as string | undefined;
|
||||
const sessionID = resolveMessageEventSessionID(props);
|
||||
restoreBackgroundOutputConsumption(sessionID, messageID);
|
||||
}
|
||||
|
||||
if (event.type === "session.idle" && pluginConfig.openclaw) {
|
||||
const sessionID = props?.sessionID as string | undefined;
|
||||
const sessionID = resolveSessionEventID(props);
|
||||
if (sessionID) {
|
||||
await dispatchOpenClawEvent({
|
||||
config: pluginConfig.openclaw,
|
||||
@@ -582,7 +584,7 @@ export function createEventHandler(args: {
|
||||
|
||||
if (event.type === "message.updated") {
|
||||
const info = props?.info as Record<string, unknown> | undefined;
|
||||
const sessionID = info?.sessionID as string | undefined;
|
||||
const sessionID = resolveMessageEventSessionID(props);
|
||||
const agent = info?.agent as string | undefined;
|
||||
const role = info?.role as string | undefined;
|
||||
if (sessionID && info?.finish === true) {
|
||||
@@ -665,7 +667,7 @@ export function createEventHandler(args: {
|
||||
}
|
||||
|
||||
if (event.type === "session.status") {
|
||||
const sessionID = props?.sessionID as string | undefined;
|
||||
const sessionID = resolveSessionEventID(props);
|
||||
const status = props?.status as { type?: string; attempt?: number; message?: string; next?: number } | undefined;
|
||||
|
||||
// Retry dedupe lifecycle: set key when a retry status is handled, clear it after recovery
|
||||
@@ -733,7 +735,7 @@ export function createEventHandler(args: {
|
||||
|
||||
if (event.type === "session.error") {
|
||||
try {
|
||||
const sessionID = props?.sessionID as string | undefined;
|
||||
const sessionID = resolveSessionEventID(props);
|
||||
const error = props?.error;
|
||||
|
||||
const errorName = extractErrorName(error);
|
||||
@@ -818,7 +820,7 @@ export function createEventHandler(args: {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const sessionID = props?.sessionID as string | undefined;
|
||||
const sessionID = resolveSessionEventID(props);
|
||||
log("[event] model-fallback error in session.error:", { sessionID, error: err });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user