From ed4d10c5977f34ab7d7fd2ab59e86ef96e9601aa Mon Sep 17 00:00:00 2001 From: Harsh Date: Fri, 6 Mar 2026 13:57:23 +0530 Subject: [PATCH] fix: harden opencode cleanup and ignore local sisyphus state --- .gitignore | 1 + .../src/__tests__/session-manager.test.ts | 25 +++++++++++++++++++ packages/core/src/session-manager.ts | 15 ++++++++--- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 9a9ed1648..fd0927cc7 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ id_ed25519 # Development symlinks (created per-worktree, not committed) .claude +.sisyphus packages/web/agent-orchestrator.yaml # Local agent orchestrator config (may contain secrets) diff --git a/packages/core/src/__tests__/session-manager.test.ts b/packages/core/src/__tests__/session-manager.test.ts index 5f4eadd76..7dae83861 100644 --- a/packages/core/src/__tests__/session-manager.test.ts +++ b/packages/core/src/__tests__/session-manager.test.ts @@ -1639,6 +1639,31 @@ describe("cleanup", () => { expect(deleteLog).toContain("session delete ses_archived"); }); + it("skips invalid archived OpenCode session ids during cleanup", async () => { + const deleteLogPath = join(tmpDir, "opencode-delete-archived-invalid.log"); + const mockBin = installMockOpencode("[]", deleteLogPath); + process.env.PATH = `${mockBin}:${originalPath ?? ""}`; + + writeMetadata(sessionsDir, "app-8", { + worktree: "/tmp", + branch: "main", + status: "spawning", + project: "my-app", + agent: "opencode", + opencodeSessionId: "ses bad id", + runtimeHandle: JSON.stringify(makeHandle("rt-8")), + }); + deleteMetadata(sessionsDir, "app-8", true); + + const sm = createSessionManager({ config, registry: mockRegistry }); + const result = await sm.cleanup(); + + expect(result.killed).not.toContain("app-8"); + expect(result.errors).toEqual([]); + expect(result.skipped).toContain("app-8"); + expect(existsSync(deleteLogPath)).toBe(false); + }); + it("does not delete archived OpenCode sessions in cleanup dry-run", async () => { const deleteLogPath = join(tmpDir, "opencode-delete-archived-dry-run.log"); const mockBin = installMockOpencode("[]", deleteLogPath); diff --git a/packages/core/src/session-manager.ts b/packages/core/src/session-manager.ts index 6feead8b2..8c8fe045f 100644 --- a/packages/core/src/session-manager.ts +++ b/packages/core/src/session-manager.ts @@ -1080,7 +1080,10 @@ export function createSessionManager(deps: SessionManagerDeps): SessionManager { const plugins = resolvePlugins(project, selectedAgentName); // Cap per-session enrichment at 2s — subprocess calls (tmux/ps) can be // slow under load. If we time out, session keeps its metadata values. - const enrichTimeout = new Promise((resolve) => setTimeout(resolve, 2_000)); + let enrichTimeoutId: ReturnType | null = null; + const enrichTimeout = new Promise((resolve) => { + enrichTimeoutId = setTimeout(resolve, 2_000); + }); const enrichPromise = ensureHandleAndEnrich( session, sessionName, @@ -1089,7 +1092,13 @@ export function createSessionManager(deps: SessionManagerDeps): SessionManager { selectedAgentName, plugins, ).catch(() => {}); - await Promise.race([enrichPromise, enrichTimeout]); + try { + await Promise.race([enrichPromise, enrichTimeout]); + } finally { + if (enrichTimeoutId) { + clearTimeout(enrichTimeoutId); + } + } return session; }, @@ -1316,7 +1325,7 @@ export function createSessionManager(deps: SessionManagerDeps): SessionManager { } const cleanupAgent = archived["agent"] ?? project.agent ?? config.defaults.agent; - const mappedOpenCodeSessionId = archived["opencodeSessionId"]; + const mappedOpenCodeSessionId = asValidOpenCodeSessionId(archived["opencodeSessionId"]); if (cleanupAgent === "opencode" && archived["opencodeCleanedAt"]) { pushUnique(result.skipped, archivedId); continue;