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:
@@ -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>>
|
||||
}
|
||||
Reference in New Issue
Block a user