From 00137269cbafd77c8394c0a23829e8f52f699cbd Mon Sep 17 00:00:00 2001 From: Evangelos Moschou Date: Fri, 19 Jun 2026 20:53:13 +0300 Subject: [PATCH 1/2] feat(hooks): add V4 checkpoint writer for long DeepSeek V4 sessions DeepSeek V4 loses coherence on long sessions. MiMo Code's research showed that periodic state extraction at intervals keeps context lean and enables crash recovery. This is a simplified version: every 20 tool calls, writes a checkpoint file with session ID, model, tool call count, and timestamp. The hook: - Tracks V4 sessions from message.updated events - Every 20 tool calls, writes checkpoint to .omo/checkpoints/{sessionID}.json - Non-V4 sessions are unaffected (zero overhead) - Gated behind disabled_hooks as 'v4-checkpoint-writer' TDD: 4 tests (20 calls -> checkpoint, 19 -> no checkpoint, non-V4 -> none, 40 -> 2nd checkpoint). All existing tool-guard composition tests still pass. --- .../omo-opencode/src/config/schema/hooks.ts | 1 + packages/omo-opencode/src/hooks/index.ts | 1 + .../hooks/v4-checkpoint-writer/hook.test.ts | 146 ++++++++++++++++++ .../src/hooks/v4-checkpoint-writer/hook.ts | 104 +++++++++++++ .../src/hooks/v4-checkpoint-writer/index.ts | 1 + .../plugin/hooks/create-tool-guard-hooks.ts | 7 + 6 files changed, 260 insertions(+) create mode 100644 packages/omo-opencode/src/hooks/v4-checkpoint-writer/hook.test.ts create mode 100644 packages/omo-opencode/src/hooks/v4-checkpoint-writer/hook.ts create mode 100644 packages/omo-opencode/src/hooks/v4-checkpoint-writer/index.ts diff --git a/packages/omo-opencode/src/config/schema/hooks.ts b/packages/omo-opencode/src/config/schema/hooks.ts index c243e882356..3deec422228 100644 --- a/packages/omo-opencode/src/config/schema/hooks.ts +++ b/packages/omo-opencode/src/config/schema/hooks.ts @@ -58,6 +58,7 @@ export const HookNameSchema = z.enum([ "webfetch-redirect-guard", "fsync-skip-warning", "plan-format-validator", + "v4-checkpoint-writer", "legacy-plugin-toast", ]) diff --git a/packages/omo-opencode/src/hooks/index.ts b/packages/omo-opencode/src/hooks/index.ts index 537145e095b..d7501a7dbee 100644 --- a/packages/omo-opencode/src/hooks/index.ts +++ b/packages/omo-opencode/src/hooks/index.ts @@ -69,3 +69,4 @@ export { createFsyncSkipWarningHook } from "./fsync-skip-warning" export { createNotepadWriteGuardHook } from "./notepad-write-guard" export { createPlanFormatValidatorHook } from "./plan-format-validator" export { createMonitorStatusInjectorHook } from "./monitor-status-injector" +export { createV4CheckpointWriterHook } from "./v4-checkpoint-writer" diff --git a/packages/omo-opencode/src/hooks/v4-checkpoint-writer/hook.test.ts b/packages/omo-opencode/src/hooks/v4-checkpoint-writer/hook.test.ts new file mode 100644 index 00000000000..a63c3868a43 --- /dev/null +++ b/packages/omo-opencode/src/hooks/v4-checkpoint-writer/hook.test.ts @@ -0,0 +1,146 @@ +/// + +import { describe, expect, test, mock, afterAll, beforeEach, afterEach } from "bun:test" +import { existsSync, readFileSync, rmSync, mkdtempSync } from "node:fs" +import { join } from "node:path" +import { tmpdir } from "node:os" + +const logMock = mock(() => {}) + +mock.module("../../shared/logger", () => ({ + log: logMock, +})) + +afterAll(() => { + mock.restore() +}) + +const { createV4CheckpointWriterHook } = await import("./hook") + +describe("v4-checkpoint-writer", () => { + let tempDir: string + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "v4-checkpoint-test-")) + }) + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }) + }) + + test("#given V4 session reaching 20 tool calls #when tool.execute.after runs #then writes checkpoint file", () => { + // given + const hook = createV4CheckpointWriterHook({ directory: tempDir }) + const sessionID = "ses_v4_checkpoint" + + // simulate model detection + hook.event({ + event: { + type: "message.updated", + properties: { + info: { sessionID, modelID: "deepseek/deepseek-v4-pro", role: "assistant" }, + }, + }, + }) + + // when — simulate 20 tool calls + for (let i = 0; i < 20; i++) { + hook["tool.execute.after"]( + { tool: "bash", sessionID, callID: `call_${i}` }, + { title: "", output: "result", metadata: null }, + ) + } + + // then + const checkpointPath = join(tempDir, ".omo/checkpoints", `${sessionID}.json`) + expect(existsSync(checkpointPath)).toBe(true) + const checkpoint = JSON.parse(readFileSync(checkpointPath, "utf8")) + expect(checkpoint.sessionID).toBe(sessionID) + expect(checkpoint.modelID).toBe("deepseek/deepseek-v4-pro") + expect(checkpoint.toolCallCount).toBe(20) + expect(checkpoint.lastToolName).toBe("bash") + }) + + test("#given V4 session at 19 tool calls #when tool.execute.after runs #then does NOT write checkpoint", () => { + // given + const hook = createV4CheckpointWriterHook({ directory: tempDir }) + const sessionID = "ses_v4_no_checkpoint" + + hook.event({ + event: { + type: "message.updated", + properties: { + info: { sessionID, modelID: "deepseek/deepseek-v4-flash", role: "assistant" }, + }, + }, + }) + + // when — 19 tool calls (one short of the 20-call interval) + for (let i = 0; i < 19; i++) { + hook["tool.execute.after"]( + { tool: "read", sessionID, callID: `call_${i}` }, + { title: "", output: "result", metadata: null }, + ) + } + + // then + const checkpointPath = join(tempDir, ".omo/checkpoints", `${sessionID}.json`) + expect(existsSync(checkpointPath)).toBe(false) + }) + + test("#given non-V4 session at 20 tool calls #when tool.execute.after runs #then does NOT write checkpoint", () => { + // given + const hook = createV4CheckpointWriterHook({ directory: tempDir }) + const sessionID = "ses_non_v4" + + hook.event({ + event: { + type: "message.updated", + properties: { + info: { sessionID, modelID: "anthropic/claude-sonnet-4-6", role: "assistant" }, + }, + }, + }) + + // when + for (let i = 0; i < 20; i++) { + hook["tool.execute.after"]( + { tool: "bash", sessionID, callID: `call_${i}` }, + { title: "", output: "result", metadata: null }, + ) + } + + // then + const checkpointPath = join(tempDir, ".omo/checkpoints", `${sessionID}.json`) + expect(existsSync(checkpointPath)).toBe(false) + }) + + test("#given V4 session reaching 40 tool calls #when tool.execute.after runs #then writes 2nd checkpoint", () => { + // given + const hook = createV4CheckpointWriterHook({ directory: tempDir }) + const sessionID = "ses_v4_40" + + hook.event({ + event: { + type: "message.updated", + properties: { + info: { sessionID, modelID: "deepseek/deepseek-v4-pro", role: "assistant" }, + }, + }, + }) + + // when — 40 tool calls (two intervals) + for (let i = 0; i < 40; i++) { + hook["tool.execute.after"]( + { tool: "edit", sessionID, callID: `call_${i}` }, + { title: "", output: "result", metadata: null }, + ) + } + + // then — checkpoint should show 40 calls + const checkpointPath = join(tempDir, ".omo/checkpoints", `${sessionID}.json`) + expect(existsSync(checkpointPath)).toBe(true) + const checkpoint = JSON.parse(readFileSync(checkpointPath, "utf8")) + expect(checkpoint.toolCallCount).toBe(40) + }) +}) diff --git a/packages/omo-opencode/src/hooks/v4-checkpoint-writer/hook.ts b/packages/omo-opencode/src/hooks/v4-checkpoint-writer/hook.ts new file mode 100644 index 00000000000..91c64574651 --- /dev/null +++ b/packages/omo-opencode/src/hooks/v4-checkpoint-writer/hook.ts @@ -0,0 +1,104 @@ +import { writeFileSync, mkdirSync, existsSync } from "node:fs" +import { join, dirname } from "node:path" +import { homedir } from "node:os" +import { log } from "../../shared/logger" + +const CHECKPOINT_INTERVAL = 20 +const CHECKPOINT_DIR_NAME = ".omo/checkpoints" + +function isV4Model(modelID: string): boolean { + const lower = modelID.toLowerCase() + return lower.includes("deepseek-v4") || lower.includes("deepseek_v4") +} + +function resolveCheckpointDir(directory?: string): string { + if (directory) return join(directory, CHECKPOINT_DIR_NAME) + return join(homedir(), ".omo", "checkpoints") +} + +type SessionState = { + modelID: string + toolCallCount: number + lastToolName: string + lastCheckpointAt: number +} + +type SessionStateCache = Map + +export function createV4CheckpointWriterHook(options?: { directory?: string }) { + const sessionStates: SessionStateCache = new Map() + const checkpointDir = resolveCheckpointDir(options?.directory) + + return { + event: (input: { + event: { + type: string + properties: { + info?: { + sessionID?: string + modelID?: string + role?: string + } + } + } + }): void => { + if (input.event.type !== "message.updated") return + const info = input.event.properties?.info + if (!info?.modelID || !info?.sessionID) return + if (!isV4Model(info.modelID)) return + + const existing = sessionStates.get(info.sessionID) + if (!existing) { + sessionStates.set(info.sessionID, { + modelID: info.modelID, + toolCallCount: 0, + lastToolName: "", + lastCheckpointAt: 0, + }) + } else { + existing.modelID = info.modelID + } + }, + + "tool.execute.after": ( + input: { tool: string; sessionID: string; callID: string }, + _output?: { title?: string; output?: string; metadata?: unknown }, + ): void => { + const state = sessionStates.get(input.sessionID) + if (!state) return + + state.toolCallCount++ + state.lastToolName = input.tool + + if (state.toolCallCount % CHECKPOINT_INTERVAL !== 0) return + + const now = Date.now() + state.lastCheckpointAt = now + + const checkpoint = { + sessionID: input.sessionID, + modelID: state.modelID, + toolCallCount: state.toolCallCount, + lastToolName: state.lastToolName, + timestamp: new Date(now).toISOString(), + } + + try { + if (!existsSync(checkpointDir)) { + mkdirSync(checkpointDir, { recursive: true }) + } + const filePath = join(checkpointDir, `${input.sessionID}.json`) + writeFileSync(filePath, JSON.stringify(checkpoint, null, 2) + "\n", "utf8") + log("[v4-checkpoint-writer] Checkpoint written", { + sessionID: input.sessionID, + toolCallCount: state.toolCallCount, + }) + } catch (error) { + log("[v4-checkpoint-writer] Failed to write checkpoint", { + sessionID: input.sessionID, + error: String(error), + }) + } + }, + } +} diff --git a/packages/omo-opencode/src/hooks/v4-checkpoint-writer/index.ts b/packages/omo-opencode/src/hooks/v4-checkpoint-writer/index.ts new file mode 100644 index 00000000000..38bfb2fe112 --- /dev/null +++ b/packages/omo-opencode/src/hooks/v4-checkpoint-writer/index.ts @@ -0,0 +1 @@ +export { createV4CheckpointWriterHook } from "./hook" diff --git a/packages/omo-opencode/src/plugin/hooks/create-tool-guard-hooks.ts b/packages/omo-opencode/src/plugin/hooks/create-tool-guard-hooks.ts index 0ddfea11815..a01862de22e 100644 --- a/packages/omo-opencode/src/plugin/hooks/create-tool-guard-hooks.ts +++ b/packages/omo-opencode/src/plugin/hooks/create-tool-guard-hooks.ts @@ -21,6 +21,7 @@ import { createFsyncSkipWarningHook, createNotepadWriteGuardHook, createPlanFormatValidatorHook, + createV4CheckpointWriterHook, } from "../../hooks" import { getOpenCodeVersion, @@ -49,6 +50,7 @@ export type ToolGuardHooks = { teamToolGating: ReturnType | null notepadWriteGuard: ReturnType | null planFormatValidator: ReturnType | null + v4CheckpointWriter: ReturnType | null } export function createToolGuardHooks(args: { @@ -157,6 +159,10 @@ export function createToolGuardHooks(args: { ? safeHook("notepad-write-guard", () => createNotepadWriteGuardHook()) : null + const v4CheckpointWriter = isHookEnabled("v4-checkpoint-writer") + ? safeHook("v4-checkpoint-writer", () => createV4CheckpointWriterHook({ directory: ctx.directory })) + : null + return { commentChecker, toolOutputTruncator, @@ -176,5 +182,6 @@ export function createToolGuardHooks(args: { teamToolGating, notepadWriteGuard, planFormatValidator, + v4CheckpointWriter, } } From 205693ab3d1587e2172630b1b46f6104f6ecd9a6 Mon Sep 17 00:00:00 2001 From: Evangelos Moschou Date: Fri, 19 Jun 2026 22:04:32 +0300 Subject: [PATCH 2/2] ci: re-trigger pipeline after windows-latest test failure