fix(tmux-subagent): avoid transcript fetches during idle stability checks
This commit is contained in:
@@ -923,6 +923,10 @@ export class TmuxSessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
onEvent(event: { type: string; properties?: Record<string, unknown> }): void {
|
||||
this.pollingManager.handleEvent(event)
|
||||
}
|
||||
|
||||
createEventHandler(): (input: { event: { type: string; properties?: unknown } }) => Promise<void> {
|
||||
return async (input) => {
|
||||
await this.onSessionCreated(input.event as SessionCreatedEvent)
|
||||
|
||||
@@ -55,4 +55,55 @@ describe("TmuxPollingManager overlap", () => {
|
||||
expect(maxActiveCalls).toBe(1)
|
||||
expect(statusCallCount).toBe(1)
|
||||
})
|
||||
|
||||
test("closes stable idle sessions without fetching full messages when activity was already observed from events", async () => {
|
||||
//#given
|
||||
const sessions = new Map<string, TrackedSession>()
|
||||
sessions.set("ses-1", {
|
||||
sessionId: "ses-1",
|
||||
paneId: "%1",
|
||||
description: "test",
|
||||
createdAt: new Date(Date.now() - 15_000),
|
||||
lastSeenAt: new Date(),
|
||||
closePending: false,
|
||||
closeRetryCount: 0,
|
||||
activityVersion: 0,
|
||||
})
|
||||
|
||||
let messagesCallCount = 0
|
||||
const closedSessionIds: string[] = []
|
||||
const client = {
|
||||
session: {
|
||||
status: async () => ({ data: { "ses-1": { type: "idle" } } }),
|
||||
messages: async () => {
|
||||
messagesCallCount += 1
|
||||
return { data: [] }
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const manager = new TmuxPollingManager(
|
||||
client as unknown as import("../../tools/delegate-task/types").OpencodeClient,
|
||||
sessions,
|
||||
async (sessionId) => {
|
||||
closedSessionIds.push(sessionId)
|
||||
},
|
||||
)
|
||||
|
||||
manager.handleEvent({
|
||||
type: "message.part.delta",
|
||||
properties: { sessionID: "ses-1", field: "text", delta: "done" },
|
||||
})
|
||||
|
||||
//#when
|
||||
const pollSessions = (manager as unknown as { pollSessions: () => Promise<void> }).pollSessions
|
||||
await pollSessions.call(manager)
|
||||
await pollSessions.call(manager)
|
||||
await pollSessions.call(manager)
|
||||
await pollSessions.call(manager)
|
||||
|
||||
//#then
|
||||
expect(messagesCallCount).toBe(0)
|
||||
expect(closedSessionIds).toEqual(["ses-1"])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,6 +19,16 @@ export class TmuxPollingManager {
|
||||
private closeSessionById: (sessionId: string) => Promise<void>
|
||||
) {}
|
||||
|
||||
handleEvent(event: { type: string; properties?: Record<string, unknown> }): void {
|
||||
const sessionId = this.getEventSessionId(event)
|
||||
if (!sessionId) return
|
||||
|
||||
const tracked = this.sessions.get(sessionId)
|
||||
if (!tracked) return
|
||||
|
||||
tracked.activityVersion = (tracked.activityVersion ?? 0) + 1
|
||||
}
|
||||
|
||||
startPolling(): void {
|
||||
if (this.pollInterval) return
|
||||
|
||||
@@ -73,42 +83,29 @@ export class TmuxPollingManager {
|
||||
let shouldCloseViaStability = false
|
||||
|
||||
if (isIdle && elapsedMs >= MIN_STABILITY_TIME_MS) {
|
||||
try {
|
||||
const messagesResult = await this.client.session.messages({
|
||||
path: { id: sessionId }
|
||||
})
|
||||
const currentMsgCount = Array.isArray(messagesResult.data)
|
||||
? messagesResult.data.length
|
||||
: 0
|
||||
const activityVersion = tracked.activityVersion ?? 0
|
||||
|
||||
if (tracked.lastMessageCount === currentMsgCount) {
|
||||
tracked.stableIdlePolls = (tracked.stableIdlePolls ?? 0) + 1
|
||||
|
||||
if (tracked.stableIdlePolls >= STABLE_POLLS_REQUIRED) {
|
||||
const recheckResult = await this.client.session.status({ path: undefined })
|
||||
const recheckStatuses = normalizeSDKResponse(recheckResult, {} as Record<string, { type: string }>)
|
||||
const recheckStatus = recheckStatuses[sessionId]
|
||||
|
||||
if (recheckStatus?.type === "idle") {
|
||||
shouldCloseViaStability = true
|
||||
} else {
|
||||
tracked.stableIdlePolls = 0
|
||||
log("[tmux-session-manager] stability reached but session not idle on recheck, resetting", {
|
||||
sessionId,
|
||||
recheckStatus: recheckStatus?.type,
|
||||
})
|
||||
}
|
||||
if (tracked.observedIdleActivityVersion === activityVersion) {
|
||||
tracked.stableIdlePolls = (tracked.stableIdlePolls ?? 0) + 1
|
||||
|
||||
if (tracked.stableIdlePolls >= STABLE_POLLS_REQUIRED) {
|
||||
const recheckResult = await this.client.session.status({ path: undefined })
|
||||
const recheckStatuses = normalizeSDKResponse(recheckResult, {} as Record<string, { type: string }>)
|
||||
const recheckStatus = recheckStatuses[sessionId]
|
||||
|
||||
if (recheckStatus?.type === "idle") {
|
||||
shouldCloseViaStability = true
|
||||
} else {
|
||||
tracked.stableIdlePolls = 0
|
||||
log("[tmux-session-manager] stability reached but session not idle on recheck, resetting", {
|
||||
sessionId,
|
||||
recheckStatus: recheckStatus?.type,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
tracked.stableIdlePolls = 0
|
||||
}
|
||||
|
||||
tracked.lastMessageCount = currentMsgCount
|
||||
} catch (msgErr) {
|
||||
log("[tmux-session-manager] failed to fetch messages for stability check", {
|
||||
sessionId,
|
||||
error: String(msgErr),
|
||||
})
|
||||
} else {
|
||||
tracked.stableIdlePolls = 0
|
||||
tracked.observedIdleActivityVersion = activityVersion
|
||||
}
|
||||
} else if (!isIdle) {
|
||||
tracked.stableIdlePolls = 0
|
||||
@@ -120,7 +117,8 @@ export class TmuxPollingManager {
|
||||
isIdle,
|
||||
elapsedMs,
|
||||
stableIdlePolls: tracked.stableIdlePolls,
|
||||
lastMessageCount: tracked.lastMessageCount,
|
||||
activityVersion: tracked.activityVersion,
|
||||
observedIdleActivityVersion: tracked.observedIdleActivityVersion,
|
||||
missingSince,
|
||||
missingTooLong,
|
||||
isTimedOut,
|
||||
@@ -142,4 +140,28 @@ export class TmuxPollingManager {
|
||||
this.pollingInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
private getEventSessionId(event: { type: string; properties?: Record<string, unknown> }): string | undefined {
|
||||
const properties = event.properties
|
||||
if (!properties) return undefined
|
||||
|
||||
if (event.type === "message.updated") {
|
||||
const info = properties.info
|
||||
if (!info || typeof info !== "object") return undefined
|
||||
const sessionId = (info as { sessionID?: unknown }).sessionID
|
||||
return typeof sessionId === "string" ? sessionId : undefined
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === "message.part.updated"
|
||||
|| event.type === "message.part.delta"
|
||||
|| event.type === "message.part.removed"
|
||||
|| event.type === "message.removed"
|
||||
) {
|
||||
const sessionId = properties.sessionID
|
||||
return typeof sessionId === "string" ? sessionId : undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export function createTrackedSession(params: {
|
||||
lastSeenAt: now,
|
||||
closePending: false,
|
||||
closeRetryCount: 0,
|
||||
activityVersion: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ export interface TrackedSession {
|
||||
// Stability detection fields (prevents premature closure)
|
||||
lastMessageCount?: number
|
||||
stableIdlePolls?: number
|
||||
activityVersion?: number
|
||||
observedIdleActivityVersion?: number
|
||||
}
|
||||
|
||||
export const MIN_PANE_WIDTH = 52
|
||||
|
||||
Reference in New Issue
Block a user