fix(cli-run): rely on continuation markers for completion

Use hook-written continuation marker state to gate run completion checks and remove the noisy event-stream shutdown timeout log in run mode.
This commit is contained in:
YeonGyu-Kim
2026-02-17 17:50:47 +09:00
parent 706ee61333
commit d0bd24bede
19 changed files with 401 additions and 74 deletions
@@ -0,0 +1 @@
export const CONTINUATION_MARKER_DIR = ".sisyphus/run-continuation"
@@ -0,0 +1,3 @@
export * from "./types"
export * from "./constants"
export * from "./storage"
@@ -0,0 +1,91 @@
import { afterEach, describe, expect, it } from "bun:test"
import { mkdtempSync, rmSync } from "node:fs"
import { join } from "node:path"
import { tmpdir } from "node:os"
import {
clearContinuationMarker,
isContinuationMarkerActive,
readContinuationMarker,
setContinuationMarkerSource,
} from "./storage"
const tempDirs: string[] = []
function createTempDir(): string {
const directory = mkdtempSync(join(tmpdir(), "omo-run-marker-"))
tempDirs.push(directory)
return directory
}
afterEach(() => {
while (tempDirs.length > 0) {
const directory = tempDirs.pop()
if (directory) {
rmSync(directory, { recursive: true, force: true })
}
}
})
describe("run-continuation-state storage", () => {
it("stores and reads per-source marker state", () => {
// given
const directory = createTempDir()
const sessionID = "ses_test"
// when
setContinuationMarkerSource(directory, sessionID, "todo", "active", "2 todos remaining")
setContinuationMarkerSource(directory, sessionID, "stop", "stopped", "user requested stop")
const marker = readContinuationMarker(directory, sessionID)
// then
expect(marker).not.toBeNull()
expect(marker?.sessionID).toBe(sessionID)
expect(marker?.sources.todo?.state).toBe("active")
expect(marker?.sources.todo?.reason).toBe("2 todos remaining")
expect(marker?.sources.stop?.state).toBe("stopped")
})
it("treats marker as active when any source is active", () => {
// given
const directory = createTempDir()
const sessionID = "ses_active"
setContinuationMarkerSource(directory, sessionID, "todo", "active", "pending")
setContinuationMarkerSource(directory, sessionID, "stop", "idle")
const marker = readContinuationMarker(directory, sessionID)
// when
const isActive = isContinuationMarkerActive(marker)
// then
expect(isActive).toBe(true)
})
it("returns inactive when no source is active", () => {
// given
const directory = createTempDir()
const sessionID = "ses_idle"
setContinuationMarkerSource(directory, sessionID, "todo", "idle")
setContinuationMarkerSource(directory, sessionID, "stop", "stopped")
const marker = readContinuationMarker(directory, sessionID)
// when
const isActive = isContinuationMarkerActive(marker)
// then
expect(isActive).toBe(false)
})
it("clears marker for a session", () => {
// given
const directory = createTempDir()
const sessionID = "ses_clear"
setContinuationMarkerSource(directory, sessionID, "todo", "active")
// when
clearContinuationMarker(directory, sessionID)
const marker = readContinuationMarker(directory, sessionID)
// then
expect(marker).toBeNull()
})
})
@@ -0,0 +1,80 @@
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { CONTINUATION_MARKER_DIR } from "./constants"
import type {
ContinuationMarker,
ContinuationMarkerSource,
ContinuationMarkerState,
} from "./types"
function getMarkerPath(directory: string, sessionID: string): string {
return join(directory, CONTINUATION_MARKER_DIR, `${sessionID}.json`)
}
export function readContinuationMarker(
directory: string,
sessionID: string,
): ContinuationMarker | null {
const markerPath = getMarkerPath(directory, sessionID)
if (!existsSync(markerPath)) return null
try {
const raw = readFileSync(markerPath, "utf-8")
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null
return parsed as ContinuationMarker
} catch {
return null
}
}
export function setContinuationMarkerSource(
directory: string,
sessionID: string,
source: ContinuationMarkerSource,
state: ContinuationMarkerState,
reason?: string,
): ContinuationMarker {
const now = new Date().toISOString()
const existing = readContinuationMarker(directory, sessionID)
const next: ContinuationMarker = {
sessionID,
updatedAt: now,
sources: {
...(existing?.sources ?? {}),
[source]: {
state,
...(reason ? { reason } : {}),
updatedAt: now,
},
},
}
const markerPath = getMarkerPath(directory, sessionID)
mkdirSync(join(directory, CONTINUATION_MARKER_DIR), { recursive: true })
writeFileSync(markerPath, JSON.stringify(next, null, 2), "utf-8")
return next
}
export function clearContinuationMarker(directory: string, sessionID: string): void {
const markerPath = getMarkerPath(directory, sessionID)
if (!existsSync(markerPath)) return
try {
rmSync(markerPath)
} catch {
}
}
export function isContinuationMarkerActive(marker: ContinuationMarker | null): boolean {
if (!marker) return false
return Object.values(marker.sources).some((entry) => entry?.state === "active")
}
export function getActiveContinuationMarkerReason(marker: ContinuationMarker | null): string | null {
if (!marker) return null
const active = Object.entries(marker.sources).find(([, entry]) => entry?.state === "active")
if (!active || !active[1]) return null
const [source, entry] = active
return entry.reason ?? `${source} continuation is active`
}
@@ -0,0 +1,15 @@
export type ContinuationMarkerSource = "todo" | "stop"
export type ContinuationMarkerState = "idle" | "active" | "stopped"
export interface ContinuationMarkerSourceEntry {
state: ContinuationMarkerState
reason?: string
updatedAt: string
}
export interface ContinuationMarker {
sessionID: string
updatedAt: string
sources: Partial<Record<ContinuationMarkerSource, ContinuationMarkerSourceEntry>>
}